// src/main/kotlin/ui/DashboardScreen.kt package ui import AutoTradeItem import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import model.KisSession import network.KisTradeService import network.KisWebSocketManager @Composable fun DashboardScreen() { val tradeService = remember { KisTradeService() } val wsManager = remember { KisWebSocketManager() } val config = KisSession.config val scope = rememberCoroutineScope() // 데이터 갱신을 위한 트리거 상태 var refreshTrigger by remember { mutableStateOf(0) } // 전역 상태: 현재 선택된 종목 정보 var selectedStockCode by remember { mutableStateOf("") } var selectedStockName by remember { mutableStateOf("") } var isDomestic by remember { mutableStateOf(true) } LaunchedEffect(Unit) { // 1. 웹소켓 연결 wsManager.connect() // 2. 체결 통보 콜백 설정 (매수 성공 시 감시 시작) wsManager.onExecutionReceived = { orderNo, code, price, qty, isBuy -> if (isBuy) { // [매수 체결 시] DB에 감시 데이터 저장 // 주의: targetPrice와 stopLossPrice는 이전에 설정된 값을 가져오거나 // 임시 상태값에서 가져와야 함 (여기선 예시로 현재가의 +5%, -3% 설정) val execPrice = price.toDoubleOrNull() ?: 0.0 DatabaseFactory.saveAutoTrade( AutoTradeItem( code = code, name = "", // 필요 시 종목명 매핑 targetPrice = execPrice * 1.05, stopLossPrice = execPrice * 0.97, status = "MONITORING", isDomestic = true ) ) println("📝 매수 체결로 인한 자동 감시 등록: $code") } else { // [매도 체결 시] 감시 종료 및 DB 삭제 DatabaseFactory.deleteAutoTrade(code) println("✅ 매도 체결로 인한 감시 종료: $code") } refreshTrigger++ } // 3. 목표가 도달 콜백 설정 (자동 매도 실행) wsManager.onTargetReached = { code, price, isProfit -> scope.launch { println("🚀 목표가 도달! 자동 매도 주문 실행: $code (이유: ${if(isProfit) "익절" else "손절"})") // 실제 매도 주문 API 호출 tradeService.postOrder( stockCode = code, qty = "1", // 실제론 보유 수량을 가져와야 함 price = "0", // 시장가 매도 isBuy = false ).onSuccess { // 매도 주문 성공 시 로그 기록 DatabaseFactory.saveTradeLog( code, "", "매도", price, 1, if(isProfit) "AI 익절 조건 달성" else "AI 손절 조건 달성" ) } } refreshTrigger++ } if (config.htsId.isNotEmpty()) { wsManager.subscribeExecution(config.htsId) println("📡 HTS ID(${config.htsId})로 체결 통보 구독을 시작합니다.") } } Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) { // [좌측 25%] 내 자산 및 통합 잔고 Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) { BalanceSection(tradeService) { code, name, isDom -> selectedStockCode = code selectedStockName = name isDomestic = isDom println("selectedStockCode $selectedStockCode selectedStockName $selectedStockName isDomestic $isDomestic") } } VerticalDivider() // [중앙 45%] 실시간 정보 및 주문 Column(modifier = Modifier.weight(0.45f).fillMaxHeight().background(Color.White)) { if (selectedStockCode.isNotEmpty()) { StockDetailSection( stockCode = selectedStockCode, stockName = selectedStockName, isDomestic = isDomestic, tradeService = tradeService, wsManager = wsManager ) } else { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("분석할 종목을 선택하세요", color = Color.Gray) } } } VerticalDivider() Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) { AutoTradeSection( tradeService = tradeService, onRefresh = { refreshTrigger++ }, refreshTrigger = refreshTrigger // 트리거 전달 ) { item -> selectedStockCode = item.code selectedStockName = item.name isDomestic = item.isDomestic } } VerticalDivider() // [우측 30%] 시장 추천 TOP 20 (실전 데이터) Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) { MarketSection(tradeService) { code, name, isDom -> selectedStockCode = code selectedStockName = name isDomestic = isDom println("selectedStockCode $selectedStockCode selectedStockName $selectedStockName isDomestic $isDomestic") } } } } @Composable fun VerticalDivider() { Box(Modifier.fillMaxHeight().width(1.dp).background(Color.LightGray)) }