This commit is contained in:
2026-04-09 13:17:52 +09:00
parent 8bb099e4f6
commit 95e44b18d0
6 changed files with 11832 additions and 9160 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ fun SettingsScreen(onAuthSuccess: () -> Unit) {
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()) {
if (config.realAppKey.isNotEmpty() && config.vtsAppKey.isNotEmpty() && config.embedModelPath.isNotEmpty() && config.modelPath.isNotEmpty()) {
SystemSleepPreventer.wakeDisplay() // 모니터 켜기
statusMessage = "⏰ 자동 실행 시간(08:30)입니다. 시스템을 가동합니다."
authenticateAndStart()
+107 -1
View File
@@ -2,6 +2,7 @@ package ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
@@ -9,15 +10,20 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
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.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.onExternalDrag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -26,7 +32,10 @@ import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import model.ConfigIndex
import model.KisSession
import network.StockUniverseLoader
import service.AutoTradingManager
import java.io.File
import java.net.URI
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -140,6 +149,19 @@ fun TradingDecisionLog() {
}
Divider(Modifier.padding(bottom = 8.dp))
CsvDropZone(
onUniverseUpdated = { updatedList ->
// UI 갱신 (필요한 경우)
// currentUniverse = updatedList
// 💡 봇의 실제 작업 큐인 loadedTops 에도 갱신된 데이터를 덮어씌워 줌
AutoTradingManager.loadedTops.clear()
AutoTradingManager.loadedTops.addAll(updatedList)
AutoTradingManager.loadedTops.shuffle() // 섞어주면 편향 분석 방지!
}
)
Spacer(modifier = Modifier.height(16.dp))
// [수정] filteredLogs를 사용하여 최신 로그가 위로 오게 표시
LazyColumn(
@@ -426,4 +448,88 @@ fun getRemaining(original: String, common: String): String {
if (common.isEmpty()) return original
// 가장 처음 발견되는 공통 문자열을 한 번만 제거
return original.replaceFirst(common, "").trim()
}
}
fun parseSmartCsv(file: File): List<Pair<String, String>> {
val result = mutableListOf<Pair<String, String>>()
try {
val lines = file.readLines()
if (lines.isEmpty()) return result
// 1. 헤더 분석하여 열(Column) 인덱스 자동 추적
val headers = lines[0].split(",").map { it.replace("\"", "").trim() }
val codeIndex = headers.indexOfFirst { it.contains("종목코드") }
val nameIndex = headers.indexOfFirst { it.contains("종목명") }
// 헤더를 못 찾았다면 기본값 (0번: 코드, 1번: 이름)으로 폴백
val finalCodeIdx = if (codeIndex != -1) codeIndex else 0
val finalNameIdx = if (nameIndex != -1) nameIndex else 1
// 2. 데이터 추출
for (i in 1 until lines.size) {
val line = lines[i]
if (line.isBlank()) continue
val parts = line.split(",").map { it.replace("\"", "").trim() }
if (parts.size > maxOf(finalCodeIdx, finalNameIdx)) {
val code = parts[finalCodeIdx]
val name = parts[finalNameIdx]
// 6자리 숫자로 된 정상적인 종목코드인지 검증
if (code.length == 6 && code.all { it.isDigit() }) {
result.add(code to name)
}
}
}
} catch (e: Exception) {
println("❌ CSV 파싱 에러: ${e.message}")
}
return result
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CsvDropZone(
onUniverseUpdated: (List<Pair<String, String>>) -> Unit
) {
var isDragging by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.background(if (isDragging) Color(0xFFE3F2FD) else Color(0xFFFAFAFA))
.border(
width = 1.dp,
color = if (isDragging) Color.Blue else Color.LightGray,
shape = RoundedCornerShape(8.dp)
)
.onExternalDrag(
onDragStart = { isDragging = true },
onDragExit = { isDragging = false },
onDrop = { state ->
isDragging = false
val dragData = state.dragData
if (dragData is DragData.FilesList) {
val fileUris = dragData.readFiles()
fileUris.firstOrNull { it.endsWith(".csv", ignoreCase = true) }?.let { uri ->
// 💡 여기서 깔끔하게 StockUniverseLoader 에 처리를 위임!
val file = File(URI(uri))
val updatedUniverse = StockUniverseLoader.parseAndMergeCsv(file)
onUniverseUpdated(updatedUniverse)
}
}
}
),
contentAlignment = Alignment.Center
) {
Text(
text = if (isDragging) "📥 파일을 놓아서 업데이트!" else "📁 [CSV 추가] 파일을 드래그하여 유니버스 자동 병합",
color = if (isDragging) Color.Blue else Color.Gray,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold
)
}
}