...
This commit is contained in:
parent
7ce24cc631
commit
d804c7061b
@ -68,11 +68,19 @@ compose.desktop {
|
|||||||
isEnabled.set(false) // 임시로 false 설정
|
isEnabled.set(false) // 임시로 false 설정
|
||||||
}
|
}
|
||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
targetFormats(TargetFormat.Dmg)
|
targetFormats(TargetFormat.Dmg, TargetFormat.Exe, TargetFormat.Msi)
|
||||||
packageName = "AutoTradeAI"
|
packageName = "AutoTradeAI"
|
||||||
|
|
||||||
macOS {
|
macOS {
|
||||||
bundleID = "com.autotrade.ai"
|
bundleID = "com.autotrade.ai"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 윈도우 관련 상세 설정 (선택 사항)
|
||||||
|
windows {
|
||||||
|
packageVersion = "1.0.0"
|
||||||
|
shortcut = true // 바탕화면 바로가기 생성
|
||||||
|
menu = true // 시작 메뉴 등록
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,9 +14,12 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.window.Tray
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.WindowPlacement
|
import androidx.compose.ui.window.WindowPlacement
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
|
import androidx.compose.ui.window.rememberTrayState
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
@ -75,14 +78,47 @@ fun getLlamaBinPath(): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
|
|
||||||
|
val trayState = rememberTrayState()
|
||||||
|
var isWindowOpen by remember { mutableStateOf(true) } // 창의 표시 상태 관리
|
||||||
|
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
SystemSleepPreventer.start()
|
SystemSleepPreventer.start()
|
||||||
|
AutoTradingManager.startBackgroundScheduler()
|
||||||
|
}
|
||||||
|
LaunchedEffect(AutoTradingManager.shouldShowFullWindow) {
|
||||||
|
if (AutoTradingManager.shouldShowFullWindow) {
|
||||||
|
isWindowOpen = true
|
||||||
|
// 신호를 처리했으므로 다시 초기화 (트레이에서 수동으로 닫았을 때 다시 뜰 수 있게 함)
|
||||||
|
AutoTradingManager.shouldShowFullWindow = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 트레이 아이콘 설정
|
||||||
|
Tray(
|
||||||
|
state = trayState,
|
||||||
|
icon = painterResource("neko.png"), // resources 폴더에 아이콘 파일 필요
|
||||||
|
tooltip = "KIS AI 자동매매",
|
||||||
|
onAction = { isWindowOpen = true }, // 트레이 아이콘 더블클릭 시 창 열기
|
||||||
|
menu = {
|
||||||
|
Item("앱 열기", onClick = { isWindowOpen = true })
|
||||||
|
Separator()
|
||||||
|
Item("종료", onClick = {
|
||||||
|
// 종료 전 리소스 정리 호출
|
||||||
|
AutoTradingManager.stopDiscovery()
|
||||||
|
exitApplication()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// 앱 실행 시 필요한 바이너리 경로 (실행 파일 위치)
|
// 앱 실행 시 필요한 바이너리 경로 (실행 파일 위치)
|
||||||
val binPath = getLlamaBinPath()
|
val binPath = getLlamaBinPath()
|
||||||
val windowState = rememberWindowState(
|
val windowState = rememberWindowState(
|
||||||
placement = WindowPlacement.Floating
|
placement = WindowPlacement.Floating
|
||||||
)
|
)
|
||||||
Window(onCloseRequest = ::exitApplication, title = "KIS AI 자동매매", state = windowState) {
|
if (isWindowOpen) {
|
||||||
|
|
||||||
|
Window(onCloseRequest = { isWindowOpen = false }, title = "KIS AI 자동매매", state = windowState) {
|
||||||
var currentScreen by remember { mutableStateOf(AppScreen.Settings) }
|
var currentScreen by remember { mutableStateOf(AppScreen.Settings) }
|
||||||
var isLoaded by remember { mutableStateOf(false) }
|
var isLoaded by remember { mutableStateOf(false) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@ -142,6 +178,7 @@ fun main() = application {
|
|||||||
AutoTradingManager.onMarketClosed = {
|
AutoTradingManager.onMarketClosed = {
|
||||||
println("프로그램 초기화 실행됨")
|
println("프로그램 초기화 실행됨")
|
||||||
currentScreen = AppScreen.Settings
|
currentScreen = AppScreen.Settings
|
||||||
|
isWindowOpen = false
|
||||||
}
|
}
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
onAuthSuccess = {
|
onAuthSuccess = {
|
||||||
@ -179,3 +216,4 @@ fun main() = application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@ -90,6 +90,12 @@ object TradeLogTable : Table("trade_logs") {
|
|||||||
override val primaryKey = PrimaryKey(id)
|
override val primaryKey = PrimaryKey(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object HolidayTable : Table("holiday_cache") {
|
||||||
|
val bassDt = varchar("bass_dt", 8) // YYYYMMDD
|
||||||
|
val isHoliday = bool("is_holiday")
|
||||||
|
override val primaryKey = PrimaryKey(bassDt)
|
||||||
|
}
|
||||||
|
|
||||||
object DatabaseFactory {
|
object DatabaseFactory {
|
||||||
fun init() {
|
fun init() {
|
||||||
val dbPath = File("db/autotrade_db").absolutePath
|
val dbPath = File("db/autotrade_db").absolutePath
|
||||||
@ -101,10 +107,23 @@ object DatabaseFactory {
|
|||||||
transaction {
|
transaction {
|
||||||
|
|
||||||
// 테이블 생성 (AutoTradeTable 포함)
|
// 테이블 생성 (AutoTradeTable 포함)
|
||||||
SchemaUtils.createMissingTablesAndColumns(ConfigTable, TradeLogTable, AutoTradeTable)
|
SchemaUtils.createMissingTablesAndColumns(ConfigTable, TradeLogTable, AutoTradeTable,HolidayTable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveHoliday(date: String, holiday: Boolean) = transaction {
|
||||||
|
HolidayTable.replace {
|
||||||
|
it[bassDt] = date
|
||||||
|
it[isHoliday] = holiday
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 특정 날짜의 휴장 여부 조회
|
||||||
|
fun getHoliday(date: String): Boolean? = transaction {
|
||||||
|
HolidayTable.select { HolidayTable.bassDt eq date }
|
||||||
|
.map { it[HolidayTable.isHoliday] }
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 새로운 자동매매 건 등록 (주로 PENDING_BUY 상태로 시작)
|
* 새로운 자동매매 건 등록 (주로 PENDING_BUY 상태로 시작)
|
||||||
|
|||||||
@ -52,6 +52,29 @@ object KisTradeService {
|
|||||||
private val prodUrl = "https://openapi.koreainvestment.com:9443"
|
private val prodUrl = "https://openapi.koreainvestment.com:9443"
|
||||||
private val vtsUrl = "https://openapivts.koreainvestment.com:29443"
|
private val vtsUrl = "https://openapivts.koreainvestment.com:29443"
|
||||||
|
|
||||||
|
suspend fun fetchIsHoliday(date: String): Result<Boolean> {
|
||||||
|
val config = KisSession.config
|
||||||
|
return try {
|
||||||
|
val response = client.get("$prodUrl/uapi/domestic-stock/v1/quotations/chk-holiday") {
|
||||||
|
header("authorization", "Bearer ${config.marketToken}")
|
||||||
|
header("appkey", config.realAppKey)
|
||||||
|
header("appsecret", config.realSecretKey)
|
||||||
|
header("tr_id", "CTCA0903R")
|
||||||
|
header("custtype", "P")
|
||||||
|
|
||||||
|
parameter("BASS_DT", date)
|
||||||
|
parameter("CTX_AREA_NK", "")
|
||||||
|
parameter("CTX_AREA_FK", "")
|
||||||
|
}
|
||||||
|
val body = response.body<JsonObject>()
|
||||||
|
// output의 opnd_yn (영업일 여부)가 'Y'이면 영업일, 'N'이면 휴장일
|
||||||
|
val isOpeningDay = body["output"]?.jsonArray?.firstOrNull()?.jsonObject?.get("opnd_yn")?.jsonPrimitive?.content == "Y"
|
||||||
|
Result.success(!isOpeningDay)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [1] 통합 잔고 조회 (국내 + 해외 합산)
|
* [1] 통합 잔고 조회 (국내 + 해외 합산)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -4,6 +4,9 @@ import AutoTradeItem
|
|||||||
import network.TradingDecision
|
import network.TradingDecision
|
||||||
import TradingLogStore
|
import TradingLogStore
|
||||||
import TradingLogStore.decisionLogs
|
import TradingLogStore.decisionLogs
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import getLlamaBinPath
|
import getLlamaBinPath
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -61,6 +64,31 @@ object AutoTradingManager {
|
|||||||
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
|
// private val processedCodes = mutableSetOf<String>() // 중복 처리 방지용 (선택 사항)
|
||||||
private val reanalysisList = mutableListOf<RankingStock>()
|
private val reanalysisList = mutableListOf<RankingStock>()
|
||||||
private val retryCountMap = mutableMapOf<String, Int>()
|
private val retryCountMap = mutableMapOf<String, Int>()
|
||||||
|
var shouldShowFullWindow by mutableStateOf(false)
|
||||||
|
fun startBackgroundScheduler() {
|
||||||
|
// scope.launch {
|
||||||
|
// while (isActive) {
|
||||||
|
// val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||||
|
//
|
||||||
|
// // 1. 오전 8시 30분 ~ 15시 30분 사이인지 확인
|
||||||
|
// if (now.isAfter(LocalTime.of(8, 30)) && now.isBefore(LocalTime.of(15, 30))) {
|
||||||
|
// // 2. 아직 오늘 시스템 준비가 안 되었고, 설정값이 있는 경우
|
||||||
|
// if (!isSystemReadyToday && KisSession.config.realAppKey.isNotEmpty()) {
|
||||||
|
// println("⏰ [Scheduler] 자동 실행 시간이 되어 인증을 시작합니다.")
|
||||||
|
// SystemSleepPreventer.wakeDisplay() //
|
||||||
|
//
|
||||||
|
// // 인증 및 토큰 갱신 시도
|
||||||
|
// val success = KisAuthService.refreshAllTokens() && KisTradeService.refreshWebsocketKey()
|
||||||
|
// if (success) {
|
||||||
|
// isSystemReadyToday = true
|
||||||
|
// startAutoDiscoveryLoop() // 자동 매매 루프 시작
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// delay(60_000) // 1분마다 체크
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean ->
|
val globalCallback = { completeTradingDecision: TradingDecision?, isSuccess: Boolean ->
|
||||||
if (isSuccess && completeTradingDecision != null) {
|
if (isSuccess && completeTradingDecision != null) {
|
||||||
@ -389,6 +417,7 @@ object AutoTradingManager {
|
|||||||
}
|
}
|
||||||
KisWebSocketManager.connect()
|
KisWebSocketManager.connect()
|
||||||
isSystemReadyToday = true
|
isSystemReadyToday = true
|
||||||
|
shouldShowFullWindow = true
|
||||||
} else {
|
} else {
|
||||||
println("❌ [System] 토큰 갱신 실패. 2분 후 재시도합니다.")
|
println("❌ [System] 토큰 갱신 실패. 2분 후 재시도합니다.")
|
||||||
}
|
}
|
||||||
@ -425,6 +454,16 @@ object AutoTradingManager {
|
|||||||
// Main.kt에 설정 화면으로 가라고 신호 전송
|
// Main.kt에 설정 화면으로 가라고 신호 전송
|
||||||
stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐)
|
stopDiscovery() // 발굴 루프 완전 폭파 (내일 8시 30분에 다시 켜짐)
|
||||||
return@launch
|
return@launch
|
||||||
|
} else if (now.isAfter(H08M30) && now.isBefore(H08M50) && !isSystemReadyToday) {
|
||||||
|
if (MarketUtil.canTradeToday()) {
|
||||||
|
println("✅ [System] 오늘은 영업일입니다. 시스템을 가동합니다.")
|
||||||
|
tryRefreshToken() // 토큰 갱신 및 화면 표시 신호(shouldShowFullWindow = true)
|
||||||
|
} else {
|
||||||
|
println("💤 [System] 오늘은 휴장일(또는 주말)입니다. 대기 모드를 유지합니다.")
|
||||||
|
isSystemReadyToday = false
|
||||||
|
delay(3600_000) // 휴장일이면 1시간 뒤에 다시 체크하도록 긴 지연시간 부여
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
@ -23,15 +25,62 @@ import androidx.compose.ui.unit.sp
|
|||||||
import model.ConfigIndex
|
import model.ConfigIndex
|
||||||
import model.KisSession
|
import model.KisSession
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TradingDecisionLog() {
|
fun TradingDecisionLog() {
|
||||||
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
var selectedFilter by remember { mutableStateOf("전체") }
|
||||||
|
val filterOptions = listOf("전체", "BUY", "SELL", "HOLD", "SETTING")
|
||||||
|
|
||||||
|
// [핵심] 원본 로그에서 필터 조건에 맞는 리스트만 산출
|
||||||
|
val filteredLogs = TradingLogStore.decisionLogs.filter { log ->
|
||||||
|
val matchesType = if (selectedFilter == "전체") true else log.decision == selectedFilter
|
||||||
|
val matchesQuery = log.stockName.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
log.reason.contains(searchQuery, ignoreCase = true)
|
||||||
|
matchesType && matchesQuery
|
||||||
|
}
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
|
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
|
||||||
Column(modifier = Modifier.weight(0.5f).padding(8.dp).fillMaxHeight().background(Color.White)) {
|
Column(modifier = Modifier.weight(0.5f).padding(8.dp).fillMaxHeight().background(Color.White)) {
|
||||||
Text("AI 자동매매 실시간 로그", style = MaterialTheme.typography.h6)
|
Text("AI 자동매매 실시간 로그", style = MaterialTheme.typography.h6)
|
||||||
Divider(Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
LazyColumn(reverseLayout = true) { // 최신 로그가 위로 오게 함
|
// [추가] 상단 검색 및 필터 UI
|
||||||
items(TradingLogStore.decisionLogs) { log ->
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
// 1. 검색창
|
||||||
|
OutlinedTextField(
|
||||||
|
value = searchQuery,
|
||||||
|
onValueChange = { searchQuery = it },
|
||||||
|
label = { Text("종목명 또는 내용 검색") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 2. 필터 버튼 그룹 (Chip 형태)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
filterOptions.forEach { option ->
|
||||||
|
val isSelected = selectedFilter == option
|
||||||
|
FilterChip(
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = { selectedFilter = option },
|
||||||
|
colors = ChipDefaults.filterChipColors(
|
||||||
|
selectedBackgroundColor = Color(0xFF0E62CF),
|
||||||
|
selectedContentColor = Color.White
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(option, fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider(Modifier.padding(bottom = 8.dp))
|
||||||
|
|
||||||
|
// [수정] filteredLogs를 사용하여 최신 로그가 위로 오게 표시
|
||||||
|
LazyColumn(reverseLayout = true) {
|
||||||
|
items(filteredLogs) { log ->
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||||
elevation = 2.dp
|
elevation = 2.dp
|
||||||
@ -43,10 +92,10 @@ fun TradingDecisionLog() {
|
|||||||
text = log.decision,
|
text = log.decision,
|
||||||
color = when (log.decision) {
|
color = when (log.decision) {
|
||||||
"BUY" -> Color.Red
|
"BUY" -> Color.Red
|
||||||
"SETTING" -> Color(0xFFFFA500) // 주황색
|
"SETTING" -> Color(0xFFFFA500)
|
||||||
"SELL" -> Color(0xFF800080) // 보라색
|
"SELL" -> Color(0xFF800080)
|
||||||
"HOLD" -> Color.Gray // HOLD는 그레이
|
"HOLD" -> Color.Gray
|
||||||
else -> Color.Gray // 그 외 기본값 그레이
|
else -> Color.Gray
|
||||||
},
|
},
|
||||||
fontWeight = FontWeight.ExtraBold
|
fontWeight = FontWeight.ExtraBold
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,9 +1,42 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
|
import network.KisTradeService
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
|
||||||
object MarketUtil {
|
object MarketUtil {
|
||||||
|
private var isHolidayCached: Boolean? = null // 하루 한 번만 체크하기 위한 캐시
|
||||||
|
|
||||||
|
suspend fun canTradeToday(): Boolean {
|
||||||
|
val seoulZone = java.time.ZoneId.of("Asia/Seoul")
|
||||||
|
val now = java.time.ZonedDateTime.now(seoulZone)
|
||||||
|
val todayStr = now.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
|
||||||
|
|
||||||
|
// 1. 주말 체크
|
||||||
|
val dayOfWeek = now.dayOfWeek.value
|
||||||
|
if (dayOfWeek >= 6) return false
|
||||||
|
// 1. 주말 체크 (토, 일)
|
||||||
|
val cachedHoliday = DatabaseFactory.getHoliday(todayStr)
|
||||||
|
if (cachedHoliday != null) {
|
||||||
|
println("📂 [DB Cache] 오늘($todayStr)의 휴장 여부를 DB에서 로드했습니다: ${if(cachedHoliday) "휴장" else "영업일"}")
|
||||||
|
return !cachedHoliday
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. DB에 없으면 API 호출
|
||||||
|
return try {
|
||||||
|
val result = KisTradeService.fetchIsHoliday(todayStr)
|
||||||
|
val isHoliday = result.getOrDefault(true)
|
||||||
|
|
||||||
|
// 결과를 DB에 저장하여 다음 실행 시 재사용
|
||||||
|
DatabaseFactory.saveHoliday(todayStr, isHoliday)
|
||||||
|
|
||||||
|
println("🌐 [API Call] 오늘($todayStr)의 휴장 여부를 새로 조회하여 DB에 저장했습니다.")
|
||||||
|
!isHoliday
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun isKoreanMarketOpen(): Boolean {
|
fun isKoreanMarketOpen(): Boolean {
|
||||||
// 한국 시간대 기준 현재 시간 가져오기
|
// 한국 시간대 기준 현재 시간 가져오기
|
||||||
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
val now = LocalTime.now(ZoneId.of("Asia/Seoul"))
|
||||||
|
|||||||
BIN
src/main/resources/neko.png
Normal file
BIN
src/main/resources/neko.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Loading…
x
Reference in New Issue
Block a user