180 lines
7.0 KiB
Kotlin
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
|
|
)
|
|
}
|
|
}
|
|
}
|
|
} |