atrade/src/main/kotlin/ui/StockDetailArea.kt

182 lines
7.3 KiB
Kotlin
Raw Normal View History

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
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<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
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
}
)
}
}