This commit is contained in:
2026-01-13 16:04:25 +09:00
parent d4770af62f
commit 2bb94e2856
23 changed files with 1406 additions and 1103 deletions
+65 -105
View File
@@ -5,8 +5,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.DragData
@@ -14,151 +12,113 @@ 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 kotlinx.coroutines.launch
import model.AppConfig
import model.KisSession
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 javax.swing.JFileChooser
import javax.swing.filechooser.FileNameExtensionFilter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog // 파일 선택기용
// src/main/kotlin/ui/SettingsScreen.kt
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SettingsScreen(
initialConfig: AppConfig, // 모델 경로가 포함된 확장된 AppConfig 필요
onAuthSuccess: (AppConfig, String) -> Unit
) {
fun SettingsScreen(onAuthSuccess: () -> Unit) {
val scope = rememberCoroutineScope()
val authService = remember { KisAuthService() }
var config by remember { mutableStateOf(KisSession.config) }
var statusMessage by remember { mutableStateOf("정보를 입력하세요.") }
// 화면 입력 상태값
var appKey by remember { mutableStateOf(initialConfig.appKey) }
var secretKey by remember { mutableStateOf(initialConfig.secretKey) }
var accountNo by remember { mutableStateOf(initialConfig.accountNo) }
var isSimulation by remember { mutableStateOf(initialConfig.isSimulation) }
var modelPath by remember { mutableStateOf(initialConfig.modelPath ?: "") } // AI 모델 경로
var statusMessage by remember { mutableStateOf("설정 정보를 입력하세요.") }
var isLoading by remember { mutableStateOf(false) }
// 계좌번호 입력 시 데이터 자동 로드 함수
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("API 및 계좌 설정", style = MaterialTheme.typography.h6)
OutlinedTextField(value = appKey, onValueChange = { appKey = it }, label = { Text("App Key") }, modifier = Modifier.fillMaxWidth())
OutlinedTextField(value = secretKey, onValueChange = { secretKey = it }, label = { Text("Secret Key") }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation())
OutlinedTextField(value = accountNo, onValueChange = { accountNo = it }, label = { Text("계좌번호") }, modifier = Modifier.fillMaxWidth())
Text("거래 방식 선택", style = MaterialTheme.typography.h6)
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = isSimulation, onCheckedChange = { isSimulation = it })
Text("모의투자 서버 사용")
RadioButton(selected = !config.isSimulation, onClick = { config = config.copy(isSimulation = false) })
Text("실전투자")
Spacer(Modifier.width(16.dp))
RadioButton(selected = config.isSimulation, onClick = { config = config.copy(isSimulation = true) })
Text("모의투자")
}
Divider(Modifier.padding(vertical = 12.dp))
// 실전 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))
// --- 추가된 AI 모델 설정 섹션 ---
Text("AI 모델 설정 (Gemma-2-9b)", style = MaterialTheme.typography.h6)
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = modelPath,
onValueChange = { modelPath = it },
label = { Text("GGUF 모델 경로") },
modifier = Modifier.weight(1f),
placeholder = { Text("파일을 선택하거나 드래그하세요") }
)
IconButton(onClick = {
val chooser = JFileChooser().apply {
fileFilter = FileNameExtensionFilter("GGUF 모델", "gguf")
}
if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
modelPath = chooser.selectedFile.absolutePath
}
}) {
Icon(Icons.Default.FolderOpen, contentDescription = "파일 선택")
}
}
// 드래그 앤 드롭 영역
// AI 모델 경로 및 드래그 앤 드롭
Text("AI 모델 설정", fontWeight = FontWeight.Bold)
Box(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.padding(top = 8.dp)
.border(1.dp, Color.LightGray, RoundedCornerShape(8.dp))
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) modelPath = path
if (path?.endsWith(".gguf") == true) config = config.copy(modelPath = path)
}
}),
contentAlignment = Alignment.Center
) {
Text("여기에 .gguf 파일을 드래그하여 놓으세요", fontSize = 12.sp, color = Color.Gray)
Text(if(config.modelPath.isEmpty()) "GGUF 모델 파일을 여기로 드래그하세요" else config.modelPath, fontSize = 12.sp)
}
Spacer(modifier = Modifier.height(24.dp))
Spacer(Modifier.height(24.dp))
// 저장 및 접속 버튼
Button(
modifier = Modifier.fillMaxWidth().height(50.dp),
enabled = !isLoading,
onClick = {
isLoading = true
scope.launch {
// 1. 새로운 설정 객체 생성 (순서 주의: isSimulation 다음 modelPath)
val config = AppConfig(
appKey = appKey.trim(),
secretKey = secretKey.trim(),
accountNo = accountNo.trim(),
isSimulation = isSimulation,
modelPath = modelPath
)
// isLoading = true
// 1. KisSession.config 업데이트 및 DB 저장
KisSession.config = config
DatabaseFactory.saveConfig(config)
val authService = KisAuthService()
val tradeService = KisTradeService()
val authSuccess = authService.refreshAllTokens()
val wsKeySuccess = tradeService.refreshWebsocketKey()
transaction {
ConfigTable.deleteAll()
ConfigTable.insert {
it[ConfigTable.appKey] = config.appKey
it[ConfigTable.secretKey] = config.secretKey
it[ConfigTable.accountNo] = config.accountNo
it[ConfigTable.isSimulation] = config.isSimulation
it[ConfigTable.modelPath] = config.modelPath
}
if (authSuccess && wsKeySuccess) {
statusMessage = "✅ 인증 성공! LLM 시작 중..."
onAuthSuccess()
} else {
statusMessage = "❌ 인증 실패. 키 정보를 확인하세요."
}
statusMessage = "인증 토큰 발급 시도 중..."
authService.fetchAccessToken(appKey, secretKey, isSimulation)
.onSuccess { response ->
statusMessage = "✅ 인증 성공!"
onAuthSuccess(config, response.access_token)
}.onFailure {
statusMessage = "❌ 인증 실패(정보 저장됨): ${it.localizedMessage}"
}
isLoading = false
// isLoading = false
}
}
) {
if (isLoading) {
// [수정된 프로그래스 바] size -> Modifier.size
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Text("설정 저장 및 접속 시작")
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(statusMessage, color = if (statusMessage.contains("")) Color.Green else Color.Gray)
) { Text("설정 저장 및 실행") }
Text(statusMessage, color = Color.Gray, modifier = Modifier.padding(top = 8.dp))
}
}
}
}