atrade/src/main/kotlin/ui/BalanceSection.kt
2026-01-23 17:05:09 +09:00

180 lines
7.0 KiB
Kotlin

// 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<UnifiedBalance?>(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
)
}
}
}
}