// src/main/kotlin/ui/BalanceSection.kt package ui import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.* import androidx.compose.material.icons.filled.Refresh import androidx.compose.runtime.* 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.unit.dp import androidx.compose.ui.unit.sp import model.UnifiedBalance import network.KisTradeService @Composable fun BalanceSection( tradeService: KisTradeService, refreshTrigger: Int, // 갱신 트리거 추가 onRefresh: () -> Unit, onStockSelect: (code: String, name: String, isDomestic: Boolean,quantity: String) -> Unit ) { var balanceData by remember { mutableStateOf(null) } var isLoading by remember { mutableStateOf(false) } // 화면 진입 시 및 갱신 시 데이터 로드 LaunchedEffect(refreshTrigger) { isLoading = true tradeService.fetchIntegratedBalance().onSuccess { balanceData = it }.onFailure { println("❌ 잔고 로드 실패: ${it.localizedMessage}") } isLoading = false } Column(modifier = Modifier.fillMaxSize()) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = "나의 자산", style = MaterialTheme.typography.h6, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 8.dp) ) // 강제 갱신 버튼 IconButton( onClick = onRefresh, modifier = Modifier.size(24.dp) ) { Icon( imageVector = androidx.compose.material.icons.Icons.Default.Refresh, contentDescription = "새로고침", tint = Color(0xFF0E62CF), modifier = Modifier.size(18.dp) ) } } // 1. 자산 요약 카드 BalanceSummaryCard(balanceData) Spacer(modifier = Modifier.height(16.dp)) // 2. 통합 보유 종목 리스트 Text( text = "보유 종목", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold, modifier = Modifier.padding(vertical = 8.dp) ) if (isLoading) { Box(Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center) { CircularProgressIndicator() } } else { LazyColumn(modifier = Modifier.weight(1f, true)) { items(balanceData?.holdings ?: emptyList()) { holding -> UnifiedStockItemRow(holding) { onStockSelect(holding.code, holding.name, holding.isDomestic, holding.quantity) } } } } } } @Composable fun BalanceSummaryCard(summary: UnifiedBalance?) { Card( elevation = 2.dp, shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth(), backgroundColor = androidx.compose.ui.graphics.Color.White ) { Column(modifier = Modifier.padding(16.dp)) { Text("총 평가 자산", style = MaterialTheme.typography.caption, color = androidx.compose.ui.graphics.Color.Gray) Text( text = "${summary?.totalAsset ?: "0"} 원", style = MaterialTheme.typography.h5, fontWeight = FontWeight.Bold ) val rate = summary?.totalProfitRate?.toDoubleOrNull() ?: 0.0 val color = if (rate > 0) androidx.compose.ui.graphics.Color.Red else if (rate < 0) androidx.compose.ui.graphics.Color.Blue else androidx.compose.ui.graphics.Color.DarkGray Text( text = "수익률: ${if (rate > 0) "+" else ""}$rate%", color = color, style = MaterialTheme.typography.body2, fontWeight = FontWeight.Medium ) } } } @Composable fun UnifiedStockItemRow(holding: model.UnifiedStockHolding, onClick: () -> Unit) { val avgPrice = holding.avgPrice.toDoubleOrNull() ?: 0.0 val breakEvenPrice = if (avgPrice > 0) avgPrice / 0.9978 else 0.0 Card( modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).clickable { onClick() }, elevation = 1.dp ) { Row(modifier = Modifier.padding(12.dp), verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) { // 국내/해외 구분 배지 Surface( color = if (holding.isDomestic) androidx.compose.ui.graphics.Color(0xFFE3F2FD) else androidx.compose.ui.graphics.Color(0xFFF3E5F5), shape = androidx.compose.foundation.shape.RoundedCornerShape(4.dp) ) { Text( text = if (holding.isDomestic) "국내" else "해외", modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), fontSize = 10.sp, color = if (holding.isDomestic) androidx.compose.ui.graphics.Color.Blue else androidx.compose.ui.graphics.Color(0xFF7B1FA2) ) } Spacer(Modifier.width(4.dp)) Text(holding.name, fontWeight = FontWeight.Bold, maxLines = 1) } Text(holding.code, style = MaterialTheme.typography.caption, color = androidx.compose.ui.graphics.Color.Gray) } Column(horizontalAlignment = Alignment.End) { Text("${holding.currentPrice} 원", fontWeight = FontWeight.Bold) // 손익분기점 표시 추가 Text( "손익분기: ${String.format("%,.0f", breakEvenPrice)}원", fontSize = 10.sp, color = Color(0xFF666666) ) val rate = holding.profitRate.toDoubleOrNull() ?: 0.0 Text( text = "${if (rate > 0) "+" else ""}${holding.profitRate}%", color = if (rate > 0) Color.Red else if (rate < 0) Color.Blue else Color.DarkGray, fontSize = 12.sp, fontWeight = FontWeight.Bold ) Text( "매수: ${String.format("%,.0f", avgPrice)}원 ${holding.quantity}", fontSize = 11.sp, color = Color.Gray ) } } } }