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

213 lines
11 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
// 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(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 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.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 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(10.dp))
Button(
modifier = Modifier.fillMaxWidth().height(50.dp),
onClick = {
scope.launch {
var retryCount = 0
val maxRetries = 3
val totalDelaySeconds = 90 // 1분 30초 = 90초
var isAuthCompleted = false
while (retryCount <= maxRetries && !isAuthCompleted) {
// 재시도 시 대기 및 카운트다운 표시
if (retryCount > 0) {
for (secondsLeft in totalDelaySeconds downTo 1) {
statusMessage = "⚠️ 인증 실패. ${secondsLeft}초 후 자동으로 다시 시도합니다. (시도 ${retryCount}/${maxRetries})"
delay(1000L) // 1초 대기
}
}
statusMessage = if (retryCount == 0) "⏳ 인증 시도 중..." else "${retryCount}차 재시도 중..."
// 1. 설정값 저장
KisSession.config = config
DatabaseFactory.saveConfig(config)
// 2. 법인코드 업데이트
DartCodeManager.updateCorpCodes(HttpClient(CIO) {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
})
// 3. 토큰 및 웹소켓 키 갱신
val authSuccess = KisAuthService.refreshAllTokens()
val wsKeySuccess = KisTradeService.refreshWebsocketKey()
if (authSuccess && wsKeySuccess) {
statusMessage = "✅ 인증 성공! LLM 시작 중..."
isAuthCompleted = true
onAuthSuccess()
} else {
retryCount++
if (retryCount > maxRetries) {
statusMessage = "❌ 인증 실패. 3회 재시도 후 중단되었습니다. 키 정보를 확인하세요."
}
// 다음 루프에서 카운트다운 진입
}
}
}
}
) { Text("설정 저장 및 실행") }
Text(statusMessage, color = Color.Gray, modifier = Modifier.padding(top = 8.dp))
}
}
}