atrade/src/main/kotlin/ui/SettingsScreen.kt
2026-03-19 17:00:53 +09:00

257 lines
12 KiB
Kotlin

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.delay
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
import service.SystemSleepPreventer
// 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("프로그램 초기화") }
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분마다 시간 체크
}
}
// 계좌번호 입력 시 데이터 자동 로드 함수
fun checkAndLoadConfig(accountNo: String, isReal: Boolean) {
val loaded = DatabaseFactory.findConfigByAccount(accountNo)
if (loaded != null) {
config = loaded
statusMessage = "✅ 기존 데이터를 불러왔습니다."
}
}
LazyColumn(modifier = Modifier.fillMaxSize().padding(20.dp)) {
item {
Row(
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
){
Text("투자 방식", style = MaterialTheme.typography.subtitle1)
Spacer(Modifier.width(10.dp))
RadioButton(selected = !config.isSimulation, onClick = { config = config.copy(isSimulation = false,) })
Text("실전")
Spacer(Modifier.width(10.dp))
RadioButton(selected = config.isSimulation, onClick = { config = config.copy() })
Text("모의")
}
Divider(Modifier.padding(vertical = 10.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)
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)
)
}
OutlinedTextField(value = config.realSecretKey, onValueChange = { config = config.copy(realSecretKey = it,) }, label = { Text("실전 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
Spacer(Modifier.height(10.dp))
// 모의 3종 입력
Text("모의투자 정보", fontWeight = FontWeight.Bold)
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)
)
}
OutlinedTextField(value = config.vtsSecretKey, onValueChange = { config = config.copy(vtsSecretKey = it,) }, label = { Text("모의 Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
Spacer(Modifier.height(10.dp))
Text("정보 조회 Api Keyz", fontWeight = FontWeight.Bold)
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())
Spacer(Modifier.height(10.dp))
Text("AI 모델 설정", fontWeight = FontWeight.Bold)
Row(
modifier = Modifier.fillMaxWidth(), // 필요에 따라 너비 설정
verticalAlignment = Alignment.CenterVertically // 상하 중앙 정렬 추가
) {
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) {
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)
}
}
}),
contentAlignment = Alignment.Center
) {
Text(
if (config.modelPath.isEmpty()) "GGUF 모델 파일을 여기로 드래그하세요" else config.modelPath,
fontSize = 12.sp
)
}
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) {
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)
}
}
}),
contentAlignment = Alignment.Center
) {
Text(
if (config.embedModelPath.isEmpty()) "임베드용 GGUF 모델 파일을 여기로 드래그하세요" else config.embedModelPath,
fontSize = 12.sp
)
}
}
Spacer(Modifier.height(10.dp))
Button(
modifier = Modifier.fillMaxWidth().height(50.dp),
onClick = {
scope.launch {
authenticateAndStart()
}
}
) { Text("설정 저장 및 실행") }
Text(statusMessage, color = Color.Gray, modifier = Modifier.padding(top = 8.dp))
}
}
}