atrade/src/main/kotlin/ui/DashboardScreen.kt

171 lines
7.3 KiB
Kotlin
Raw Normal View History

2026-01-13 16:04:25 +09:00
// src/main/kotlin/ui/DashboardScreen.kt
2026-01-10 18:16:50 +09:00
package ui
2026-01-14 15:42:26 +09:00
import AutoTradeItem
2026-01-10 18:16:50 +09:00
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
2026-01-14 15:42:26 +09:00
import kotlinx.coroutines.launch
2026-01-13 16:04:25 +09:00
import model.KisSession
2026-01-19 17:09:37 +09:00
import model.StockBasicInfo
2026-01-10 18:16:50 +09:00
import network.KisTradeService
import network.KisWebSocketManager
2026-01-19 17:09:37 +09:00
import util.MarketUtil
2026-01-10 18:16:50 +09:00
@Composable
2026-01-13 16:04:25 +09:00
fun DashboardScreen() {
val tradeService = remember { KisTradeService() }
val wsManager = remember { KisWebSocketManager() }
2026-01-14 15:42:26 +09:00
val scope = rememberCoroutineScope()
var refreshTrigger by remember { mutableStateOf(0) }
2026-01-10 18:16:50 +09:00
var selectedStockCode by remember { mutableStateOf("") }
var selectedStockName by remember { mutableStateOf("") }
2026-01-13 16:04:25 +09:00
var isDomestic by remember { mutableStateOf(true) }
2026-01-19 17:09:37 +09:00
var selectedStockQuantity by remember { mutableStateOf("0") }
var selectedItem by remember { mutableStateOf<AutoTradeItem?>(null) } // 감시/미체결 아이템 선택 시
var selectedStockInfo by remember { mutableStateOf<StockBasicInfo?>(null) } // 단순 종목 선택 시
2026-01-10 18:16:50 +09:00
LaunchedEffect(Unit) {
2026-01-14 15:42:26 +09:00
// 1. 웹소켓 연결
2026-01-13 16:04:25 +09:00
wsManager.connect()
2026-01-14 15:42:26 +09:00
2026-01-19 17:09:37 +09:00
// 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)
2026-01-14 15:42:26 +09:00
refreshTrigger++
}
2026-01-19 17:09:37 +09:00
// 3. 실시간 체결 통보 핸들러 (주문번호 중심)
wsManager.onExecutionReceived = { orderNo, code, price, qty, isBuy ->
2026-01-14 15:42:26 +09:00
scope.launch {
2026-01-19 17:09:37 +09:00
val dbItem = DatabaseFactory.findByOrderNo(orderNo)
if (dbItem != null) {
when (dbItem.status) {
TradeStatus.PENDING_BUY -> {
2026-01-20 15:13:50 +09:00
// 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}")
}
2026-01-19 17:09:37 +09:00
}
TradeStatus.SELLING -> {
// 매도(손절/익절) 주문 체결 -> COMPLETED
DatabaseFactory.updateStatusAndOrderNo(dbItem.id!!, TradeStatus.COMPLETED)
}
}
refreshTrigger++
2026-01-14 15:42:26 +09:00
}
}
}
2026-01-10 18:16:50 +09:00
}
Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) {
2026-01-13 16:04:25 +09:00
// [좌측 25%] 내 자산 및 통합 잔고
2026-01-14 15:42:26 +09:00
Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) {
2026-01-19 17:09:37 +09:00
BalanceSection(tradeService,
onRefresh = { refreshTrigger++ },
refreshTrigger = refreshTrigger) { code, name, isDom,qty ->
2026-01-10 18:16:50 +09:00
selectedStockCode = code
selectedStockName = name
2026-01-13 16:04:25 +09:00
isDomestic = isDom
2026-01-19 17:09:37 +09:00
selectedStockQuantity = qty
2026-01-13 16:04:25 +09:00
println("selectedStockCode $selectedStockCode selectedStockName $selectedStockName isDomestic $isDomestic")
2026-01-10 18:16:50 +09:00
}
}
VerticalDivider()
2026-01-13 16:04:25 +09:00
// [중앙 45%] 실시간 정보 및 주문
Column(modifier = Modifier.weight(0.45f).fillMaxHeight().background(Color.White)) {
2026-01-10 18:16:50 +09:00
if (selectedStockCode.isNotEmpty()) {
2026-01-13 16:04:25 +09:00
StockDetailSection(
stockCode = selectedStockCode,
stockName = selectedStockName,
2026-01-19 17:09:37 +09:00
holdingQuantity = selectedStockQuantity,
2026-01-13 16:04:25 +09:00
isDomestic = isDomestic,
tradeService = tradeService,
wsManager = wsManager
)
2026-01-10 18:16:50 +09:00
} else {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
2026-01-13 16:04:25 +09:00
Text("분석할 종목을 선택하세요", color = Color.Gray)
2026-01-10 18:16:50 +09:00
}
}
}
VerticalDivider()
2026-01-14 15:42:26 +09:00
Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) {
AutoTradeSection(
2026-01-19 17:09:37 +09:00
isDomestic = isDomestic,
2026-01-14 15:42:26 +09:00
tradeService = tradeService,
onRefresh = { refreshTrigger++ },
2026-01-19 17:09:37 +09:00
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
})
2026-01-14 15:42:26 +09:00
}
VerticalDivider()
2026-01-13 16:04:25 +09:00
// [우측 30%] 시장 추천 TOP 20 (실전 데이터)
2026-01-14 15:42:26 +09:00
Column(modifier = Modifier.weight(0.18f).fillMaxHeight().padding(8.dp)) {
2026-01-13 16:04:25 +09:00
MarketSection(tradeService) { code, name, isDom ->
2026-01-19 17:09:37 +09:00
val info = StockBasicInfo(
code = code,
name = name,
isDomestic = isDom
)
selectedStockInfo = info
2026-01-10 18:16:50 +09:00
selectedStockCode = code
selectedStockName = name
2026-01-13 16:04:25 +09:00
isDomestic = isDom
println("selectedStockCode $selectedStockCode selectedStockName $selectedStockName isDomestic $isDomestic")
2026-01-10 18:16:50 +09:00
}
}
}
}
@Composable
2026-01-13 16:04:25 +09:00
fun VerticalDivider() {
Box(Modifier.fillMaxHeight().width(1.dp).background(Color.LightGray))
2026-01-10 18:16:50 +09:00
}