package ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items // 반드시 수동 import 확인 import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import io.ktor.client.engine.cio.CIO // 아래 두 import가 'delegate' 에러를 해결합니다. import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch import model.AppConfig import model.BalanceSummary import model.RankingStock import model.RankingType import model.StockHolding import network.KisTradeService import network.KisWebSocketManager import util.MarketUtil @Composable fun DashboardScreen(config: AppConfig, token: String) { val wsManager = remember { KisWebSocketManager(config.isSimulation) } val tradeService = remember { KisTradeService(config.isSimulation) } // 전역 상태: 현재 선택된 종목 var selectedStockCode by remember { mutableStateOf("") } var selectedStockName by remember { mutableStateOf("") } // 잔고 데이터 상태 var holdings by remember { mutableStateOf>(emptyList()) } var summary by remember { mutableStateOf(null) } // 초기 데이터 로드 및 웹소켓 연결 LaunchedEffect(Unit) { val approvalKey = tradeService.fetchApprovalKey(config.appKey, config.secretKey) approvalKey?.let { wsManager.connect(it) } tradeService.fetchBalance(token, config.appKey, config.secretKey, config.accountNo) .onSuccess { holdings = it.output1 summary = it.output2.firstOrNull() } } // 메인 3분할 레이아웃 Row(modifier = Modifier.fillMaxSize().background(Color(0xFFF2F2F2))) { // [좌측 25%] 나의 자산 및 잔고 Column(modifier = Modifier.weight(0.25f).fillMaxHeight().padding(8.dp)) { Text("나의 잔고", style = MaterialTheme.typography.h6, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) BalanceSummaryCard(summary) Spacer(modifier = Modifier.height(8.dp)) MyStockList(holdings) { code, name -> selectedStockCode = code selectedStockName = name } } VerticalDivider() // [중앙 45%] 실시간 차트 및 주문 (가장 중요) Column(modifier = Modifier.weight(0.45f).fillMaxHeight().background(Color.White).padding(12.dp)) { if (selectedStockCode.isNotEmpty()) { StockDetailArea(config, token, selectedStockCode, selectedStockName, wsManager) } else { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("좌측 잔고나 우측 추천 종목을 클릭하세요", color = Color.Gray) } } } VerticalDivider() // [우측 30%] 시장 추천 리스트 (탭 방식) Column(modifier = Modifier.weight(0.3f).fillMaxHeight().padding(8.dp)) { Text("시장 추천 TOP 20", style = MaterialTheme.typography.h6, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.height(8.dp)) RecommendationTabs(config, token) { code, name -> selectedStockCode = code selectedStockName = name } } } } @Composable fun StockItemRow(stock: StockHolding) { Row( modifier = Modifier.fillMaxWidth().padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Column(modifier = Modifier.weight(1f)) { Text(stock.prdt_name, fontWeight = FontWeight.Bold, fontSize = 16.sp) Text(stock.pdno, fontSize = 12.sp, color = Color.Gray) } Column(horizontalAlignment = Alignment.End) { Text("${stock.prpr} 원", fontWeight = FontWeight.Bold) // 수익률에 따른 색상 처리 (웹 소스 format.color 로직 이식) val rate = stock.evlu_pfls_rt.toDoubleOrNull() ?: 0.0 val color = when { rate > 0 -> Color(0xFFE03E2D) // 웹 소스의 빨간색 rate < 0 -> Color(0xFF0E62CF) // 웹 소스의 파란색 else -> Color.DarkGray } Text( text = "${if(rate > 0) "▲" else if(rate < 0) "▼" else ""} ${stock.evlu_pfls_rt}%", color = color, fontSize = 13.sp, fontWeight = FontWeight.Medium ) } } } @Composable fun RankingItemRow( index: Int, // 순위 표시를 위해 index 추가 rank: RankingStock, isDomestic: Boolean, type: RankingType, onClick: () -> Unit ) { val displayColor = when { type == RankingType.FALL -> Color(0xFF0E62CF) // 하락 탭은 무조건 파랑 rank.prdy_ctrt.toDoubleOrNull() ?: 0.0 > 0 -> Color(0xFFE03E2D) // 그 외 양수면 빨강 rank.prdy_ctrt.toDoubleOrNull() ?: 0.0 < 0 -> Color(0xFF0E62CF) // 음수면 파랑 else -> Color.DarkGray } Card( modifier = Modifier .fillMaxWidth() .clickable { onClick() }, elevation = 0.dp, backgroundColor = Color.White ) { Row( modifier = Modifier.padding(vertical = 10.dp, horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically ) { // [1] 순위 표시 (1~20) Text( text = "${index + 1}", style = MaterialTheme.typography.caption, fontWeight = FontWeight.Bold, color = if (index < 3) displayColor else Color.Gray, // 1~3위 강조 modifier = Modifier.width(24.dp) ) // [2] 종목명 및 코드 Column(modifier = Modifier.weight(1f)) { Text( text = rank.hts_kor_alph_nm, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis ) Text( text = rank.mkrtc_objt_iscd, fontSize = 11.sp, color = Color.Gray ) } // [3] 등락률 배지 Surface( color = displayColor.copy(alpha = 0.1f), shape = RoundedCornerShape(4.dp) ) { Text( text = "${if (rank.prdy_ctrt.toDouble() > 0) "+" else ""}${rank.prdy_ctrt}%", color = displayColor, fontSize = 12.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) } } } } @Composable fun VerticalDivider(modifier: Modifier = Modifier) { Box(modifier.fillMaxHeight().width(1.dp).background(Color.LightGray)) } @Composable fun BalanceSummaryCard(summary: BalanceSummary?) { Card( elevation = 4.dp, shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth(), backgroundColor = Color(0xFFF8F9FA) // 가벼운 배경색 ) { Column(modifier = Modifier.padding(20.dp)) { Text("총 평가 자산", style = MaterialTheme.typography.caption, color = Color.Gray) Text( text = "${summary?.tot_evlu_amt ?: "0"} 원", style = MaterialTheme.typography.h5, fontWeight = FontWeight.Bold, color = Color(0xFF333333) ) Spacer(modifier = Modifier.height(8.dp)) val profitRate = summary?.evlu_pfls_rt?.toDoubleOrNull() ?: 0.0 val profitColor = if (profitRate > 0) Color(0xFFE03E2D) else if (profitRate < 0) Color(0xFF0E62CF) else Color.DarkGray Row(verticalAlignment = Alignment.CenterVertically) { Text("실현 수익률: ", style = MaterialTheme.typography.body2) Text( text = "${if (profitRate > 0) "+" else ""}$profitRate%", style = MaterialTheme.typography.body1, fontWeight = FontWeight.Bold, color = profitColor ) } } } } @Composable fun StockItemRow(stock: StockHolding, onClick: () -> Unit) { Card( modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp) .clickable { onClick() }, elevation = 2.dp, shape = RoundedCornerShape(4.dp) ) { Row( modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically ) { // 1. 종목명 및 코드 (왼쪽) Column(modifier = Modifier.weight(1.2f)) { Text( text = stock.prdt_name, style = MaterialTheme.typography.body1, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis ) Text( text = stock.pdno, style = MaterialTheme.typography.caption, color = Color.Gray ) } // 2. 보유 수량 및 현재가 (중앙) Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.End) { Text("${stock.hldg_qty} 주", style = MaterialTheme.typography.body2) Text( text = "${stock.prpr} 원", style = MaterialTheme.typography.caption, color = Color.DarkGray ) } // 3. 수익률 (오른쪽) val rate = stock.evlu_pfls_rt.toDoubleOrNull() ?: 0.0 val color = if (rate > 0) Color(0xFFE03E2D) else if (rate < 0) Color(0xFF0E62CF) else Color.DarkGray Box( modifier = Modifier.weight(0.8f), contentAlignment = Alignment.CenterEnd ) { Surface( color = color.copy(alpha = 0.1f), shape = RoundedCornerShape(4.dp) ) { Text( text = "${if (rate > 0) "▲" else if (rate < 0) "▼" else ""}${stock.evlu_pfls_rt}%", color = color, style = MaterialTheme.typography.body2, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) } } } } }