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 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.window.Window import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.application 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 service.LlamaServerManager import network.NewsService import org.jetbrains.exposed.sql.selectAll import service.AutoTradingManager import service.SystemSleepPreventer import service.TradingDecisionCallback import ui.DashboardScreen import ui.SettingsScreen // 화면 상태 정의 enum class AppScreen { Settings, Dashboard } 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 main() = application { SystemSleepPreventer.start() LaunchedEffect(Unit) { // NewsService나 KisTradeService에서 사용하는 client를 전달 DartCodeManager.updateCorpCodes(HttpClient(CIO) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true encodeDefaults = true // 기본값이 포함된 요청 바디를 정확히 전송하기 위해 필요 }) } // [수정] 모든 로그(Headers + Body)를 찍도록 설정 install(Logging) { logger = Logger.DEFAULT level = LogLevel.BODY } }) } // 앱 실행 시 필요한 바이너리 경로 (실행 파일 위치) val binPath = getLlamaBinPath() val windowState = rememberWindowState( placement = WindowPlacement.Maximized ) Window(onCloseRequest = ::exitApplication, title = "KIS AI 자동매매", state = windowState) { var currentScreen by remember { mutableStateOf(AppScreen.Settings) } var isLoaded by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() // 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], 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], 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 -> { SettingsScreen( onAuthSuccess = { // 2. 설정 및 인증 완료 시점의 처리 val config = KisSession.config // LLM 서버 시작 (설정된 모델 경로 사용) if (config.modelPath.isNotEmpty()) { LlamaServerManager.startServer(binPath, config.modelPath,port = 8080) } if (config.embedModelPath.isNotEmpty()) { LlamaServerManager.startServer(binPath, config.embedModelPath, port = 8081) } // 대시보드로 화면 전환 currentScreen = AppScreen.Dashboard } ) } AppScreen.Dashboard -> { DashboardScreen() } } } } }