2026-01-10 18:16:50 +09:00
|
|
|
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
|
2026-01-13 16:04:25 +09:00
|
|
|
fun StockDetailSection(
|
|
|
|
|
stockCode: String,
|
|
|
|
|
stockName: String,
|
|
|
|
|
isDomestic: Boolean,
|
|
|
|
|
tradeService: KisTradeService,
|
|
|
|
|
wsManager: KisWebSocketManager
|
2026-01-10 18:16:50 +09:00
|
|
|
) {
|
|
|
|
|
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) }
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
// 이전 종목 코드를 기억하기 위한 상태
|
|
|
|
|
var previousCode by remember { mutableStateOf("") }
|
|
|
|
|
|
|
|
|
|
// 종목 변경 시 데이터 로드 및 웹소켓 구독 관리
|
|
|
|
|
LaunchedEffect(stockCode) {
|
|
|
|
|
if (stockCode.isEmpty()) return@LaunchedEffect
|
2026-01-10 18:16:50 +09:00
|
|
|
|
|
|
|
|
isLoading = true
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
// 1. 웹소켓 구독 관리: 이전 종목 해제 -> 새 종목 구독
|
|
|
|
|
if (previousCode.isNotEmpty()) {
|
|
|
|
|
wsManager.unsubscribeStock(previousCode)
|
|
|
|
|
}
|
|
|
|
|
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()
|
|
|
|
|
}
|
2026-01-10 18:16:50 +09:00
|
|
|
|
|
|
|
|
isLoading = false
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
val latestPrice by wsManager.currentPrice // 웹소켓에서 업데이트되는 현재가
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
LaunchedEffect(latestPrice) {
|
|
|
|
|
println("latestPrice $latestPrice")
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
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")
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
2026-01-13 16:04:25 +09:00
|
|
|
}
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
|
|
|
|
// [상단] 종목명 및 상태 메시지
|
|
|
|
|
StockHeader(stockName, stockCode, isDomestic, resultMessage, isSuccess)
|
2026-01-10 18:16:50 +09:00
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
// [중앙] 캔들 차트 (Card 내부)
|
2026-01-10 18:16:50 +09:00
|
|
|
Card(
|
2026-01-13 16:04:25 +09:00
|
|
|
modifier = Modifier.fillMaxWidth().height(300.dp),
|
2026-01-10 18:16:50 +09:00
|
|
|
backgroundColor = Color(0xFF121212)
|
|
|
|
|
) {
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
Box(contentAlignment = Alignment.Center) { CircularProgressIndicator(color = Color.White) }
|
|
|
|
|
} else {
|
2026-01-13 16:04:25 +09:00
|
|
|
CandleChart(data = chartData, modifier = Modifier.padding(16.dp))
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-13 16:04:25 +09:00
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
|
|
|
|
|
|
|
|
// [중앙 하단] AI 투자 전략
|
2026-01-10 18:16:50 +09:00
|
|
|
AiAnalysisView(
|
2026-01-13 16:04:25 +09:00
|
|
|
stockName = stockName,
|
2026-01-10 18:16:50 +09:00
|
|
|
currentPrice = wsManager.currentPrice.value,
|
|
|
|
|
trades = wsManager.tradeLogs
|
|
|
|
|
)
|
2026-01-13 16:04:25 +09:00
|
|
|
|
|
|
|
|
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)
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-13 16:04:25 +09:00
|
|
|
Spacer(modifier = Modifier.width(12.dp))
|
|
|
|
|
|
|
|
|
|
// 주문 섹션 (인자 간소화)
|
|
|
|
|
OrderSection(
|
|
|
|
|
stockCode = stockCode,
|
|
|
|
|
currentPrice = wsManager.currentPrice.value,
|
|
|
|
|
onOrderResult = { msg, success ->
|
|
|
|
|
resultMessage = msg
|
|
|
|
|
isSuccess = success
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
2026-01-13 16:04:25 +09:00
|
|
|
)
|
2026-01-10 18:16:50 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|