atrade/src/main/kotlin/ui/SettingsScreen.kt

257 lines
12 KiB
Kotlin
Raw Normal View History

2026-01-10 18:16:50 +09:00
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
2026-01-13 16:04:25 +09:00
import androidx.compose.ui.text.font.FontWeight
2026-01-10 18:16:50 +09:00
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
2026-03-13 11:06:20 +09:00
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
2026-03-13 16:37:53 +09:00
import kotlinx.coroutines.delay
2026-01-10 18:16:50 +09:00
import kotlinx.coroutines.launch
2026-03-13 11:06:20 +09:00
import kotlinx.serialization.json.Json
2026-01-10 18:16:50 +09:00
import model.AppConfig
2026-01-13 16:04:25 +09:00
import model.KisSession
2026-03-13 11:06:20 +09:00
import network.DartCodeManager
2026-01-10 18:16:50 +09:00
import network.KisAuthService
2026-01-13 16:04:25 +09:00
import network.KisTradeService
2026-01-10 18:16:50 +09:00
import org.jetbrains.exposed.sql.deleteAll
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.transactions.transaction
2026-03-19 11:41:21 +09:00
import service.SystemSleepPreventer
2026-01-10 18:16:50 +09:00
2026-01-13 16:04:25 +09:00
// src/main/kotlin/ui/SettingsScreen.kt
2026-01-10 18:16:50 +09:00
@OptIn(ExperimentalComposeUiApi::class)
@Composable
2026-01-13 16:04:25 +09:00
fun SettingsScreen(onAuthSuccess: () -> Unit) {
2026-01-10 18:16:50 +09:00
val scope = rememberCoroutineScope()
2026-01-13 16:04:25 +09:00
var config by remember { mutableStateOf(KisSession.config) }
2026-03-19 17:00:53 +09:00
var statusMessage by remember { mutableStateOf("프로그램 초기화") }
2026-01-10 18:16:50 +09:00
2026-03-19 11:41:21 +09:00
val authenticateAndStart: suspend () -> Unit = {
var retryCount = 0
val maxRetries = 3
val totalDelaySeconds = 90
var isAuthCompleted = false
while (retryCount <= maxRetries && !isAuthCompleted) {
if (retryCount > 0) {
for (secondsLeft in totalDelaySeconds downTo 1) {
statusMessage = "⚠️ 인증 실패. ${secondsLeft}초 후 자동으로 다시 시도합니다. (시도 ${retryCount}/${maxRetries})"
delay(1000L)
}
}
statusMessage = if (retryCount == 0) "⏳ 인증 시도 중..." else "${retryCount}차 재시도 중..."
KisSession.config = config
DatabaseFactory.saveConfig(config)
DartCodeManager.updateCorpCodes(HttpClient(CIO) {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
})
val authSuccess = KisAuthService.refreshAllTokens()
val wsKeySuccess = KisTradeService.refreshWebsocketKey()
if (authSuccess && wsKeySuccess) {
statusMessage = "✅ 인증 성공! LLM 시작 중..."
isAuthCompleted = true
onAuthSuccess() // 여기서 대시보드로 넘어감
} else {
retryCount++
if (retryCount > maxRetries) {
statusMessage = "❌ 인증 실패. 3회 재시도 후 중단되었습니다."
}
}
}
}
LaunchedEffect(config) {
while (true) {
val now = java.time.LocalTime.now(java.time.ZoneId.of("Asia/Seoul"))
// 08:30 ~ 15:30 사이이고, 키값이 최소한 하나라도 존재할 때 자동 실행
if (now.isAfter(java.time.LocalTime.of(8, 30)) && now.isBefore(java.time.LocalTime.of(15, 30))) {
if (config.realAppKey.isNotEmpty() || config.vtsAppKey.isNotEmpty()) {
SystemSleepPreventer.wakeDisplay() // 모니터 켜기
statusMessage = "⏰ 자동 실행 시간(08:30)입니다. 시스템을 가동합니다."
authenticateAndStart()
break // 성공하면 루프 탈출
}
}
delay(60_000 * 2) // 1분마다 시간 체크
}
}
2026-01-13 16:04:25 +09:00
// 계좌번호 입력 시 데이터 자동 로드 함수
fun checkAndLoadConfig(accountNo: String, isReal: Boolean) {
val loaded = DatabaseFactory.findConfigByAccount(accountNo)
if (loaded != null) {
config = loaded
statusMessage = "✅ 기존 데이터를 불러왔습니다."
}
}
2026-01-10 18:16:50 +09:00
2026-03-13 16:37:53 +09:00
LazyColumn(modifier = Modifier.fillMaxSize().padding(20.dp)) {
2026-01-10 18:16:50 +09:00
item {
2026-03-13 16:37:53 +09:00
Row(
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
){
Text("투자 방식", style = MaterialTheme.typography.subtitle1)
Spacer(Modifier.width(10.dp))
2026-02-19 15:47:31 +09:00
RadioButton(selected = !config.isSimulation, onClick = { config = config.copy(isSimulation = false,) })
2026-03-13 16:37:53 +09:00
Text("실전")
Spacer(Modifier.width(10.dp))
2026-02-19 15:47:31 +09:00
RadioButton(selected = config.isSimulation, onClick = { config = config.copy() })
2026-03-13 16:37:53 +09:00
Text("모의")
2026-01-10 18:16:50 +09:00
}
2026-03-13 16:37:53 +09:00
Divider(Modifier.padding(vertical = 10.dp))
2026-01-14 15:42:26 +09:00
OutlinedTextField(
value = config.htsId,
2026-02-19 15:47:31 +09:00
onValueChange = { config = config.copy(htsId = it,) },
2026-01-14 15:42:26 +09:00
label = { Text("HTS ID (실시간 체결 통보용)") },
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
placeholder = { Text("한국투자증권 HTS 접속 ID를 입력하세요") }
)
2026-01-13 16:04:25 +09:00
// 실전 3종 입력
Text("실전투자 정보 (시세 조회 필수)", fontWeight = FontWeight.Bold)
2026-03-13 16:37:53 +09:00
Row(
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
) {
OutlinedTextField(
value = config.realAccountNo, onValueChange = {
config = config.copy(realAccountNo = it,)
if (it.length >= 8) checkAndLoadConfig(it, true)
}, label = { Text("실전 계좌번호") }, modifier = Modifier.weight(0.5f))
OutlinedTextField(
value = config.realAppKey,
onValueChange = { config = config.copy(realAppKey = it,) },
label = { Text("실전 App Key") },
modifier = Modifier.weight(0.5f)
)
}
2026-02-19 15:47:31 +09:00
OutlinedTextField(value = config.realSecretKey, onValueChange = { config = config.copy(realSecretKey = it,) }, label = { Text("실전 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
2026-03-13 16:37:53 +09:00
Spacer(Modifier.height(10.dp))
2026-01-10 18:16:50 +09:00
2026-01-13 16:04:25 +09:00
// 모의 3종 입력
Text("모의투자 정보", fontWeight = FontWeight.Bold)
2026-03-13 16:37:53 +09:00
Row(
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
) {
OutlinedTextField(value = config.vtsAccountNo, onValueChange = {
config = config.copy(vtsAccountNo = it,)
if (it.length >= 8) checkAndLoadConfig(it, false)
}, label = { Text("모의 계좌번호") }, modifier = Modifier.weight(0.5f))
OutlinedTextField(
value = config.vtsAppKey,
onValueChange = { config = config.copy(vtsAppKey = it,) },
label = { Text("모의 App Key") },
modifier = Modifier.weight(0.5f)
)
}
2026-02-19 15:47:31 +09:00
OutlinedTextField(value = config.vtsSecretKey, onValueChange = { config = config.copy(vtsSecretKey = it,) }, label = { Text("모의 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
2026-01-13 16:04:25 +09:00
2026-03-13 16:37:53 +09:00
Spacer(Modifier.height(10.dp))
Text("정보 조회 Api Keyz", fontWeight = FontWeight.Bold)
2026-03-13 11:06:20 +09:00
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())
2026-03-13 16:37:53 +09:00
Spacer(Modifier.height(10.dp))
2026-01-13 16:04:25 +09:00
Text("AI 모델 설정", fontWeight = FontWeight.Bold)
2026-03-13 16:37:53 +09:00
Row(
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
2026-03-17 10:50:13 +09:00
) {
2026-03-13 16:37:53 +09:00
Box(
modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
.onExternalDrag(onDrop = { state ->
val data = state.dragData
if (data is DragData.FilesList) {
2026-03-17 10:50:13 +09:00
val rawUri = data.readFiles().firstOrNull()
if (rawUri != null) {
// 1. file:// 또는 file: 접두사 제거
var path = rawUri.removePrefix("file://").removePrefix("file:")
// 2. 윈도우 환경의 드라이브 문자(예: /C:/) 앞의 슬래시 제거
if (path.startsWith("/") && path.getOrNull(2) == ':') {
path = path.drop(1)
}
if (path.endsWith(".gguf")) config = config.copy(modelPath = path)
}
2026-03-13 16:37:53 +09:00
}
}),
contentAlignment = Alignment.Center
) {
2026-03-17 10:50:13 +09:00
Text(
if (config.modelPath.isEmpty()) "GGUF 모델 파일을 여기로 드래그하세요" else config.modelPath,
fontSize = 12.sp
)
2026-03-13 16:37:53 +09:00
}
2026-03-17 10:50:13 +09:00
2026-03-13 16:37:53 +09:00
Box(
modifier = Modifier.weight(0.5f).height(60.dp).border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
.onExternalDrag(onDrop = { state ->
val data = state.dragData
if (data is DragData.FilesList) {
2026-03-17 10:50:13 +09:00
val rawUri = data.readFiles().firstOrNull()
if (rawUri != null) {
// 1. file:// 또는 file: 접두사 제거
var embedModelPath = rawUri.removePrefix("file://").removePrefix("file:")
// 2. 윈도우 환경의 드라이브 문자(예: /C:/) 앞의 슬래시 제거
if (embedModelPath.startsWith("/") && embedModelPath.getOrNull(2) == ':') {
embedModelPath = embedModelPath.drop(1)
}
if (embedModelPath.endsWith(".gguf")) config =
config.copy(embedModelPath = embedModelPath)
}
2026-03-13 16:37:53 +09:00
}
}),
contentAlignment = Alignment.Center
) {
2026-03-17 10:50:13 +09:00
Text(
if (config.embedModelPath.isEmpty()) "임베드용 GGUF 모델 파일을 여기로 드래그하세요" else config.embedModelPath,
fontSize = 12.sp
)
2026-03-13 16:37:53 +09:00
}
2026-01-21 18:30:03 +09:00
}
2026-03-13 16:37:53 +09:00
Spacer(Modifier.height(10.dp))
2026-01-10 18:16:50 +09:00
Button(
modifier = Modifier.fillMaxWidth().height(50.dp),
onClick = {
scope.launch {
2026-03-19 11:41:21 +09:00
authenticateAndStart()
2026-01-10 18:16:50 +09:00
}
}
2026-01-13 16:04:25 +09:00
) { Text("설정 저장 및 실행") }
Text(statusMessage, color = Color.Gray, modifier = Modifier.padding(top = 8.dp))
2026-01-10 18:16:50 +09:00
}
}
2026-01-13 16:04:25 +09:00
}