package ui import TradingDecision import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* // 아래 두 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.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import model.CandleData import network.DartCodeManager import network.KisTradeService import network.KisWebSocketManager import network.NewsService import service.TechnicalAnalyzer import java.time.LocalTime import java.time.format.DateTimeFormatter import kotlin.collections.isNotEmpty @Composable fun StockDetailSection( stockCode: String, stockName: String, holdingQuantity: String, isDomestic: Boolean, tradeService: KisTradeService, wsManager: KisWebSocketManager, onOrderSaved: (String) -> Unit, completeTradingDecision: TradingDecision? ) { var openPrice by remember { mutableStateOf("0") } var chartData by remember { mutableStateOf>(emptyList()) } var isLoading by remember { mutableStateOf(false) } var resultMessage by remember { mutableStateOf("") } var isSuccess by remember { mutableStateOf(true) } var daySummary by remember { mutableStateOf>(emptyList()) } var weekSummary by remember { mutableStateOf>(emptyList()) } var monthSummary by remember { mutableStateOf>(emptyList()) } var yearSummary by remember { mutableStateOf>(emptyList()) } val todayOpen = remember(daySummary) { daySummary.lastOrNull()?.stck_oprc ?: "0" } val previousClose = remember(daySummary) { if (daySummary.size >= 2) daySummary[daySummary.size - 2].stck_prpr else "0" } // 이전 종목 코드를 기억하기 위한 상태 var previousCode by remember { mutableStateOf("") } // 종목 변경 시 데이터 로드 및 웹소켓 구독 관리 LaunchedEffect(stockCode) { if (stockCode.isEmpty()) return@LaunchedEffect isLoading = true // 1. 웹소켓 구독 관리: 이전 종목 해제 -> 새 종목 구독 if (previousCode.isNotEmpty()) { wsManager.unsubscribeStock(previousCode) } wsManager.clearData() wsManager.subscribeStock(stockCode) previousCode = stockCode // 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화) coroutineScope { TechnicalAnalyzer.clear() launch {tradeService.fetchChartData(stockCode, isDomestic) .onSuccess { data -> println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력 chartData = data TechnicalAnalyzer.min30 = chartData } .onFailure { error -> println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}") chartData = emptyList() } } launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess { daySummary = it.takeLast(7) TechnicalAnalyzer.daily = it println("daySummary ${daySummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}") } } // 최근 7일 launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess { weekSummary = it.takeLast(4) TechnicalAnalyzer.weekly = it println("weekSummary ${weekSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}") } } // 최근 4주 launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess { monthSummary = it.takeLast(6) // 최근 6개월 yearSummary = it.takeLast(36) // 최근 3년 TechnicalAnalyzer.monthly = it println("monthSummary ${monthSummary.size} yearSummary ${yearSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}") } } launch { DartCodeManager.getCorpCode(stockCode)?.let { it.stockName = stockName NewsService.fetchAndIngestNews(it) } } } isLoading = false } val latestPrice by wsManager.currentPrice // 웹소켓에서 업데이트되는 현재가 LaunchedEffect(latestPrice) { if (chartData.isNotEmpty() && latestPrice != "0") { val priceDouble = latestPrice.replace(",", "").toDoubleOrNull() ?: return@LaunchedEffect val lastCandle = chartData.last() // 현재 시간(분 단위) 확인 val currentMinute = LocalTime.now().format(DateTimeFormatter.ofPattern("HHmm00")) if (lastCandle.stck_bsop_date != currentMinute) { // [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과) val newCandle = CandleData( stck_bsop_date = currentMinute, stck_oprc = latestPrice, stck_hgpr = latestPrice, stck_lwpr = latestPrice, stck_prpr = latestPrice, stck_cntg_hour = currentMinute, cntg_vol = "1", acml_tr_pbmn = "1", ) // 최대 100개까지만 유지하여 성능 최적화 chartData = (chartData + newCandle).takeLast(100) } else { // 같은 분 내에서는 기존 마지막 캔들만 업데이트 val updatedCandle = lastCandle.copy( stck_prpr = latestPrice, stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) latestPrice else lastCandle.stck_hgpr, stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) latestPrice else lastCandle.stck_lwpr ) chartData = chartData.dropLast(1) + updatedCandle } } } Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { // [상단] 종목명 및 상태 메시지 Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { StockHeader( name = stockName, code = stockCode, isDomestic = isDomestic, previousClose = previousClose, openPrice = openPrice, resultMessage = resultMessage, resultMessageClear = {resultMessage = ""}, isSuccess = isSuccess ) // 실시간 가격 표시 (WebSocket 데이터) Column(horizontalAlignment = Alignment.End) { Text( text = "${wsManager.currentPrice.value} 원", style = MaterialTheme.typography.h4, fontWeight = FontWeight.Bold, color = if (wsManager.currentPrice.value.contains("-")) Color.Blue else Color.Red ) Text("실시간 체결가", style = MaterialTheme.typography.caption, color = Color.Gray) } } // 통합된 트렌드 카드 배치 Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { PeriodTrendCard("7일", daySummary, Modifier.weight(1f)) PeriodTrendCard("4주", weekSummary, Modifier.weight(1f)) PeriodTrendCard("6개월", monthSummary, Modifier.weight(1f)) PeriodTrendCard("3년", yearSummary, Modifier.weight(1f)) } Spacer(modifier = Modifier.height(4.dp)) // [중앙] 캔들 차트 (Card 내부) Card( modifier = Modifier.fillMaxWidth().height(320.dp), backgroundColor = Color(0xFF121212) ) { if (isLoading) { Box(contentAlignment = Alignment.Center) { CircularProgressIndicator(color = Color.White) } } else { CandleChart(data = chartData, modifier = Modifier.padding(16.dp)) } } Spacer(modifier = Modifier.height(4.dp)) // [하단] 실시간 체결 내역 및 주문 섹션 Row(modifier = Modifier.weight(1f)) { // 실시간 체결 리스트 Column(modifier = Modifier.weight(1f)) { Text("실시간 체결", style = MaterialTheme.typography.subtitle2, fontWeight = FontWeight.Bold) RealTimeTradeList(wsManager.tradeLogs) } Spacer(modifier = Modifier.width(12.dp)) // 주문 섹션 (인자 간소화) Column(modifier = Modifier.weight(0.6f)) { IntegratedOrderSection( stockCode = stockCode, stockName = stockName, isDomestic = isDomestic, currentPrice = wsManager.currentPrice.value, holdingQuantity = holdingQuantity, tradeService = tradeService, onOrderSaved = onOrderSaved, onOrderResult = { msg, success -> resultMessage = msg isSuccess = success }, completeTradingDecision ) } } } } @Composable fun PeriodSummaryCard(label: String, avgPrice: String, modifier: Modifier = Modifier) { Card(modifier = modifier, elevation = 2.dp, backgroundColor = Color.White) { Column(modifier = Modifier.padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally) { Text(label, fontSize = 10.sp, color = Color.Gray) Text(text = "${avgPrice}원", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color.Black) } } }