빨라져라

This commit is contained in:
2026-04-07 17:32:21 +09:00
parent a700d54dfe
commit 9172cca791
24 changed files with 3337 additions and 3253 deletions
+294 -294
View File
@@ -1,302 +1,302 @@
package ui
import network.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 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?,
min30 : MutableList<CandleData>,
daySummary : MutableList<CandleData>,
weekSummary : MutableList<CandleData>,
monthSummary : MutableList<CandleData>,
yearSummary : MutableList<CandleData>
) {
// 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) }
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("") }
var lastPrice by remember { mutableStateOf("0") }
// 종목 변경 시 데이터 로드 및 웹소켓 구독 관리
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 {
launch {
wsManager.onPriceUpdate = {tradeLog ->
if (tradeLog.code.equals(stockCode)) {
val code = tradeLog.code
val price = tradeLog.price
wsManager.tradeLogs.add(tradeLog)
if (wsManager.tradeLogs.size > 50) wsManager.tradeLogs.removeLast()
// println("code $code ,price $price")
val currentPrice = price
if (chartData.isNotEmpty() && currentPrice != "0") {
val priceDouble = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0
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 = currentPrice,
stck_hgpr = currentPrice,
stck_lwpr = currentPrice,
stck_prpr = currentPrice,
stck_cntg_hour = currentMinute,
cntg_vol = "1",
acml_tr_pbmn = "1",
)
// 최대 100개까지만 유지하여 성능 최적화
chartData = (chartData + newCandle).takeLast(100)
} else {
// 같은 분 내에서는 기존 마지막 캔들만 업데이트
val updatedCandle = lastCandle.copy(
stck_prpr = currentPrice,
stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) currentPrice else lastCandle.stck_hgpr,
stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) currentPrice else lastCandle.stck_lwpr
)
chartData = chartData.dropLast(1) + updatedCandle
}
}
lastPrice = currentPrice
}
}
}
launch {tradeService.fetchChartData(stockCode, isDomestic)
.onSuccess { data ->
// println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
chartData = data
min30.clear()
min30.addAll(chartData)
}
.onFailure { error ->
// println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
chartData = emptyList()
}
}
launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
daySummary.clear()
daySummary.addAll(it)
}
} // 최근 7일
launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess {
weekSummary.clear()
weekSummary.addAll(it.takeLast(4))
// println("weekSummary ${weekSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
}
} // 최근 4주
launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
monthSummary.clear()
monthSummary.addAll(it.takeLast(6))
yearSummary.clear()
yearSummary.addAll(it.takeLast(36))
}
}
launch {
DartCodeManager.getCorpCode(stockCode)?.let {
it.stockName = stockName
// NewsService.fetchAndIngestNews(it)
}
}
}
isLoading = false
}
// LaunchedEffect(latestPrice) {
// println("latestPrice >>> $latestPrice")
// if (chartData.isNotEmpty() && latestPrice != "0") {
// val latestPrice = latestPrice ?: "0"
// val priceDouble = latestPrice?.replace(",", "")?.toDoubleOrNull() ?: return@LaunchedEffect
// val lastCandle = chartData.last()
//package ui
//
// // 현재 시간(분 단위) 확인
// 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",
//
//import network.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 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?,
// min30 : MutableList<CandleData>,
// daySummary : MutableList<CandleData>,
// weekSummary : MutableList<CandleData>,
// monthSummary : MutableList<CandleData>,
// yearSummary : MutableList<CandleData>
//) {
//
//// 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) }
//
//
// 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("") }
// var lastPrice by remember { mutableStateOf("0") }
//
//
// // 종목 변경 시 데이터 로드 및 웹소켓 구독 관리
// 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 {
// launch {
// wsManager.onPriceUpdate = {tradeLog ->
//
//
// if (tradeLog.code.equals(stockCode)) {
// val code = tradeLog.code
// val price = tradeLog.price
// wsManager.tradeLogs.add(tradeLog)
// if (wsManager.tradeLogs.size > 50) wsManager.tradeLogs.removeLast()
//// println("code $code ,price $price")
// val currentPrice = price
// if (chartData.isNotEmpty() && currentPrice != "0") {
// val priceDouble = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0
// 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 = currentPrice,
// stck_hgpr = currentPrice,
// stck_lwpr = currentPrice,
// stck_prpr = currentPrice,
// stck_cntg_hour = currentMinute,
// cntg_vol = "1",
// acml_tr_pbmn = "1",
// )
// // 최대 100개까지만 유지하여 성능 최적화
// chartData = (chartData + newCandle).takeLast(100)
// } else {
// // 같은 분 내에서는 기존 마지막 캔들만 업데이트
// val updatedCandle = lastCandle.copy(
// stck_prpr = currentPrice,
// stck_hgpr = if (priceDouble > (lastCandle.stck_hgpr.toDoubleOrNull() ?: 0.0)) currentPrice else lastCandle.stck_hgpr,
// stck_lwpr = if (priceDouble < (lastCandle.stck_lwpr.toDoubleOrNull() ?: Double.MAX_VALUE)) currentPrice else lastCandle.stck_lwpr
// )
// chartData = chartData.dropLast(1) + updatedCandle
// }
// }
// lastPrice = currentPrice
// }
//
// }
// }
// launch {tradeService.fetchChartData(stockCode, isDomestic)
// .onSuccess { data ->
//// println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
// chartData = data
// min30.clear()
// min30.addAll(chartData)
// }
// .onFailure { error ->
//// println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
// chartData = emptyList()
// }
// }
// launch { tradeService.fetchPeriodChartData(stockCode, "D").onSuccess {
// daySummary.clear()
// daySummary.addAll(it)
// }
// } // 최근 7일
// launch { tradeService.fetchPeriodChartData(stockCode, "W").onSuccess {
// weekSummary.clear()
// weekSummary.addAll(it.takeLast(4))
//// println("weekSummary ${weekSummary.size} total: ${it.size} ${it.firstOrNull()?.toString()}")
// }
// } // 최근 4주
// launch { tradeService.fetchPeriodChartData(stockCode, "M").onSuccess {
// monthSummary.clear()
// monthSummary.addAll(it.takeLast(6))
// yearSummary.clear()
// yearSummary.addAll(it.takeLast(36))
// }
// }
// launch {
// DartCodeManager.getCorpCode(stockCode)?.let {
// it.stockName = stockName
//// NewsService.fetchAndIngestNews(it)
// }
// }
// }
// isLoading = false
// }
//
//
//
//// LaunchedEffect(latestPrice) {
//// println("latestPrice >>> $latestPrice")
//// if (chartData.isNotEmpty() && latestPrice != "0") {
//// val latestPrice = 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 = lastPrice,
// resultMessage = resultMessage,
// resultMessageClear = {resultMessage = ""},
// isSuccess = isSuccess
// )
//
// // 실시간 가격 표시 (WebSocket 데이터)
// Column(horizontalAlignment = Alignment.End) {
// Text(
// text = "${lastPrice} 원",
// style = MaterialTheme.typography.h4,
// fontWeight = FontWeight.Bold,
// color = if (lastPrice?.contains("-") ?: false) Color.Blue else Color.Red
// )
// // 최대 100개까지만 유지하여 성능 최적화
// chartData = (chartData + newCandle).takeLast(100)
// 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 {
// // 같은 분 내에서는 기존 마지막 캔들만 업데이트
// 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
// 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 = lastPrice,
// holdingQuantity = holdingQuantity,
// tradeService = tradeService,
// onOrderSaved = onOrderSaved,
// onOrderResult = { msg, success ->
// resultMessage = msg
// isSuccess = success
// },
// completeTradingDecision
// )
// 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 = lastPrice,
resultMessage = resultMessage,
resultMessageClear = {resultMessage = ""},
isSuccess = isSuccess
)
// 실시간 가격 표시 (WebSocket 데이터)
Column(horizontalAlignment = Alignment.End) {
Text(
text = "${lastPrice}",
style = MaterialTheme.typography.h4,
fontWeight = FontWeight.Bold,
color = if (lastPrice?.contains("-") ?: false) 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 = lastPrice,
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)
}
}
}
//}
//
//@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)
// }
// }
//}