import React, { useState, useEffect, useMemo } from 'react'; import { X, RotateCcw, ChevronDown, Zap, Plus, Minus, Calendar, ToggleLeft, ToggleRight, CheckSquare, Square, TrendingUp, TrendingDown, Wallet, Target, ShieldAlert, BadgePercent, Save, PlayCircle, Info, BarChart3, Maximize2, Circle, CheckCircle2 } from 'lucide-react'; import { StockItem, OrderType, MarketType, ReservedOrder, TradeOrder } from '../types'; import { DbService, HoldingItem } from '../services/dbService'; interface TradeModalProps { stock: StockItem; type: OrderType; onClose: () => void; onExecute: (order: Omit) => Promise; onImmediateOrder?: (order: Omit) => Promise; } type StrategyType = 'NONE' | 'PROFIT' | 'LOSS' | 'TRAILING_STOP'; const TradeModal: React.FC = ({ stock, type: initialType, onClose, onExecute, onImmediateOrder }) => { const [orderType, setOrderType] = useState(initialType); const isBuyMode = orderType === OrderType.BUY; const [holding, setHolding] = useState(null); const [buyingPower, setBuyingPower] = useState(0); const dbService = useMemo(() => new DbService(), []); useEffect(() => { const fetchData = async () => { const holdings = await dbService.getHoldings(); const currentHolding = holdings.find(h => h.code === stock.code) || null; setHolding(currentHolding); const summary = await dbService.getAccountSummary(); setBuyingPower(summary.buyingPower); }; fetchData(); }, [stock.code, dbService, orderType]); const plInfo = useMemo(() => { if (!holding) return null; const pl = (stock.price - holding.avgPrice) * holding.quantity; const percent = ((stock.price - holding.avgPrice) / holding.avgPrice) * 100; return { pl, percent }; }, [holding, stock.price]); const [monitoringEnabled, setMonitoringEnabled] = useState(false); const [immediateStart, setImmediateStart] = useState(true); // 전략 선택 (라디오 버튼 방식) const [activeStrategy, setActiveStrategy] = useState(isBuyMode ? 'TRAILING_STOP' : 'NONE'); // TS 설정 const [tsValue, setTsValue] = useState(3); const [tsUnit, setTsUnit] = useState<'PERCENT' | 'TICK' | 'AMOUNT'>('PERCENT'); // TS 내부 감시 시작 조건 (Trigger) const [triggerEnabled, setTriggerEnabled] = useState(false); const [triggerType, setTriggerType] = useState<'CURRENT' | 'HIGH' | 'LOW' | 'VOLUME'>('CURRENT'); const [triggerValue, setTriggerValue] = useState(stock.price); const [monCondition, setMonCondition] = useState<'ABOVE' | 'BELOW'>(isBuyMode ? 'BELOW' : 'ABOVE'); // 자동 조건 결정 로직 const applyAutoCondition = (type: string, buyMode: boolean) => { if (!buyMode) { setMonCondition('ABOVE'); // 매도시에는 현재/고/저/거래량 모두 이상 } else { // 매수시 if (type === 'CURRENT' || type === 'HIGH') { setMonCondition('BELOW'); // 현재가, 고가는 이하 } else { setMonCondition('ABOVE'); // 저가, 거래량은 이상 } } }; // 모드 변경 시 전략 및 조건 초기화 useEffect(() => { if (isBuyMode) { setActiveStrategy('TRAILING_STOP'); } else { setActiveStrategy('NONE'); } applyAutoCondition(triggerType, isBuyMode); if (triggerType !== 'VOLUME') { setTriggerValue(stock.price); } }, [isBuyMode, stock.price]); const [monTargetUnit, setMonTargetUnit] = useState<'PRICE' | 'PERCENT' | 'AMOUNT'>('PRICE'); const [profitValue, setProfitValue] = useState(stock.price * 1.1); const [lossValue, setLossValue] = useState(stock.price * 0.9); const [expiryDays, setExpiryDays] = useState(1); const [customExpiryDate, setCustomExpiryDate] = useState( new Date(Date.now() + 86400000).toISOString().split('T')[0] ); const [priceMethod, setPriceMethod] = useState<'CURRENT' | 'MARKET' | 'HIGH' | 'LOW'>('CURRENT'); const [tickOffset, setTickOffset] = useState(0); const [quantityMode, setQuantityMode] = useState<'DIRECT' | 'RATIO'>('DIRECT'); const [quantity, setQuantity] = useState(1); const [quantityRatio, setQuantityRatio] = useState(isBuyMode ? 10 : 100); const currencySymbol = stock.market === MarketType.DOMESTIC ? '원' : '$'; const handleMaxQuantity = () => { if (quantityMode === 'RATIO') { setQuantityRatio(100); } else { if (isBuyMode) { const maxQty = Math.floor(buyingPower / stock.price); setQuantity(maxQty > 0 ? maxQty : 1); } else if (holding) { setQuantity(holding.quantity); } } }; const handleReset = () => { setMonitoringEnabled(false); setImmediateStart(true); setActiveStrategy(isBuyMode ? 'TRAILING_STOP' : 'NONE'); setTriggerEnabled(false); setTriggerType('CURRENT'); setTriggerValue(stock.price); applyAutoCondition('CURRENT', isBuyMode); setPriceMethod('CURRENT'); setTickOffset(0); setQuantity(1); setQuantityRatio(isBuyMode ? 10 : 100); setExpiryDays(1); }; const finalQuantity = useMemo(() => { if (quantityMode === 'RATIO') { if (isBuyMode) { const targetBudget = buyingPower * (quantityRatio / 100); return Math.floor(targetBudget / stock.price) || 0; } else if (holding) { return Math.floor(holding.quantity * (quantityRatio / 100)); } } return quantity; }, [isBuyMode, quantityMode, quantity, quantityRatio, holding, buyingPower, stock.price]); const summaryMessage = useMemo(() => { const action = isBuyMode ? '매수' : '매도'; const quantityText = quantityMode === 'RATIO' ? `${finalQuantity}주(${isBuyMode ? '예수금' : '보유수량'}대비 ${quantityRatio}%)` : `${finalQuantity}주`; const priceMethodMap = { CURRENT: '현재가', MARKET: '시장가', HIGH: '고가', LOW: '저가' }; const tickText = tickOffset === 0 ? '+0틱' : (tickOffset > 0 ? `+${tickOffset}틱` : `${tickOffset}틱`); const priceText = priceMethod === 'MARKET' ? '시장가' : `${priceMethodMap[priceMethod]}${tickText}`; if (!monitoringEnabled) { return `${priceText}로 ${quantityText}를 ${action}합니다.`; } let strategyDesc = ''; if (activeStrategy === 'TRAILING_STOP') { const triggerTypeMap = { CURRENT: '현재가', HIGH: '고가', LOW: '저가', VOLUME: '거래량' }; const triggerLabel = triggerTypeMap[triggerType]; const josa = triggerType === 'VOLUME' ? '이' : '가'; const triggerDesc = triggerEnabled ? `${triggerLabel}${josa} ${triggerValue.toLocaleString()}${triggerType === 'VOLUME' ? '주' : currencySymbol} ${monCondition === 'ABOVE' ? '이상' : '이하'} 도달 후` : '현재 시점부터'; const tsUnitText = tsUnit === 'PERCENT' ? '%' : tsUnit === 'TICK' ? '틱' : currencySymbol; const turnAction = isBuyMode ? '반등' : '하락'; strategyDesc = `${triggerDesc} ${tsValue}${tsUnitText} ${turnAction} 시`; } else if (activeStrategy === 'PROFIT') { const unitLabel = monTargetUnit === 'PRICE' ? '현재가' : monTargetUnit === 'PERCENT' ? '수익률(%)' : '수익금액'; const unit = monTargetUnit === 'PERCENT' ? '%' : currencySymbol; strategyDesc = `(이익실현) ${unitLabel} ${profitValue.toLocaleString()}${unit} 이상 도달 시`; } else if (activeStrategy === 'LOSS') { const unitLabel = monTargetUnit === 'PRICE' ? '현재가' : monTargetUnit === 'PERCENT' ? '수익률(%)' : '수익금액'; const unit = monTargetUnit === 'PERCENT' ? '%' : currencySymbol; strategyDesc = `(손절제한) ${unitLabel} ${lossValue.toLocaleString()}${unit} 이하 도달 시`; } else { strategyDesc = '조건 만족 시'; } return `${strategyDesc} (${priceText})으로 ${quantityText}를 ${action}합니다.`; }, [isBuyMode, monitoringEnabled, activeStrategy, triggerEnabled, triggerType, triggerValue, monCondition, tsValue, tsUnit, priceMethod, tickOffset, finalQuantity, quantityMode, quantityRatio, profitValue, lossValue, monTargetUnit, currencySymbol]); const handleExecute = () => { if (monitoringEnabled) { let finalExpiry = new Date(); if (expiryDays > 0) finalExpiry.setDate(finalExpiry.getDate() + expiryDays); else finalExpiry = new Date(customExpiryDate); onExecute({ stockCode: stock.code, stockName: stock.name, type: orderType, quantity: finalQuantity, monitoringType: activeStrategy === 'TRAILING_STOP' ? 'TRAILING_STOP' : 'PRICE_TRIGGER', triggerPrice: triggerEnabled ? triggerValue : stock.price, trailingType: tsUnit === 'PERCENT' ? 'PERCENT' : 'AMOUNT', trailingValue: tsValue, market: stock.market, expiryDate: finalExpiry }); } else { if (onImmediateOrder) { onImmediateOrder({ stockCode: stock.code, stockName: stock.name, type: orderType, price: priceMethod === 'MARKET' ? 0 : stock.price, quantity: finalQuantity }); } } onClose(); }; const StrategyRadio = ({ type, label, icon: Icon }: { type: StrategyType, label: string, icon: any }) => { const isSelected = activeStrategy === type; return (
setActiveStrategy(type)} className={`p-4 rounded-2xl border-2 transition-all cursor-pointer flex items-center justify-between ${isSelected ? 'bg-amber-50/40 border-amber-300 shadow-sm' : 'bg-transparent border-slate-100 opacity-60'}`} >
{isSelected ? : } {label}
{isSelected && type === 'TRAILING_STOP' && (
e.stopPropagation()}> setTsValue(Number(e.target.value))} />
)} {isSelected && type === 'PROFIT' && (
e.stopPropagation()}> setProfitValue(Number(e.target.value))} /> {monTargetUnit === 'PERCENT' ? '%' : currencySymbol}
)} {isSelected && type === 'LOSS' && (
e.stopPropagation()}> setLossValue(Number(e.target.value))} /> {monTargetUnit === 'PERCENT' ? '%' : currencySymbol}
)}
); }; return (
{/* 헤더 */}
{/* 상단 통합 정보 */}
{stock.name[0]}

{stock.name}

{stock.market === MarketType.DOMESTIC ? 'KRX' : 'NYSE'} {stock.code}

= 0 ? 'text-rose-500' : 'text-blue-600'}`}> {stock.market === MarketType.DOMESTIC ? stock.price.toLocaleString() : `$${stock.price}`}

= 0 ? 'text-rose-500' : 'text-blue-600'}`}> {stock.changePercent >= 0 ? : } {Math.abs(stock.changePercent)}%

