This commit is contained in:
2026-01-31 23:55:41 +09:00
parent f25b6f538f
commit 83491c08b9
2 changed files with 262 additions and 83 deletions

View File

@@ -0,0 +1,140 @@
import React, { useState, useEffect, useMemo } from 'react';
import { X, Search, Star, Globe, Building2, TrendingUp, TrendingDown, ChevronRight } from 'lucide-react';
import { StockItem, WatchlistGroup, MarketType } from '../types';
import { DbService } from '../services/dbService';
import { MOCK_STOCKS } from '../constants';
interface StockSearchModalProps {
onClose: () => void;
onSelect: (stock: StockItem) => void;
currentMarket: MarketType;
}
const StockSearchModal: React.FC<StockSearchModalProps> = ({ onClose, onSelect, currentMarket }) => {
const [searchQuery, setSearchQuery] = useState('');
const [groups, setGroups] = useState<WatchlistGroup[]>([]);
const [selectedGroupId, setSelectedGroupId] = useState<string | 'ALL'>('ALL');
const dbService = useMemo(() => new DbService(), []);
useEffect(() => {
const fetchGroups = async () => {
const data = await dbService.getWatchlistGroups();
setGroups(data.filter(g => g.market === currentMarket));
};
fetchGroups();
}, [dbService, currentMarket]);
const filteredStocks = useMemo(() => {
let base = MOCK_STOCKS.filter(s => s.market === currentMarket);
if (selectedGroupId !== 'ALL') {
const group = groups.find(g => g.id === selectedGroupId);
if (group) {
base = base.filter(s => group.codes.includes(s.code));
}
}
if (searchQuery) {
const lowerQuery = searchQuery.toLowerCase();
base = base.filter(s =>
s.name.toLowerCase().includes(lowerQuery) ||
s.code.toLowerCase().includes(lowerQuery)
);
}
return base;
}, [searchQuery, selectedGroupId, groups, currentMarket]);
return (
<div className="fixed inset-0 z-[400] bg-slate-900/40 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-white w-full max-w-3xl h-[600px] rounded-3xl shadow-2xl flex overflow-hidden border border-slate-200 animate-in zoom-in-95 duration-200">
{/* 사이드바: 그룹 목록 */}
<div className="w-56 bg-slate-50 border-r border-slate-100 flex flex-col">
<div className="p-5 border-b border-slate-100">
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
<Star size={14} className="text-amber-500" />
</h4>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1">
<button
onClick={() => setSelectedGroupId('ALL')}
className={`w-full text-left px-4 py-2.5 rounded-xl text-[13px] font-bold transition-all flex items-center justify-between group ${selectedGroupId === 'ALL' ? 'bg-white text-slate-900 shadow-sm border border-slate-100' : 'text-slate-500 hover:bg-slate-100'}`}
>
<ChevronRight size={14} className={`transition-transform ${selectedGroupId === 'ALL' ? 'translate-x-0 opacity-100' : '-translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100'}`} />
</button>
{groups.map(group => (
<button
key={group.id}
onClick={() => setSelectedGroupId(group.id)}
className={`w-full text-left px-4 py-2.5 rounded-xl text-[13px] font-bold transition-all flex items-center justify-between group ${selectedGroupId === group.id ? 'bg-white text-slate-900 shadow-sm border border-slate-100' : 'text-slate-500 hover:bg-slate-100'}`}
>
<span className="truncate">{group.name}</span>
<span className="text-[10px] bg-slate-200 text-slate-500 px-1.5 rounded-md">{group.codes.length}</span>
</button>
))}
</div>
</div>
{/* 메인: 검색 및 목록 */}
<div className="flex-1 flex flex-col bg-white">
<div className="p-4 border-b border-slate-100 flex items-center justify-between gap-4">
<div className="flex-1 relative group">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-300 group-focus-within:text-blue-500 transition-colors" size={18} />
<input
autoFocus
type="text"
placeholder="종목명 또는 코드 입력..."
className="w-full bg-slate-50 border-none rounded-2xl py-2.5 pl-10 pr-4 text-[14px] font-bold outline-none ring-2 ring-transparent focus:ring-blue-500/10 focus:bg-white transition-all"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-full transition-colors text-slate-400">
<X size={20} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-2 custom-scrollbar">
{filteredStocks.length > 0 ? (
filteredStocks.map(stock => (
<div
key={stock.code}
onClick={() => onSelect(stock)}
className="p-3 rounded-2xl border border-slate-50 hover:border-blue-100 hover:bg-blue-50/30 cursor-pointer transition-all flex items-center justify-between group"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-slate-100 rounded-xl flex items-center justify-center text-slate-400 font-bold group-hover:bg-blue-100 group-hover:text-blue-600 transition-colors">
{stock.name[0]}
</div>
<div>
<h5 className="text-[14px] font-black text-slate-800">{stock.name}</h5>
<span className="text-[11px] font-bold text-slate-400 tracking-wider">{stock.code}</span>
</div>
</div>
<div className="text-right">
<p className="text-[14px] font-black font-mono text-slate-900">
{stock.market === MarketType.DOMESTIC ? stock.price.toLocaleString() : `$${stock.price}`}
</p>
<div className={`text-[11px] font-bold flex items-center justify-end gap-1 ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
{stock.changePercent >= 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
{Math.abs(stock.changePercent)}%
</div>
</div>
</div>
))
) : (
<div className="h-full flex flex-col items-center justify-center text-slate-300 space-y-3">
<Search size={48} strokeWidth={1} />
<p className="text-[13px] font-bold"> .</p>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default StockSearchModal;

View File

@@ -1,8 +1,9 @@
import React, { useState, useEffect, useMemo } from 'react'; 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 { X, RotateCcw, ChevronDown, Zap, Plus, Minus, Calendar, ToggleLeft, ToggleRight, CheckSquare, Square, TrendingUp, TrendingDown, Wallet, Target, ShieldAlert, BadgePercent, Save, PlayCircle, Info, BarChart3, Maximize2, Circle, CheckCircle2, Search } from 'lucide-react';
import { StockItem, OrderType, MarketType, ReservedOrder, TradeOrder } from '../types'; import { StockItem, OrderType, MarketType, ReservedOrder, TradeOrder } from '../types';
import { DbService, HoldingItem } from '../services/dbService'; import { DbService, HoldingItem } from '../services/dbService';
import StockSearchModal from './StockSearchModal';
interface TradeModalProps { interface TradeModalProps {
stock: StockItem; stock: StockItem;
@@ -14,7 +15,9 @@ interface TradeModalProps {
type StrategyType = 'NONE' | 'PROFIT' | 'LOSS' | 'TRAILING_STOP'; type StrategyType = 'NONE' | 'PROFIT' | 'LOSS' | 'TRAILING_STOP';
const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClose, onExecute, onImmediateOrder }) => { const TradeModal: React.FC<TradeModalProps> = ({ stock: initialStock, type: initialType, onClose, onExecute, onImmediateOrder }) => {
const [localStock, setLocalStock] = useState<StockItem>(initialStock);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [orderType, setOrderType] = useState<OrderType>(initialType); const [orderType, setOrderType] = useState<OrderType>(initialType);
const isBuyMode = orderType === OrderType.BUY; const isBuyMode = orderType === OrderType.BUY;
@@ -25,21 +28,21 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
const holdings = await dbService.getHoldings(); const holdings = await dbService.getHoldings();
const currentHolding = holdings.find(h => h.code === stock.code) || null; const currentHolding = holdings.find(h => h.code === localStock.code) || null;
setHolding(currentHolding); setHolding(currentHolding);
const summary = await dbService.getAccountSummary(); const summary = await dbService.getAccountSummary();
setBuyingPower(summary.buyingPower); setBuyingPower(summary.buyingPower);
}; };
fetchData(); fetchData();
}, [stock.code, dbService, orderType]); }, [localStock.code, dbService, orderType]);
const plInfo = useMemo(() => { const plInfo = useMemo(() => {
if (!holding) return null; if (!holding) return null;
const pl = (stock.price - holding.avgPrice) * holding.quantity; const pl = (localStock.price - holding.avgPrice) * holding.quantity;
const percent = ((stock.price - holding.avgPrice) / holding.avgPrice) * 100; const percent = ((localStock.price - holding.avgPrice) / holding.avgPrice) * 100;
return { pl, percent }; return { pl, percent };
}, [holding, stock.price]); }, [holding, localStock.price]);
const [monitoringEnabled, setMonitoringEnabled] = useState(false); const [monitoringEnabled, setMonitoringEnabled] = useState(false);
const [immediateStart, setImmediateStart] = useState(true); const [immediateStart, setImmediateStart] = useState(true);
@@ -54,7 +57,7 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
// TS 내부 감시 시작 조건 (Trigger) // TS 내부 감시 시작 조건 (Trigger)
const [triggerEnabled, setTriggerEnabled] = useState(false); const [triggerEnabled, setTriggerEnabled] = useState(false);
const [triggerType, setTriggerType] = useState<'CURRENT' | 'HIGH' | 'LOW' | 'VOLUME'>('CURRENT'); const [triggerType, setTriggerType] = useState<'CURRENT' | 'HIGH' | 'LOW' | 'VOLUME'>('CURRENT');
const [triggerValue, setTriggerValue] = useState<number>(stock.price); const [triggerValue, setTriggerValue] = useState<number>(localStock.price);
const [monCondition, setMonCondition] = useState<'ABOVE' | 'BELOW'>(isBuyMode ? 'BELOW' : 'ABOVE'); const [monCondition, setMonCondition] = useState<'ABOVE' | 'BELOW'>(isBuyMode ? 'BELOW' : 'ABOVE');
// 자동 조건 결정 로직 // 자동 조건 결정 로직
@@ -80,13 +83,13 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
} }
applyAutoCondition(triggerType, isBuyMode); applyAutoCondition(triggerType, isBuyMode);
if (triggerType !== 'VOLUME') { if (triggerType !== 'VOLUME') {
setTriggerValue(stock.price); setTriggerValue(localStock.price);
} }
}, [isBuyMode, stock.price]); }, [isBuyMode, localStock.price]);
const [monTargetUnit, setMonTargetUnit] = useState<'PRICE' | 'PERCENT' | 'AMOUNT'>('PRICE'); const [monTargetUnit, setMonTargetUnit] = useState<'PRICE' | 'PERCENT' | 'AMOUNT'>('PRICE');
const [profitValue, setProfitValue] = useState<number>(stock.price * 1.1); const [profitValue, setProfitValue] = useState<number>(localStock.price * 1.1);
const [lossValue, setLossValue] = useState<number>(stock.price * 0.9); const [lossValue, setLossValue] = useState<number>(localStock.price * 0.9);
const [expiryDays, setExpiryDays] = useState<number>(1); const [expiryDays, setExpiryDays] = useState<number>(1);
const [customExpiryDate, setCustomExpiryDate] = useState<string>( const [customExpiryDate, setCustomExpiryDate] = useState<string>(
@@ -99,14 +102,14 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
const [quantity, setQuantity] = useState<number>(1); const [quantity, setQuantity] = useState<number>(1);
const [quantityRatio, setQuantityRatio] = useState<number>(isBuyMode ? 10 : 100); const [quantityRatio, setQuantityRatio] = useState<number>(isBuyMode ? 10 : 100);
const currencySymbol = stock.market === MarketType.DOMESTIC ? '원' : '$'; const currencySymbol = localStock.market === MarketType.DOMESTIC ? '원' : '$';
const handleMaxQuantity = () => { const handleMaxQuantity = () => {
if (quantityMode === 'RATIO') { if (quantityMode === 'RATIO') {
setQuantityRatio(100); setQuantityRatio(100);
} else { } else {
if (isBuyMode) { if (isBuyMode) {
const maxQty = Math.floor(buyingPower / stock.price); const maxQty = Math.floor(buyingPower / localStock.price);
setQuantity(maxQty > 0 ? maxQty : 1); setQuantity(maxQty > 0 ? maxQty : 1);
} else if (holding) { } else if (holding) {
setQuantity(holding.quantity); setQuantity(holding.quantity);
@@ -120,7 +123,7 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
setActiveStrategy(isBuyMode ? 'TRAILING_STOP' : 'NONE'); setActiveStrategy(isBuyMode ? 'TRAILING_STOP' : 'NONE');
setTriggerEnabled(false); setTriggerEnabled(false);
setTriggerType('CURRENT'); setTriggerType('CURRENT');
setTriggerValue(stock.price); setTriggerValue(localStock.price);
applyAutoCondition('CURRENT', isBuyMode); applyAutoCondition('CURRENT', isBuyMode);
setPriceMethod('CURRENT'); setPriceMethod('CURRENT');
setTickOffset(0); setTickOffset(0);
@@ -133,13 +136,13 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
if (quantityMode === 'RATIO') { if (quantityMode === 'RATIO') {
if (isBuyMode) { if (isBuyMode) {
const targetBudget = buyingPower * (quantityRatio / 100); const targetBudget = buyingPower * (quantityRatio / 100);
return Math.floor(targetBudget / stock.price) || 0; return Math.floor(targetBudget / localStock.price) || 0;
} else if (holding) { } else if (holding) {
return Math.floor(holding.quantity * (quantityRatio / 100)); return Math.floor(holding.quantity * (quantityRatio / 100));
} }
} }
return quantity; return quantity;
}, [isBuyMode, quantityMode, quantity, quantityRatio, holding, buyingPower, stock.price]); }, [isBuyMode, quantityMode, quantity, quantityRatio, holding, buyingPower, localStock.price]);
const summaryMessage = useMemo(() => { const summaryMessage = useMemo(() => {
const action = isBuyMode ? '매수' : '매도'; const action = isBuyMode ? '매수' : '매도';
@@ -200,24 +203,24 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
else finalExpiry = new Date(customExpiryDate); else finalExpiry = new Date(customExpiryDate);
onExecute({ onExecute({
stockCode: stock.code, stockCode: localStock.code,
stockName: stock.name, stockName: localStock.name,
type: orderType, type: orderType,
quantity: finalQuantity, quantity: finalQuantity,
monitoringType: activeStrategy === 'TRAILING_STOP' ? 'TRAILING_STOP' : 'PRICE_TRIGGER', monitoringType: activeStrategy === 'TRAILING_STOP' ? 'TRAILING_STOP' : 'PRICE_TRIGGER',
triggerPrice: triggerEnabled ? triggerValue : stock.price, triggerPrice: triggerEnabled ? triggerValue : localStock.price,
trailingType: tsUnit === 'PERCENT' ? 'PERCENT' : 'AMOUNT', trailingType: tsUnit === 'PERCENT' ? 'PERCENT' : 'AMOUNT',
trailingValue: tsValue, trailingValue: tsValue,
market: stock.market, market: localStock.market,
expiryDate: finalExpiry expiryDate: finalExpiry
}); });
} else { } else {
if (onImmediateOrder) { if (onImmediateOrder) {
onImmediateOrder({ onImmediateOrder({
stockCode: stock.code, stockCode: localStock.code,
stockName: stock.name, stockName: localStock.name,
type: orderType, type: orderType,
price: priceMethod === 'MARKET' ? 0 : stock.price, price: priceMethod === 'MARKET' ? 0 : localStock.price,
quantity: finalQuantity quantity: finalQuantity
}); });
} }
@@ -230,12 +233,12 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
return ( return (
<div <div
onClick={() => setActiveStrategy(type)} onClick={() => 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'}`} className={`p-3 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'}`}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
{isSelected ? <CheckCircle2 className="text-amber-600" size={18} /> : <Circle className="text-slate-300" size={18} />} {isSelected ? <CheckCircle2 className="text-amber-600" size={16} /> : <Circle className="text-slate-300" size={16} />}
<Icon className={isSelected ? 'text-amber-600' : 'text-slate-300'} size={18} /> <Icon className={isSelected ? 'text-amber-600' : 'text-slate-300'} size={16} />
<span className="text-[13px] font-black text-slate-800 uppercase tracking-tight">{label}</span> <span className="text-[12px] font-black text-slate-800 uppercase tracking-tight">{label}</span>
</div> </div>
{isSelected && type === 'TRAILING_STOP' && ( {isSelected && type === 'TRAILING_STOP' && (
@@ -268,10 +271,10 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
return ( return (
<div className="fixed inset-0 z-[300] bg-slate-900/60 backdrop-blur-md flex items-center justify-center p-4"> <div className="fixed inset-0 z-[300] bg-slate-900/60 backdrop-blur-md flex items-center justify-center p-4">
<div className="bg-white w-full max-w-5xl rounded-3xl shadow-2xl animate-in zoom-in-95 duration-200 flex flex-col max-h-[90vh] overflow-hidden border border-slate-200"> <div className="bg-white w-full max-w-4xl rounded-3xl shadow-2xl animate-in zoom-in-95 duration-200 flex flex-col max-h-[90vh] overflow-hidden border border-slate-200">
{/* 헤더 */} {/* 헤더 */}
<div className="px-6 py-3 flex justify-between items-center bg-white border-b border-slate-100 shrink-0"> <div className="px-5 py-2 flex justify-between items-center bg-white border-b border-slate-100 shrink-0">
<button onClick={handleReset} className="flex items-center gap-1.5 text-slate-400 hover:text-slate-600 transition-colors"> <button onClick={handleReset} className="flex items-center gap-1.5 text-slate-400 hover:text-slate-600 transition-colors">
<RotateCcw size={16} /> <RotateCcw size={16} />
<span className="text-[11px] font-black uppercase tracking-wider"> </span> <span className="text-[11px] font-black uppercase tracking-wider"> </span>
@@ -287,65 +290,86 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
<div className="flex-1 overflow-y-auto custom-scrollbar bg-slate-50/20"> <div className="flex-1 overflow-y-auto custom-scrollbar bg-slate-50/20">
{/* 상단 통합 정보 */} {/* 상단 통합 정보 */}
<div className="px-8 py-6 bg-white border-b border-slate-100 grid grid-cols-1 lg:grid-cols-2 gap-6 items-center"> <div className="px-5 py-3 bg-white border-b border-slate-100 flex items-center justify-between gap-4">
<div className="flex items-center gap-6"> {/* 1. 종목 정보 */}
<div className="w-16 h-16 bg-slate-900 rounded-2xl flex items-center justify-center text-white font-black italic text-[20px] shadow-lg">{stock.name[0]}</div> <div className="flex items-center gap-3 min-w-0 group relative">
<div className="space-y-0.5"> <button
<h3 className="text-xl font-black text-slate-900 tracking-tighter flex items-center gap-1.5">{stock.name} <ChevronDown size={18} className="text-slate-200" /></h3> onClick={() => setIsSearchOpen(true)}
<div className="flex items-center gap-2.5 text-[11px] font-black uppercase text-slate-400"> className="w-10 h-10 bg-slate-900 rounded-xl flex items-center justify-center text-white font-black italic text-[14px] shrink-0 shadow-md transition-all hover:bg-slate-800 relative group-hover:bg-blue-600"
<span className="bg-slate-100 px-2 py-0.5 rounded-md border border-slate-200 text-slate-600">{stock.market === MarketType.DOMESTIC ? 'KRX' : 'NYSE'}</span> >
<span className="tracking-widest">{stock.code}</span> <div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<Search size={18} />
</div> </div>
</div> <span className="group-hover:opacity-0 transition-opacity">{localStock.name[0]}</span>
<div className="ml-4 border-l border-slate-100 pl-6 space-y-0.5 flex flex-col justify-center"> </button>
<p className={`text-2xl font-black font-mono tracking-tighter leading-none ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}> <div className="min-w-0 cursor-pointer" onClick={() => setIsSearchOpen(true)}>
{stock.market === MarketType.DOMESTIC ? stock.price.toLocaleString() : `$${stock.price}`} <h3 className="text-[15px] font-black text-slate-900 truncate flex items-center gap-1 leading-tight group-hover:text-blue-600 transition-colors">
</p> {localStock.name} <ChevronDown size={14} className="text-slate-300" />
<div className={`text-[12px] font-black flex items-center gap-1.5 ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}> </h3>
{stock.changePercent >= 0 ? <TrendingUp size={14} /> : <TrendingDown size={14} />} <div className="flex items-center gap-1.5 text-[9px] font-black uppercase text-slate-400 mt-0.5">
{Math.abs(stock.changePercent)}% <span className="bg-slate-100 px-1 py-0.5 rounded text-slate-600 border border-slate-200/50">{localStock.market === MarketType.DOMESTIC ? 'KRX' : 'NYSE'}</span>
</div> <span className="tracking-widest">{localStock.code}</span>
</div>
<div className="ml-4 border-l border-slate-100 pl-6 flex flex-col justify-center gap-1">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest leading-none"></p>
<p className="text-[14px] font-black font-mono text-slate-900 leading-none">{stock.volume.toLocaleString()}</p>
<div className="flex items-center gap-1 text-[10px] font-black text-rose-500">
<TrendingUp size={10} /> +5.2%
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4 bg-slate-50 p-4 rounded-2xl border border-slate-100 shadow-sm"> {/* 2. 시세 및 계좌 정보 (가로 나열) */}
<div className="space-y-0.5 pr-4 border-r border-slate-200"> <div className="flex items-center gap-4 shrink-0">
<p className="text-[9px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-1.5"><Wallet size={12} className="text-blue-500" /> </p> {/* 시세 */}
<p className="text-[14px] font-black font-mono text-slate-900">{buyingPower.toLocaleString()}{currencySymbol}</p> <div className="border-l border-slate-100 pl-4 flex flex-col justify-center">
</div> <p className={`text-[17px] font-black font-mono leading-none ${localStock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
{holding ? ( {localStock.market === MarketType.DOMESTIC ? localStock.price.toLocaleString() : `$${localStock.price}`}
<div className="flex flex-col gap-1.5">
<div className="flex justify-between items-center">
<p className="text-[9px] font-black text-slate-400 uppercase tracking-widest"></p>
<p className="text-[12px] font-black font-mono text-slate-900">{holding.quantity} <span className="text-[9px] text-slate-400">EA</span></p>
</div>
<div className="flex justify-between items-center">
<p className="text-[9px] font-black text-slate-400 uppercase tracking-widest"></p>
<p className={`text-[12px] font-black font-mono ${plInfo!.pl >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
{plInfo!.pl > 0 ? '+' : ''}{plInfo!.pl.toLocaleString()} <span className="text-[10px]">({plInfo!.percent.toFixed(2)}%)</span>
</p> </p>
<div className={`text-[10px] font-black flex items-center gap-1 mt-1 ${localStock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
{localStock.changePercent >= 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
{Math.abs(localStock.changePercent)}%
</div>
</div>
{/* 거래량 */}
<div className="border-l border-slate-100 pl-4 flex flex-col justify-center">
<p className="text-[8px] font-black text-slate-400 uppercase tracking-widest leading-none mb-1"></p>
<p className="text-[12px] font-black font-mono text-slate-900 leading-none">{localStock.volume.toLocaleString()}</p>
<div className="flex items-center gap-1 text-[9px] font-black text-rose-500 mt-1">
<TrendingUp size={10} /> +5.2%
</div>
</div>
{/* 예수금 */}
<div className="border-l border-slate-100 pl-4 flex flex-col justify-center">
<p className="text-[8px] font-black text-slate-400 uppercase tracking-widest leading-none mb-1 flex items-center gap-1"><Wallet size={11} className="text-blue-500" /> </p>
<p className="text-[13px] font-black font-mono text-slate-900 leading-none">{buyingPower.toLocaleString()}{currencySymbol}</p>
</div>
{/* 보유/평가 */}
<div className="border-l border-slate-100 pl-4 flex flex-col justify-center min-w-[110px]">
{holding ? (
<div className="space-y-1">
<div className="flex justify-between items-center gap-3">
<span className="text-[8px] font-black text-slate-400 uppercase tracking-widest leading-none"></span>
<span className="text-[11px] font-black font-mono text-slate-900 leading-none">{holding.quantity} <span className="text-[8px] text-slate-300">EA</span></span>
</div>
<div className="flex justify-between items-center gap-3">
<span className="text-[8px] font-black text-slate-400 uppercase tracking-widest leading-none"></span>
<span className={`text-[11px] font-black font-mono leading-none ${plInfo!.pl >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
{plInfo!.percent.toFixed(1)}%
</span>
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex items-center justify-center opacity-20"> <div className="py-1 px-2 bg-slate-50 rounded border border-dashed border-slate-200 text-center">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]"> </p> <p className="text-[9px] font-black text-slate-300 uppercase tracking-widest"></p>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div>
<div className="grid grid-cols-2 gap-px bg-slate-200"> <div className="grid grid-cols-2 gap-px bg-slate-200">
<div className={`bg-white p-8 space-y-6 transition-all duration-300 ${!monitoringEnabled ? 'opacity-40 grayscale-[0.5]' : ''}`}> <div className={`bg-white p-5 space-y-5 transition-all duration-300 ${!monitoringEnabled ? 'opacity-40 grayscale-[0.5]' : ''}`}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-[14px] font-black text-slate-800 uppercase tracking-tight flex items-center gap-2"> <h4 className="text-[13px] font-black text-slate-800 uppercase tracking-tight flex items-center gap-2">
<ShieldAlert size={18} className={monitoringEnabled ? 'text-rose-500' : 'text-slate-300'} /> <ShieldAlert size={16} className={monitoringEnabled ? 'text-rose-500' : 'text-slate-300'} />
1. 1.
</h4> </h4>
<button <button
@@ -455,13 +479,13 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
</div> </div>
</div> </div>
<div className="bg-white p-8 space-y-6"> <div className="bg-white p-5 space-y-5">
<h4 className="text-[14px] font-black text-slate-800 uppercase tracking-tight flex items-center gap-2"> <h4 className="text-[13px] font-black text-slate-800 uppercase tracking-tight flex items-center gap-2">
<Zap size={18} className="text-blue-500" /> <Zap size={16} className="text-blue-500" />
2. 2.
</h4> </h4>
<div className="space-y-8"> <div className="space-y-6">
<div className="space-y-3"> <div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest pl-1"> </label> <label className="text-[11px] font-black text-slate-400 uppercase tracking-widest pl-1"> </label>
<div className="grid grid-cols-4 gap-2 bg-slate-50 p-1 rounded-2xl border border-slate-100"> <div className="grid grid-cols-4 gap-2 bg-slate-50 p-1 rounded-2xl border border-slate-100">
@@ -520,7 +544,7 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
</div> </div>
</div> </div>
<div className="p-4 bg-slate-900/5 rounded-2xl border border-slate-100 text-center"> <div className="p-3 bg-slate-900/5 rounded-2xl border border-slate-100 text-center">
<p className="text-[12px] font-black text-slate-600 leading-tight"> <p className="text-[12px] font-black text-slate-600 leading-tight">
{summaryMessage} {summaryMessage}
</p> </p>
@@ -531,7 +555,7 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
</div> </div>
{/* 푸터 */} {/* 푸터 */}
<div className="px-8 py-6 bg-white border-t border-slate-100 flex items-center justify-between shrink-0 gap-6"> <div className="px-6 py-4 bg-white border-t border-slate-100 flex items-center justify-between shrink-0 gap-4">
{monitoringEnabled && ( {monitoringEnabled && (
<div className="flex items-center gap-3 bg-slate-50 px-4 py-2 rounded-2xl border border-slate-200 shadow-sm"> <div className="flex items-center gap-3 bg-slate-50 px-4 py-2 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex flex-col"> <div className="flex flex-col">
@@ -554,6 +578,21 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
{monitoringEnabled ? <><Save className="w-5 h-5" /> {immediateStart ? '즉시 활성화' : '대기 저장'}</> : <><Zap size={22} fill="currentColor" /> {isBuyMode ? '매수' : '매도'} </>} {monitoringEnabled ? <><Save className="w-5 h-5" /> {immediateStart ? '즉시 활성화' : '대기 저장'}</> : <><Zap size={22} fill="currentColor" /> {isBuyMode ? '매수' : '매도'} </>}
</button> </button>
</div> </div>
{isSearchOpen && (
<StockSearchModal
currentMarket={localStock.market}
onClose={() => setIsSearchOpen(false)}
onSelect={(newStock) => {
setLocalStock(newStock);
setIsSearchOpen(false);
// 초기화 로직 (트리거 등 새 종목 가격 연동)
setTriggerValue(newStock.price);
setProfitValue(newStock.price * 1.1);
setLossValue(newStock.price * 0.9);
}}
/>
)}
</div> </div>
</div> </div>
); );