182 lines
7.3 KiB
Kotlin
182 lines
7.3 KiB
Kotlin
|
|
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
|
||
|
|
}
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
}
|