// src/main/kotlin/ui/IntegratedOrderSection.kt package ui import AutoTradeItem import TradingDecision import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField 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.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch import network.KisTradeService import util.MarketUtil /** * 통합 주문 및 자동매매 설정 섹션 * * [수정 사항] * 1. stockCode 기반이 아닌 DB 객체(monitoringItem) 기반의 상태 관리로 데이터 꼬임 방지 * 2. 수량 입력 시 콤마 제거 및 Int 변환 예외 처리 적용 * 3. 매수 성공 시 반환받은 실제 주문번호(ODNO)를 DB에 저장하여 주문번호 중심 관리 구현 */ @Composable fun IntegratedOrderSection( stockCode: String, stockName: String, isDomestic: Boolean, currentPrice: String, holdingQuantity: String, tradeService: KisTradeService, onOrderSaved: (String) -> Unit, onOrderResult: (String, Boolean) -> Unit, completeTradingDecision: TradingDecision? ) { val scope = rememberCoroutineScope() // 1. 상태 관리: 현재 종목의 감시 설정 여부를 DB에서 로드하여 객체 단위로 관리 var monitoringItem by remember(stockCode) { mutableStateOf(DatabaseFactory.findConfigByCode(stockCode)) } var activeMonitoringItem by remember(stockCode) { mutableStateOf(DatabaseFactory.findConfigByCode(stockCode)) } // 2. 체크박스의 '의도' 상태 (신규 매수 시 자동감시를 켤 것인지 여부) // 감시 중인 아이템이 있으면 true, 없으면 사용자 선택에 따름 var willEnableAutoSell by remember(stockCode) { mutableStateOf(activeMonitoringItem != 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 fun excuteTrade(willEnableAutoSell: Boolean,orderQty: String) { scope.launch { val finalPrice = if (orderPrice.isBlank()) "0" else orderPrice tradeService.postOrder(stockCode, orderQty, finalPrice, isBuy = true) .onSuccess { realOrderNo -> // KIS 서버에서 생성된 실제 주문번호 onOrderResult("주문 성공: $realOrderNo", true) if (willEnableAutoSell) { val pRate = profitRate.toDoubleOrNull() ?: 0.0 val sRate = stopLossRate.toDoubleOrNull() ?: 0.0 val calculatedTarget = MarketUtil.roundToTickSize(basePrice * (1 + pRate / 100.0)) val calculatedStop = MarketUtil.roundToTickSize(basePrice * (1 + sRate / 100.0)) DatabaseFactory.saveAutoTrade(AutoTradeItem( orderNo = realOrderNo, // 실제 주문번호 저장 (중심 관리 원칙) code = stockCode, name = stockName, quantity = inputQty, profitRate = pRate, stopLossRate = sRate, targetPrice = calculatedTarget, stopLossPrice = calculatedStop, status = "PENDING_BUY", // 체결 전까지 PENDING_BUY 상태 isDomestic = isDomestic )) monitoringItem = DatabaseFactory.findConfigByCode(stockCode) onOrderSaved(realOrderNo) onOrderResult("매수 및 즉시 체결 확인: $realOrderNo", true) } } .onFailure { onOrderResult(it.message ?: "매수 실패", false) } } } LaunchedEffect(completeTradingDecision) { if (completeTradingDecision != null && completeTradingDecision.stockCode.equals(stockCode)) { when (completeTradingDecision?.decision) { "BUY" -> if (completeTradingDecision.confidence > 70) excuteTrade(true, "1") "SELL" -> println("[$stockCode] 매도: ${completeTradingDecision?.reason}") else -> println("[$stockCode] 관망 유지: ${completeTradingDecision?.reason}") } } } Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { Text("주문 및 자동 매도 설정", style = MaterialTheme.typography.subtitle2, fontWeight = FontWeight.Bold) // 가격 및 수량 입력 필드 Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { AutoResizeOutlinedTextField( value = orderQty, onValueChange = { if (it.all { c -> c.isDigit() }) orderQty = it }, label = { Text("수량") }, modifier = Modifier.weight(1f) ) AutoResizeOutlinedTextField( 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(4.dp)) // 실시간 AI 매도 감시 설정 카드 Card(backgroundColor = Color(0xFFF8F9FA), elevation = 0.dp) { Column(modifier = Modifier.padding(4.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( checked = willEnableAutoSell, onCheckedChange = { checked -> willEnableAutoSell = 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 { AutoResizeOutlinedTextField( value = profitRate, onValueChange = { profitRate = it }, label = { Text("익절 %") }, modifier = Modifier.weight(1f).padding(end = 4.dp), ) AutoResizeOutlinedTextField( value = stopLossRate, onValueChange = { stopLossRate = it }, label = { Text("손절 %") }, modifier = Modifier.weight(1f), ) } } } Spacer(modifier = Modifier.height(4.dp)) // 매수 / 매도 실행 버튼 Row(modifier = Modifier.fillMaxWidth()) { // 매수 버튼 Button( onClick = { excuteTrade(willEnableAutoSell,orderQty) }, 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)) } } } @OptIn(ExperimentalMaterialApi::class) @Composable fun AutoResizeOutlinedTextField( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null, // 라벨 추가 placeholder: @Composable (() -> Unit)? = null, // 플레이스홀더 추가 maxFontSize: TextUnit = 20.sp, minFontSize: TextUnit = 8.sp ) { val textMeasurer = rememberTextMeasurer() var fontSize by remember { mutableStateOf(maxFontSize) } val interactionSource = remember { MutableInteractionSource() } BoxWithConstraints(modifier = modifier) { val maxWidthPx = constraints.maxWidth // 텍스트 너비에 따른 폰트 크기 자동 축소 로직 LaunchedEffect(value) { var currentSize = maxFontSize while (currentSize > minFontSize) { val layoutResult = textMeasurer.measure( text = value, style = TextStyle(fontSize = currentSize) ) if (layoutResult.size.width <= maxWidthPx) break currentSize = (currentSize.value - 0.5f).sp } fontSize = currentSize } BasicTextField( value = value, onValueChange = onValueChange, textStyle = TextStyle(fontSize = fontSize, color = Color.Black), modifier = Modifier.fillMaxWidth(), interactionSource = interactionSource, singleLine = true, decorationBox = { innerTextField -> TextFieldDefaults.OutlinedTextFieldDecorationBox( value = value, innerTextField = innerTextField, enabled = true, singleLine = true, visualTransformation = VisualTransformation.None, interactionSource = interactionSource, // [핵심] 사용자가 정의한 라벨과 플레이스홀더 연결 label = label, placeholder = placeholder, // [핵심] 내부 패딩 0.dp 설정 contentPadding = PaddingValues(0.dp), border = { TextFieldDefaults.BorderBox( enabled = true, isError = false, interactionSource = interactionSource, colors = TextFieldDefaults.outlinedTextFieldColors() ) } ) } ) } }