import ConfigTable.grade_1_buy import ConfigTable.grade_1_profit import ConfigTable.grade_2_buy import ConfigTable.grade_2_profit import ConfigTable.grade_3_buy import ConfigTable.grade_3_profit import ConfigTable.grade_4_buy import ConfigTable.grade_4_profit import ConfigTable.grade_5_buy import ConfigTable.grade_5_profit import ConfigTable.max_count import Defines.DETAILLOG import Defines.EMBEDDING_PORT import Defines.LLM_PORT import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment 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.WindowPlacement import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberTrayState import androidx.compose.ui.window.rememberWindowState import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.DEFAULT import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.transaction import model.AppConfig import model.KisSession import network.DartCodeManager import network.KisTradeService import network.KisWebSocketManager import service.LlamaServerManager import network.NewsService import org.jetbrains.exposed.sql.selectAll import service.AutoTradingManager import service.AutoTradingManager.isSystemCleanedUpToday import service.SystemSleepPreventer import service.TradingDecisionCallback import ui.DashboardScreen import ui.SettingsScreen import ui.TradingDecisionLog import util.PortFinder // 화면 상태 정의 enum class AppScreen { Settings, Dashboard, TradingDecision } fun getLlamaBinPath(): String { val os = System.getProperty("os.name").lowercase() val arch = System.getProperty("os.arch").lowercase() val basePath = "./src/main/resources/bin" return when { // Apple Silicon (M1/M2/M3) os.contains("mac") && (arch.contains("aarch64") || arch.contains("arm64")) -> { "$basePath/mac-arm64/llama-server" } // Intel Mac (2017) os.contains("mac") -> { "$basePath/mac-x64/llama-server" } // Windows NUC os.contains("win") -> { "$basePath/win-x64/llama-server.exe" } else -> "$basePath/llama-server" } } fun initLogger(isDebug: Boolean) { val root = org.slf4j.LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger if (isDebug) { root.level = ch.qos.logback.classic.Level.DEBUG println("🛠️ 디버그 모드: 상세 로그를 출력합니다.") } else { root.level = ch.qos.logback.classic.Level.ERROR // 특정 라이브러리(Exposed)만 콕 집어서 끄기 (org.slf4j.LoggerFactory.getLogger("Exposed") as ch.qos.logback.classic.Logger).level = ch.qos.logback.classic.Level.OFF println("🤫 운영 모드: 에러 로그만 출력합니다.") } } private var isAppStarted = false fun main() = application { if (!isAppStarted) { initLogger(DETAILLOG) try { val (port1, port2) = PortFinder.findAvailablePortPair(18080, false) if (port1 > 18000 && port2 > port1) { println("🚀 AI 서버용 포트 할당 완료: 메인($port1), 서브($port2)") LLM_PORT = port1 EMBEDDING_PORT = port2 isAppStarted = true } else { println("🚀 AI 서버용 포트 할당 실패") exitApplication() } } catch (e: Exception) { println("🚀 AI 서버용 포트 할당 에러 : ${e.message}") e.printStackTrace() exitApplication() } } val trayState = rememberTrayState() var isWindowOpen by remember { mutableStateOf(false) } // 창의 표시 상태 관리 LaunchedEffect(Unit) { 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 windowState = rememberWindowState( placement = WindowPlacement.Floating ) if (isWindowOpen) { Window(onCloseRequest = { isWindowOpen = false }, title = "KIS AI 자동매매", state = windowState) { var currentScreen by remember { mutableStateOf(AppScreen.Settings) } var isLoaded by remember { mutableStateOf(false) } // 1. 앱 시작 시 DB에서 마지막 설정 로드 (KisSession에 주입) LaunchedEffect(Unit) { DatabaseFactory.init() transaction { ConfigTable.selectAll().lastOrNull()?.let { KisSession.config = AppConfig( realAppKey = it[ConfigTable.realAppKey], realSecretKey = it[ConfigTable.realSecretKey], realAccountNo = it[ConfigTable.realAccountNo], vtsAppKey = it[ConfigTable.vtsAppKey], vtsSecretKey = it[ConfigTable.vtsSecretKey], vtsAccountNo = it[ConfigTable.vtsAccountNo], nAppKey = it[ConfigTable.nAppKey], nSecretKey = it[ConfigTable.nSecretKey], dAppKey = it[ConfigTable.dAppKey], isSimulation = it[ConfigTable.isSimulation], htsId = it[ConfigTable.htsId], modelPath = it[ConfigTable.modelPath], embedModelPath = it[ConfigTable.embedModelPath], FEES_AND_TAXRATE = it[ConfigTable.fees_and_taxrate], MINIMUM_NET_PROFIT = it[ConfigTable.minimum_net_profit], BUY_WEIGHT = it[ConfigTable.buy_weight], MAX_BUDGET = it[ConfigTable.max_budget], MAX_PRICE = it[ConfigTable.max_price], MIN_PRICE = it[ConfigTable.min_price], MIN_PURCHASE_SCORE = it[ConfigTable.min_purchase_score], SELL_PROFIT = it[ConfigTable.sell_profit], GRADE_5_BUY = it[grade_5_buy], GRADE_5_PROFIT = it[grade_5_profit], GRADE_4_BUY = it[grade_4_buy], GRADE_4_PROFIT = it[grade_4_profit], GRADE_3_BUY = it[grade_3_buy], GRADE_3_PROFIT = it[grade_3_profit], GRADE_2_BUY = it[grade_2_buy], GRADE_2_PROFIT = it[grade_2_profit], GRADE_1_BUY = it[grade_1_buy], GRADE_1_PROFIT = it[grade_1_profit], MAX_COUNT = it[max_count], ) } } isLoaded = true } if (!isLoaded) { // 로딩 중 표시 CircularProgressIndicator() } else { when (currentScreen) { AppScreen.Settings -> { AutoTradingManager.onMarketClosed = { println("프로그램 초기화 실행됨") currentScreen = AppScreen.Settings isWindowOpen = false } SettingsScreen( onAuthSuccess = { // 2. 설정 및 인증 완료 시점의 처리 val config = KisSession.config AutoTradingManager.isSystemReadyToday = true AutoTradingManager.isSystemCleanedUpToday = false CoroutineScope(Dispatchers.Default).launch { AutoTradingManager.startAutoDiscoveryLoop() KisWebSocketManager.onExecutionReceived = AutoTradingManager.onExecutionReceived KisWebSocketManager.connect() } if (config.modelPath.isNotEmpty()) { LlamaServerManager.startServer(binPath, config.modelPath,port = LLM_PORT) } if (config.embedModelPath.isNotEmpty()) { LlamaServerManager.startServer(binPath, config.embedModelPath, port = EMBEDDING_PORT) } // 대시보드로 화면 전환 currentScreen = AppScreen.TradingDecision } ) } AppScreen.Dashboard -> { DashboardScreen() } AppScreen.TradingDecision -> { TradingDecisionLog() } } } } } }