거래량

{stock.volume.toLocaleString()}

+5.2%

사용가능 예수금

{buyingPower.toLocaleString()}{currencySymbol}

{holding ? (

보유수량

{holding.quantity} EA

평가수익금

= 0 ? 'text-rose-500' : 'text-blue-600'}`}> {plInfo!.pl > 0 ? '+' : ''}{plInfo!.pl.toLocaleString()} ({plInfo!.percent.toFixed(2)}%)

) : (

미보유 종목

)}

1. 자동 감시 전략

{!isBuyMode ? (
{['PRICE', 'PERCENT', 'AMOUNT'].map(unit => ( ))}
) : (
)} {/* TS 내부 감시 시작 조건 설정 (TS 선택 시에만 표시) */} {activeStrategy === 'TRAILING_STOP' && (
setTriggerEnabled(!triggerEnabled)} className="flex items-center gap-2 cursor-pointer group" > {triggerEnabled ? : } 목표가 설정
{triggerEnabled && (
setTriggerValue(Number(e.target.value))} />
)} {!triggerEnabled && (

※ 조건 미설정 시 현재 시점부터 즉시 반등 감시를 시작합니다.

)}
)}
유효기간
{[1, 5, 30].map(d => ( ))}

2. 매매 실행 조건

{['CURRENT', 'MARKET', 'HIGH', 'LOW'].map((method) => ( ))}
{tickOffset > 0 ? `+${tickOffset}` : tickOffset}
{isBuyMode ? `예수금` : `보유수량`}
{quantityMode === 'DIRECT' ? (
setQuantity(Math.max(1, parseInt(e.target.value) || 0))} />
EA / UNIT
) : (
setQuantityRatio(Math.min(100, Math.max(0, parseInt(e.target.value) || 0)))} />
PERCENT
)}

{summaryMessage}

{/* 푸터 */}
{monitoringEnabled && (
즉시 실행 엔진 자동 가동
)}
); }; export default TradeModal;