// src/main/kotlin/ui/IntegratedOrderSection.kt package ui import AutoTradeItem import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* 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.launch import network.KisTradeService /** * 통합 주문 및 자동매매 설정 섹션 * * [수정 사항] * 1. stockCode 기반이 아닌 DB 객체(monitoringItem) 기반의 상태 관리로 데이터 꼬임 방지 * 2. 수량 입력 시 콤마 제거 및 Int 변환 예외 처리 적용 * 3. 매수 성공 시 반환받은 실제 주문번호(ODNO)를 DB에 저장하여 주문번호 중심 관리 구현 */ @Composable fun IntegratedOrderSection( stockCode: String, stockName: String, isDomestic: Boolean, currentPrice: String, holdingQuantity: String, tradeService: KisTradeService, onOrderResult: (String, Boolean) -> Unit ) { val scope = rememberCoroutineScope() // 1. 상태 관리: 현재 종목의 감시 설정 여부를 DB에서 로드하여 객체 단위로 관리 var monitoringItem by remember(stockCode) { mutableStateOf(DatabaseFactory.findConfigByCode(stockCode)) } val isAutoSellEnabled = monitoringItem != null // UI 입력 상태 var orderPrice by remember { mutableStateOf("") } // 빈 값이면 시장가 var orderQty by remember(holdingQuantity) { // 보유수량이 있으면 해당 수량, 없으면 기본 1주 (콤마 제거 처리) val cleanQty = holdingQuantity.replace(",", "") mutableStateOf(if(cleanQty == "0" || cleanQty.isEmpty()) "1" else cleanQty) } var profitRate by remember(monitoringItem) { mutableStateOf(monitoringItem?.profitRate?.toString() ?: "3.0") } var stopLossRate by remember(monitoringItem) { mutableStateOf(monitoringItem?.stopLossRate?.toString() ?: "-2.0") } // 계산용 변수 val curPriceNum = currentPrice.replace(",", "").toDoubleOrNull() ?: 0.0 val basePrice = (if (orderPrice.isEmpty()) curPriceNum else orderPrice.toDoubleOrNull() ?: 0.0) val inputQty = orderQty.replace(",", "").toIntOrNull() ?: 0 Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { Text("주문 및 자동 매도 설정", style = MaterialTheme.typography.subtitle2, fontWeight = FontWeight.Bold) // 가격 및 수량 입력 필드 Row(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) { OutlinedTextField( value = orderQty, onValueChange = { if (it.all { c -> c.isDigit() }) orderQty = it }, label = { Text("수량") }, modifier = Modifier.weight(1f).padding(end = 4.dp) ) OutlinedTextField( value = orderPrice, onValueChange = { if (it.all { c -> c.isDigit() }) orderPrice = it }, label = { Text("가격") }, placeholder = { Text("시장가 (${currentPrice})") }, modifier = Modifier.weight(1f) ) } // 수익률 시뮬레이션 표 if (basePrice > 0 && inputQty > 0) { SimulationCard(basePrice, inputQty.toDouble()) } Spacer(modifier = Modifier.height(12.dp)) // 실시간 AI 매도 감시 설정 카드 Card(backgroundColor = Color(0xFFF8F9FA), elevation = 0.dp) { Column(modifier = Modifier.padding(8.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( checked = isAutoSellEnabled, onCheckedChange = { checked -> if (!checked) { // [감시 해제] DB ID를 사용하여 정확한 항목 삭제 (데이터 꼬임 방지) monitoringItem?.id?.let { dbId -> DatabaseFactory.deleteAutoTrade(dbId) monitoringItem = null println("🗑️ 감시 해제: $stockName (ID: $dbId)") } } else { // [즉시 감시 등록] 보유 종목에 대해 가상의 주문번호로 감시 시작 if (curPriceNum > 0) { val pRate = profitRate.toDoubleOrNull() ?: 0.0 val sRate = stopLossRate.toDoubleOrNull() ?: 0.0 val target = curPriceNum * (1 + pRate / 100.0) val stop = curPriceNum * (1 + sRate / 100.0) val newItem = AutoTradeItem( orderNo = "EXISTING_${stockCode}_${System.currentTimeMillis()}", code = stockCode, name = stockName, quantity = inputQty, profitRate = pRate, stopLossRate = sRate, targetPrice = target, stopLossPrice = stop, status = "MONITORING", isDomestic = isDomestic ) DatabaseFactory.saveAutoTrade(newItem) monitoringItem = DatabaseFactory.findConfigByCode(stockCode) } } } ) Text("실시간 AI 매도 감시 활성화", fontSize = 12.sp, fontWeight = FontWeight.Bold) } Row { OutlinedTextField( value = profitRate, onValueChange = { profitRate = it }, label = { Text("익절 %") }, modifier = Modifier.weight(1f).padding(end = 4.dp), enabled = !isAutoSellEnabled ) OutlinedTextField( value = stopLossRate, onValueChange = { stopLossRate = it }, label = { Text("손절 %") }, modifier = Modifier.weight(1f), enabled = !isAutoSellEnabled ) } } } Spacer(modifier = Modifier.height(12.dp)) // 매수 / 매도 실행 버튼 Row(modifier = Modifier.fillMaxWidth()) { // 매수 버튼 Button( onClick = { scope.launch { val finalPrice = if (orderPrice.isBlank()) "0" else orderPrice tradeService.postOrder(stockCode, orderQty, finalPrice, isBuy = true) .onSuccess { realOrderNo -> // KIS 서버에서 생성된 실제 주문번호 onOrderResult("주문 성공: $realOrderNo", true) if (isAutoSellEnabled) { val pRate = profitRate.toDoubleOrNull() ?: 0.0 val sRate = stopLossRate.toDoubleOrNull() ?: 0.0 DatabaseFactory.saveAutoTrade(AutoTradeItem( orderNo = realOrderNo, // 실제 주문번호 저장 (중심 관리 원칙) code = stockCode, name = stockName, quantity = inputQty, profitRate = pRate, stopLossRate = sRate, targetPrice = basePrice * (1 + pRate / 100.0), stopLossPrice = basePrice * (1 + sRate / 100.0), status = "PENDING_BUY", // 체결 전까지 PENDING_BUY 상태 isDomestic = isDomestic )) monitoringItem = DatabaseFactory.findConfigByCode(stockCode) } } .onFailure { onOrderResult(it.message ?: "매수 실패", false) } } }, modifier = Modifier.weight(1f).padding(end = 4.dp), colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFFE03E2D)) ) { Text("매수", color = Color.White) } // 매도 버튼 Button( onClick = { scope.launch { val finalPrice = if (orderPrice.isBlank()) "0" else orderPrice tradeService.postOrder(stockCode, orderQty, finalPrice, isBuy = false) .onSuccess { realOrderNo -> onOrderResult("매도 주문 성공: $realOrderNo", true) // 매도 시 기존 감시 설정이 있다면 상태 변경 등 추가 로직 가능 } .onFailure { onOrderResult(it.message ?: "매도 실패", false) } } }, modifier = Modifier.weight(1f), colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFF0E62CF)) ) { Text("매도", color = Color.White) } } } } @Composable fun SimulationCard(basePrice: Double, qty: Double) { Card(backgroundColor = Color(0xFFF1F3F5), shape = RoundedCornerShape(4.dp), elevation = 0.dp) { Column(modifier = Modifier.padding(8.dp)) { Text("수익률 시뮬레이션 (수수료/세금 약 0.22% 반영)", fontSize = 10.sp, color = Color.Gray) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { SimulationColumn("수익률", listOf("+5%", "+3%", "+1%", "-1%", "-3%", "-5%")) SimulationColumn("목표가", listOf(1.05, 1.03, 1.01, 0.99, 0.97, 0.95).map { (basePrice * it).toLong().toString() }) SimulationColumn("예상수령", listOf(1.05, 1.03, 1.01, 0.99, 0.97, 0.95).map { rate -> val netAmount = (basePrice * rate * qty) * (1 - 0.0022) String.format("%,d", netAmount.toLong()) }) } } } } @Composable fun SimulationColumn(title: String, items: List) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(title, fontSize = 10.sp, fontWeight = FontWeight.Bold, color = Color.DarkGray) items.forEach { text -> val color = when { text.contains("+") -> Color(0xFFE03E2D) text.contains("-") -> Color(0xFF0E62CF) else -> Color.Black } Text(text = text, fontSize = 11.sp, color = color, modifier = Modifier.padding(vertical = 1.dp)) } } }