This commit is contained in:
2026-01-14 15:42:26 +09:00
parent 2bb94e2856
commit bdc268e325
18 changed files with 1193 additions and 205 deletions
+116 -29
View File
@@ -22,6 +22,7 @@ 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.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import model.AppConfig
@@ -41,10 +42,29 @@ fun StockDetailSection(
tradeService: KisTradeService,
wsManager: KisWebSocketManager
) {
var openPrice by remember { mutableStateOf("0") }
var chartData by remember { mutableStateOf<List<CandleData>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
var resultMessage by remember { mutableStateOf("") }
var isSuccess by remember { mutableStateOf(true) }
var daySummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
var weekSummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
var monthSummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
var yearSummary by remember { mutableStateOf<List<CandleData>>(emptyList()) }
val todayOpen = remember(daySummary) {
daySummary.lastOrNull()?.stck_oprc ?: "0"
}
val previousClose = remember(daySummary) {
if (daySummary.size >= 2) daySummary[daySummary.size - 2].stck_clpr else "0"
}
fun calculateAvg(data: List<CandleData>): String {
if (data.isEmpty()) return "0"
val avg = data.map { it.stck_clpr.toDoubleOrNull() ?: 0.0 }.average()
return String.format("%,d", avg.toLong())
}
// 이전 종목 코드를 기억하기 위한 상태
var previousCode by remember { mutableStateOf("") }
@@ -59,49 +79,103 @@ fun StockDetailSection(
if (previousCode.isNotEmpty()) {
wsManager.unsubscribeStock(previousCode)
}
wsManager.clearData()
wsManager.subscribeStock(stockCode)
previousCode = stockCode
// 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
tradeService.fetchChartData(stockCode, isDomestic)
.onSuccess { data ->
println("✅ 차트 데이터 로드 성공: ${data.size}") // ${} 사용하여 정확히 출력
chartData = data
}
.onFailure { error ->
println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
chartData = emptyList()
}
coroutineScope {
launch {tradeService.fetchChartData(stockCode, isDomestic)
.onSuccess { data ->
println("✅ 차트 데이터 로드 성공: ${data.size}") // ${} 사용하여 정확히 출력
chartData = data
}
.onFailure { error ->
println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
chartData = emptyList()
}}
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess { daySummary = it.takeLast(7) } } // 최근 7일
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess { weekSummary = it.takeLast(4) } } // 최근 4주
launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
monthSummary = it.takeLast(6) // 최근 6개월
yearSummary = it.takeLast(36) // 최근 3년
} }
}
isLoading = false
}
val latestPrice by wsManager.currentPrice // 웹소켓에서 업데이트되는 현재가
LaunchedEffect(latestPrice) {
println("latestPrice $latestPrice")
if (chartData.isNotEmpty() && latestPrice != "0") {
// 마지막 캔들 정보 업데이트
val priceDouble = latestPrice.replace(",", "").toDoubleOrNull() ?: return@LaunchedEffect
val lastCandle = chartData.last()
val updatedCandle = lastCandle.copy(
stck_clpr = 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
)
// 현재 시간(분 단위) 확인
val currentMinute = java.time.LocalTime.now().format(java.time.format.DateTimeFormatter.ofPattern("HHmm00"))
chartData = chartData.dropLast(1) + updatedCandle
println("chartData.size $chartData.size")
if (lastCandle.stck_bsop_date != currentMinute) {
// [개선] 시간이 바뀌었으면 새로운 캔들 추가 (차트가 밀려나는 효과)
val newCandle = CandleData(
stck_bsop_date = currentMinute,
stck_oprc = latestPrice,
stck_hgpr = latestPrice,
stck_lwpr = latestPrice,
stck_clpr = latestPrice,
acml_vol = "0"
)
// 최대 100개까지만 유지하여 성능 최적화
chartData = (chartData + newCandle).takeLast(100)
} else {
// 같은 분 내에서는 기존 마지막 캔들만 업데이트
val updatedCandle = lastCandle.copy(
stck_clpr = 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)) {
// [상단] 종목명 및 상태 메시지
StockHeader(stockName, stockCode, isDomestic, resultMessage, isSuccess)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
StockHeader(
name = stockName,
code = stockCode,
isDomestic = isDomestic,
previousClose = previousClose,
openPrice = openPrice,
resultMessage = 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(10.dp))
// [중앙] 캔들 차트 (Card 내부)
Card(
modifier = Modifier.fillMaxWidth().height(300.dp),
@@ -136,14 +210,27 @@ fun StockDetailSection(
Spacer(modifier = Modifier.width(12.dp))
// 주문 섹션 (인자 간소화)
OrderSection(
stockCode = stockCode,
currentPrice = wsManager.currentPrice.value,
onOrderResult = { msg, success ->
resultMessage = msg
isSuccess = success
}
)
Column(modifier = Modifier.weight(0.6f)) {
IntegratedOrderSection(
stockCode = stockCode,
currentPrice = wsManager.currentPrice.value,
tradeService = tradeService,
onOrderResult = { msg, success ->
resultMessage = msg
isSuccess = success
}
)
}
}
}
}
@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)
}
}
}