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.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.delay import kotlinx.coroutines.launch import model.AppConfig import model.BalanceSummary import model.CandleData import model.RankingStock import model.StockHolding import network.KisTradeService import network.KisWebSocketManager import kotlin.collections.isNotEmpty @Composable fun StockDetailArea( config: AppConfig, token: String, code: String, name: String, wsManager: KisWebSocketManager // 매니저 수신 ) { val currentPrice by wsManager.currentPrice val priceColor by wsManager.priceChangeColor val tradeLogs = wsManager.tradeLogs // Manager의 상태를 직접 참조 val tradeService = remember { KisTradeService(config.isSimulation) } var chartData by remember { mutableStateOf>(emptyList()) } var isLoading by remember { mutableStateOf(false) } var resultMessage by remember { mutableStateOf("") } var isSuccess by remember { mutableStateOf(true) } LaunchedEffect(code) { if (code.isEmpty()) return@LaunchedEffect isLoading = true if (code.isNotEmpty()) { // 기존 종목 구독 해지 및 새 종목 구독 메시지 전송 // (KisWebSocketManager에 해당 기능을 하는 함수를 만들어서 호출) wsManager.subscribeStock(code) } // 종목 코드 판별 (숫자 6자리면 국내, 아니면 해외로 간주) val isDomestic = code.all { it.isDigit() } && code.length == 6 val result = if (isDomestic) { tradeService.fetchChartData(token, config.appKey, config.secretKey, code) .map { it.output2.reversed() } } else { // 해외 주식 처리 (우선 NAS 나스닥 기준으로 호출) tradeService.fetchOverseasChartData(token, config.appKey, config.secretKey, code) } result.onSuccess { chartData = it } .onFailure { println("차트 로드 실패: ${it.message}") } isLoading = false } LaunchedEffect(resultMessage) { if (resultMessage.isNotEmpty()) { delay(3000) resultMessage = "" } } Column(modifier = Modifier.fillMaxSize()) { // [상단 정보] 국내/해외 구분 배지 추가 if (resultMessage.isNotEmpty()) { Surface( color = if (isSuccess) Color(0xFF4CAF50) else Color(0xFFF44336), modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp) ) { Text( text = resultMessage, color = Color.White, modifier = Modifier.padding(8.dp), textAlign = TextAlign.Center, fontSize = 12.sp ) } } Row(verticalAlignment = Alignment.CenterVertically) { val isDomestic = code.all { it.isDigit() } && code.length == 6 Badge(backgroundColor = if (isDomestic) Color(0xFFE03E2D) else Color(0xFF0E62CF)) { Text(if (isDomestic) "국내" else "해외", color = Color.White, fontSize = 10.sp) } Spacer(modifier = Modifier.width(8.dp)) Text(name, style = MaterialTheme.typography.h5, fontWeight = FontWeight.Bold) Text(" ($code)", color = Color.Gray) } Spacer(modifier = Modifier.height(16.dp)) // [차트 영역] CandleChart 컴포저블 재사용 Card( modifier = Modifier.fillMaxWidth().height(350.dp), backgroundColor = Color(0xFF121212) ) { if (isLoading) { Box(contentAlignment = Alignment.Center) { CircularProgressIndicator(color = Color.White) } } else if (chartData.isNotEmpty()) { CandleChart(data = chartData, modifier = Modifier.padding(16.dp)) } else { Box(contentAlignment = Alignment.Center) { Text("데이터가 없습니다.", color = Color.Gray) } } } Spacer(modifier = Modifier.height(16.dp)) AiAnalysisView( stockName = name, currentPrice = wsManager.currentPrice.value, trades = wsManager.tradeLogs ) Spacer(modifier = Modifier.height(16.dp)) // 웹 소스 스타일의 주문 박스 // Card(modifier = Modifier.fillMaxWidth(), backgroundColor = Color(0xFFF8F9FA)) { // Column(modifier = Modifier.padding(16.dp)) { // Text("주문 설정", fontWeight = FontWeight.Bold) // // 수량 입력, 매수/매도 버튼 배치 (detail.html 참고) // Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { // Button(onClick = { /* 매수 */ }, modifier = Modifier.weight(1f), colors = ButtonDefaults.buttonColors(Color(0xFFE03E2D))) { // Text("매수", color = Color.White) // } // Button(onClick = { /* 매도 */ }, modifier = Modifier.weight(1f), colors = ButtonDefaults.buttonColors(Color(0xFF0E62CF))) { // Text("매도", color = Color.White) // } // } // } // } Column(modifier = Modifier.weight(0.4f)) { Text("실시간 체결", style = MaterialTheme.typography.subtitle2, fontWeight = FontWeight.Bold) // 헤더 영역 Row(modifier = Modifier.fillMaxWidth().background(Color(0xFFEEEEEE)).padding(vertical = 4.dp)) { Text("시간", modifier = Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 11.sp) Text("체결가", modifier = Modifier.weight(1.5f), textAlign = TextAlign.Center, fontSize = 11.sp) Text("대비", modifier = Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 11.sp) Text("체결량", modifier = Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 11.sp) } // 실시간 리스트 LazyColumn(modifier = Modifier.fillMaxSize()) { items(tradeLogs) { trade -> TradeLogRow(trade) Divider(color = Color(0xFFF5F5F5)) } } } OrderSection( config = config, token = token, stockCode = code, currentPrice = currentPrice, onOrderResult = { msg, success -> resultMessage = msg isSuccess = success } ) } }