302 lines
11 KiB
Kotlin
302 lines
11 KiB
Kotlin
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<List<StockHolding>>(emptyList()) }
|
|
var summary by remember { mutableStateOf<BalanceSummary?>(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)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |