// 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 model.StockBasicInfo import network.KisTradeService import network.KisWebSocketManager import util.MarketUtil @Composable fun DashboardScreen() { val tradeService = remember { KisTradeService() } val wsManager = remember { KisWebSocketManager() } 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) } var selectedStockQuantity by remember { mutableStateOf("0") } var selectedItem by remember { mutableStateOf(null) } // 감시/미체결 아이템 선택 시 var selectedStockInfo by remember { mutableStateOf(null) } // 단순 종목 선택 시 LaunchedEffect(Unit) { // 1. 웹소켓 연결 wsManager.connect() // 2. [기동 시 동기화 시나리오] scope.launch { // (1) 서버 미체결 내역 로드 val serverOrders = tradeService.fetchUnfilledOrders().getOrDefault(emptyList()) val serverOrderNos = serverOrders.map { it.ord_no } // (2) DB 상태 대조 및 EXPIRED 전환 DatabaseFactory.syncWithServer(serverOrderNos) // (3) 활성 감시 종목 구독 재개 val monitoringTrades = DatabaseFactory.getAutoTradesByStatus(listOf(TradeStatus.MONITORING, TradeStatus.PENDING_BUY)) val monitoringCodes = monitoringTrades.map { it.code }.toSet() wsManager.updateSubscriptions(monitoringCodes) refreshTrigger++ } // 3. 실시간 체결 통보 핸들러 (주문번호 중심) wsManager.onExecutionReceived = { orderNo, code, price, qty, isBuy -> scope.launch { val dbItem = DatabaseFactory.findByOrderNo(orderNo) if (dbItem != null) { when (dbItem.status) { TradeStatus.PENDING_BUY -> { // 1. 매수 주문 체결 확인됨 -> 즉시 익절 매도 주문 발주 println("✅ 매수 체결 확인 [${dbItem.name}]: 익절가 ${dbItem.targetPrice}로 매도 주문을 생성합니다.") tradeService.postOrder( stockCode = dbItem.code, qty = dbItem.quantity.toString(), price = dbItem.targetPrice.toLong().toString(), // 가격은 정수형 문자열로 전달 isBuy = false ).onSuccess { newSellOrderNo -> // 2. 매도 주문 성공 시 DB 상태를 SELLING으로 변경하고 새로운 주문번호로 갱신 DatabaseFactory.updateStatusAndOrderNo( id = dbItem.id!!, newStatus = TradeStatus.SELLING, newOrderNo = newSellOrderNo ) println("🚀 익절 매도 주문 완료: 주문번호 $newSellOrderNo") refreshTrigger++ // UI 갱신 }.onFailure { println("❌ 매수 체결 후 익절 주문 발주 실패: ${it.message}") } } TradeStatus.SELLING -> { // 매도(손절/익절) 주문 체결 -> COMPLETED DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.COMPLETED) } } refreshTrigger++ } } } } Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) { // [좌측 25%] 내 자산 및 통합 잔고 Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) { BalanceSection(tradeService, onRefresh = { refreshTrigger++ }, refreshTrigger = refreshTrigger) { code, name, isDom,qty -> selectedStockCode = code selectedStockName = name isDomestic = isDom selectedStockQuantity = qty 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, holdingQuantity = selectedStockQuantity, 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( isDomestic = isDomestic, tradeService = tradeService, onRefresh = { refreshTrigger++ }, refreshTrigger = refreshTrigger , // 트리거 전달 onItemCancel = { item -> scope.launch { tradeService.cancelOrder(item.orderNo,item.code).onSuccess { refreshTrigger++ } } }, onItemSelect = { 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 -> val info = StockBasicInfo( code = code, name = name, isDomestic = isDom ) selectedStockInfo = info 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)) }