package ui import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.DragData import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.onExternalDrag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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.launch import kotlinx.serialization.json.Json import model.AppConfig import model.KisSession import network.DartCodeManager import network.KisAuthService import network.KisTradeService import org.jetbrains.exposed.sql.deleteAll import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.transactions.transaction // src/main/kotlin/ui/SettingsScreen.kt @OptIn(ExperimentalComposeUiApi::class) @Composable fun SettingsScreen(onAuthSuccess: () -> Unit) { val scope = rememberCoroutineScope() var config by remember { mutableStateOf(KisSession.config) } var statusMessage by remember { mutableStateOf("정보를 입력하세요.") } // 계좌번호 입력 시 데이터 자동 로드 함수 fun checkAndLoadConfig(accountNo: String, isReal: Boolean) { val loaded = DatabaseFactory.findConfigByAccount(accountNo) if (loaded != null) { config = loaded statusMessage = "✅ 기존 데이터를 불러왔습니다." } } LazyColumn(modifier = Modifier.fillMaxSize().padding(24.dp)) { item { Text("거래 방식 선택", style = MaterialTheme.typography.h6) Row(verticalAlignment = Alignment.CenterVertically) { RadioButton(selected = !config.isSimulation, onClick = { config = config.copy(isSimulation = false,) }) Text("실전투자") Spacer(Modifier.width(16.dp)) RadioButton(selected = config.isSimulation, onClick = { config = config.copy() }) Text("모의투자") } Divider(Modifier.padding(vertical = 12.dp)) OutlinedTextField( value = config.htsId, onValueChange = { config = config.copy(htsId = it,) }, label = { Text("HTS ID (실시간 체결 통보용)") }, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), placeholder = { Text("한국투자증권 HTS 접속 ID를 입력하세요") } ) // 실전 3종 입력 Text("실전투자 정보 (시세 조회 필수)", fontWeight = FontWeight.Bold) OutlinedTextField(value = config.realAccountNo, onValueChange = { config = config.copy(realAccountNo = it,) if(it.length >= 8) checkAndLoadConfig(it, true) }, label = { Text("실전 계좌번호") }, modifier = Modifier.fillMaxWidth()) OutlinedTextField(value = config.realAppKey, onValueChange = { config = config.copy(realAppKey = it,) }, label = { Text("실전 App Key") }, modifier = Modifier.fillMaxWidth()) OutlinedTextField(value = config.realSecretKey, onValueChange = { config = config.copy(realSecretKey = it,) }, label = { Text("실전 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation()) Spacer(Modifier.height(16.dp)) // 모의 3종 입력 Text("모의투자 정보", fontWeight = FontWeight.Bold) OutlinedTextField(value = config.vtsAccountNo, onValueChange = { config = config.copy(vtsAccountNo = it,) if(it.length >= 8) checkAndLoadConfig(it, false) }, label = { Text("모의 계좌번호") }, modifier = Modifier.fillMaxWidth()) OutlinedTextField(value = config.vtsAppKey, onValueChange = { config = config.copy(vtsAppKey = it,) }, label = { Text("모의 App Key") }, modifier = Modifier.fillMaxWidth()) OutlinedTextField(value = config.vtsSecretKey, onValueChange = { config = config.copy(vtsSecretKey = it,) }, label = { Text("모의 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation()) Divider(Modifier.padding(vertical = 16.dp)) OutlinedTextField(value = config.nAppKey, onValueChange = { config = config.copy(nAppKey = it,) }, label = { Text("NAVER Client ID") }, modifier = Modifier.fillMaxWidth()) OutlinedTextField(value = config.nSecretKey, onValueChange = { config = config.copy(nSecretKey = it,) }, label = { Text("NAVER Client Secret") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation()) OutlinedTextField(value = config.dAppKey, onValueChange = { config = config.copy(dAppKey = it,) }, label = { Text("Dart ApiKey") }, modifier = Modifier.fillMaxWidth()) // AI 모델 경로 및 드래그 앤 드롭 Text("AI 모델 설정", fontWeight = FontWeight.Bold) Box( modifier = Modifier.fillMaxWidth().height(100.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp)) .onExternalDrag(onDrop = { state -> val data = state.dragData if (data is DragData.FilesList) { val path = data.readFiles().firstOrNull()?.removePrefix("file:") if (path?.endsWith(".gguf") == true) config = config.copy(modelPath = path,) } }), contentAlignment = Alignment.Center ) { Text(if(config.modelPath.isEmpty()) "GGUF 모델 파일을 여기로 드래그하세요" else config.modelPath, fontSize = 12.sp) } Box( modifier = Modifier.fillMaxWidth().height(100.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp)) .onExternalDrag(onDrop = { state -> val data = state.dragData if (data is DragData.FilesList) { val embedModelPath = data.readFiles().firstOrNull()?.removePrefix("file:") if (embedModelPath?.endsWith(".gguf") == true) config = config.copy(embedModelPath = embedModelPath,) } }), contentAlignment = Alignment.Center ) { Text(if(config.embedModelPath.isEmpty()) "임베드용 GGUF 모델 파일을 여기로 드래그하세요" else config.embedModelPath, fontSize = 12.sp) } Spacer(Modifier.height(24.dp)) Button( modifier = Modifier.fillMaxWidth().height(50.dp), onClick = { scope.launch { // isLoading = true // 1. KisSession.config 업데이트 및 DB 저장 KisSession.config = config DatabaseFactory.saveConfig(config) DartCodeManager.updateCorpCodes(HttpClient(CIO) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true encodeDefaults = true // 기본값이 포함된 요청 바디를 정확히 전송하기 위해 필요 }) } // [수정] 모든 로그(Headers + Body)를 찍도록 설정 install(Logging) { logger = Logger.DEFAULT level = LogLevel.BODY } }) val authService = KisAuthService val tradeService = KisTradeService val authSuccess = authService.refreshAllTokens() val wsKeySuccess = tradeService.refreshWebsocketKey() if (authSuccess && wsKeySuccess) { statusMessage = "✅ 인증 성공! LLM 시작 중..." onAuthSuccess() } else { statusMessage = "❌ 인증 실패. 키 정보를 확인하세요." } // isLoading = false } } ) { Text("설정 저장 및 실행") } Text(statusMessage, color = Color.Gray, modifier = Modifier.padding(top = 8.dp)) } } }