...
This commit is contained in:
@@ -34,149 +34,116 @@ import network.KisWebSocketManager
|
||||
import kotlin.collections.isNotEmpty
|
||||
|
||||
@Composable
|
||||
fun StockDetailArea(
|
||||
config: AppConfig,
|
||||
token: String,
|
||||
code: String,
|
||||
name: String,
|
||||
wsManager: KisWebSocketManager // 매니저 수신
|
||||
fun StockDetailSection(
|
||||
stockCode: String,
|
||||
stockName: String,
|
||||
isDomestic: Boolean,
|
||||
tradeService: KisTradeService,
|
||||
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<List<CandleData>>(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
|
||||
// 이전 종목 코드를 기억하기 위한 상태
|
||||
var previousCode by remember { mutableStateOf("") }
|
||||
|
||||
// 종목 변경 시 데이터 로드 및 웹소켓 구독 관리
|
||||
LaunchedEffect(stockCode) {
|
||||
if (stockCode.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)
|
||||
// 1. 웹소켓 구독 관리: 이전 종목 해제 -> 새 종목 구독
|
||||
if (previousCode.isNotEmpty()) {
|
||||
wsManager.unsubscribeStock(previousCode)
|
||||
}
|
||||
wsManager.subscribeStock(stockCode)
|
||||
previousCode = stockCode
|
||||
|
||||
result.onSuccess { chartData = it }
|
||||
.onFailure { println("차트 로드 실패: ${it.message}") }
|
||||
// 2. 차트 데이터 로드 (KisSession 기반으로 파라미터 간소화)
|
||||
tradeService.fetchChartData(stockCode, isDomestic)
|
||||
.onSuccess { data ->
|
||||
println("✅ 차트 데이터 로드 성공: ${data.size}개") // ${} 사용하여 정확히 출력
|
||||
chartData = data
|
||||
}
|
||||
.onFailure { error ->
|
||||
println("❌ 차트 데이터 로드 실패: ${error.localizedMessage}")
|
||||
chartData = emptyList()
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
LaunchedEffect(resultMessage) {
|
||||
if (resultMessage.isNotEmpty()) {
|
||||
delay(3000)
|
||||
resultMessage = ""
|
||||
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
|
||||
)
|
||||
|
||||
chartData = chartData.dropLast(1) + updatedCandle
|
||||
println("chartData.size $chartData.size")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
// [상단] 종목명 및 상태 메시지
|
||||
StockHeader(stockName, stockCode, isDomestic, resultMessage, isSuccess)
|
||||
|
||||
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 내부)
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().height(350.dp),
|
||||
modifier = Modifier.fillMaxWidth().height(300.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) }
|
||||
CandleChart(data = chartData, modifier = Modifier.padding(16.dp))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// [중앙 하단] AI 투자 전략
|
||||
AiAnalysisView(
|
||||
stockName = name,
|
||||
stockName = stockName,
|
||||
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)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// [하단] 실시간 체결 내역 및 주문 섹션
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
// 실시간 체결 리스트
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text("실시간 체결", style = MaterialTheme.typography.subtitle2, fontWeight = FontWeight.Bold)
|
||||
RealTimeTradeList(wsManager.tradeLogs)
|
||||
}
|
||||
|
||||
// 실시간 리스트
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(tradeLogs) { trade ->
|
||||
TradeLogRow(trade)
|
||||
Divider(color = Color(0xFFF5F5F5))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
// 주문 섹션 (인자 간소화)
|
||||
OrderSection(
|
||||
stockCode = stockCode,
|
||||
currentPrice = wsManager.currentPrice.value,
|
||||
onOrderResult = { msg, success ->
|
||||
resultMessage = msg
|
||||
isSuccess = success
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
OrderSection(
|
||||
config = config,
|
||||
token = token,
|
||||
stockCode = code,
|
||||
currentPrice = currentPrice,
|
||||
onOrderResult = { msg, success ->
|
||||
resultMessage = msg
|
||||
isSuccess = success
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user