initial commit
This commit is contained in:
103
components/CommonUI.tsx
Normal file
103
components/CommonUI.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
// --- StatCard: 대시보드 및 자산 요약용 ---
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: string;
|
||||
change?: string;
|
||||
isUp?: boolean;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
export const StatCard: React.FC<StatCardProps> = ({ title, value, change, isUp, icon }) => (
|
||||
<div className="bg-white p-8 rounded-[3rem] shadow-sm border border-slate-100 flex items-start justify-between group hover:border-blue-100 transition-all">
|
||||
<div>
|
||||
<p className="text-[11px] font-black text-slate-400 mb-3 uppercase tracking-widest">{title}</p>
|
||||
<h4 className="text-2xl font-black text-slate-900 leading-none">{value}</h4>
|
||||
{change && (
|
||||
<p className={`text-[11px] font-black mt-4 flex items-center gap-2 ${isUp ? 'text-emerald-500' : 'text-rose-500'}`}>
|
||||
{change}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-slate-50 p-5 rounded-3xl group-hover:bg-blue-50 transition-colors">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// --- FilterChip: 시장/정렬/카테고리 필터용 ---
|
||||
interface FilterChipProps {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const FilterChip: React.FC<FilterChipProps> = ({ active, onClick, label }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`px-5 py-2 rounded-full text-[12px] font-black transition-all ${active ? 'bg-slate-900 text-white shadow-lg' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
||||
// --- TabButton: 대분류 섹션 전환용 ---
|
||||
interface TabButtonProps {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const TabButton: React.FC<TabButtonProps> = ({ active, onClick, icon, label }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex items-center gap-3 pb-4 whitespace-nowrap transition-all border-b-2 ${active ? 'border-blue-600 text-blue-600 font-black' : 'border-transparent text-slate-400 font-bold hover:text-slate-600'}`}
|
||||
>
|
||||
{icon}
|
||||
<span className="text-base tracking-tight">{label}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
// --- InputGroup: 설정창 및 폼 입력용 ---
|
||||
interface InputGroupProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
placeholder: string;
|
||||
type?: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const InputGroup: React.FC<InputGroupProps> = ({ label, value, onChange, placeholder, type = "text", icon }) => (
|
||||
<div className="space-y-3">
|
||||
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest block pl-1 flex items-center gap-2">
|
||||
{icon} {label}
|
||||
</label>
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full p-4 bg-slate-50 border border-slate-200 rounded-[1.2rem] focus:border-blue-500 focus:bg-white outline-none transition-all font-bold text-slate-800 placeholder:text-slate-300 shadow-sm"
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
// --- ToggleButton: 활성화/비활성 스위치 ---
|
||||
interface ToggleButtonProps {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const ToggleButton: React.FC<ToggleButtonProps> = ({ active, onClick }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-all focus:outline-none ${active ? 'bg-emerald-500' : 'bg-slate-300'}`}
|
||||
>
|
||||
<span className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${active ? 'translate-x-7' : 'translate-x-2'}`} />
|
||||
</button>
|
||||
);
|
||||
21
components/InsightBubble.tsx
Normal file
21
components/InsightBubble.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface InsightBubbleProps {
|
||||
user: string;
|
||||
time: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const InsightBubble: React.FC<InsightBubbleProps> = ({ user, time, content }) => (
|
||||
<div className="p-5 bg-slate-50/70 rounded-[1.5rem] border border-slate-100 space-y-2 hover:bg-white hover:shadow-md transition-all">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-blue-100" />
|
||||
<span className="text-[11px] font-black text-slate-700">{user}</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-slate-400">{time}</span>
|
||||
</div>
|
||||
<p className="text-[13px] font-medium text-slate-600 leading-relaxed">{content}</p>
|
||||
</div>
|
||||
);
|
||||
183
components/StockDetailModal.tsx
Normal file
183
components/StockDetailModal.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
X, TrendingUp, BarChart4, PieChart, Info, Building2, Layers, RotateCw
|
||||
} from 'lucide-react';
|
||||
import { StockItem, MarketType, StockTick } from '../types';
|
||||
import { DbService } from '../services/dbService';
|
||||
|
||||
interface StockDetailModalProps {
|
||||
stock: StockItem;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StockDetailModal: React.FC<StockDetailModalProps> = ({ stock, onClose }) => {
|
||||
const [ticks, setTicks] = useState<StockTick[]>([]);
|
||||
const dbService = useMemo(() => new DbService(), []);
|
||||
|
||||
useEffect(() => {
|
||||
const refreshTicks = async () => {
|
||||
const history = await dbService.getStockTicks(stock.code);
|
||||
setTicks(history);
|
||||
};
|
||||
|
||||
refreshTicks();
|
||||
const interval = setInterval(refreshTicks, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [stock.code, dbService]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[300] bg-slate-900/40 backdrop-blur-xl flex items-center justify-center p-6 animate-in fade-in duration-300">
|
||||
<div className="bg-white w-full max-w-7xl rounded-[4rem] h-[85vh] shadow-2xl flex flex-col overflow-hidden border border-slate-100 animate-in slide-in-from-bottom-10">
|
||||
{/* Header */}
|
||||
<div className="px-16 py-10 flex justify-between items-center border-b border-slate-50">
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="w-20 h-20 bg-slate-900 rounded-[2rem] flex items-center justify-center text-white shadow-xl">
|
||||
<BarChart4 size={40} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<h2 className="text-4xl font-black text-slate-900 tracking-tighter italic">{stock.name}</h2>
|
||||
<span className={`px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-[0.2em] ${stock.market === MarketType.DOMESTIC ? 'bg-red-50 text-red-500' : 'bg-blue-50 text-blue-600'}`}>
|
||||
{stock.market}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-base text-slate-400 font-mono font-bold tracking-[0.3em] uppercase">{stock.code}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="text-right">
|
||||
<p className="text-5xl font-black text-slate-900 tracking-tighter mb-2">
|
||||
{stock.market === MarketType.DOMESTIC ? stock.price.toLocaleString() : `$${stock.price}`}
|
||||
</p>
|
||||
<p className={`text-xl font-bold ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'} flex items-center justify-end gap-2`}>
|
||||
{stock.changePercent >= 0 ? <TrendingUp size={24}/> : <TrendingUp size={24} className="rotate-180" />}
|
||||
{Math.abs(stock.changePercent)}%
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-4 bg-slate-50 hover:bg-slate-100 text-slate-400 rounded-full transition-all">
|
||||
<X size={32} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left: Chart */}
|
||||
<div className="flex-[1.5] p-16 border-r border-slate-50 overflow-hidden flex flex-col">
|
||||
<div className="flex items-center justify-between mb-10">
|
||||
<h4 className="text-[12px] font-black text-slate-400 uppercase tracking-[0.3em] flex items-center gap-3">
|
||||
<PieChart size={20} className="text-blue-500" /> 시계열 시세 차트 (실시간)
|
||||
</h4>
|
||||
<span className="text-[10px] bg-slate-900 text-white px-4 py-1.5 rounded-lg font-black uppercase tracking-widest flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></span> 데이터 수집 중
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 relative bg-slate-50/50 rounded-[3rem] border border-slate-100 overflow-hidden group">
|
||||
{ticks.length > 0 ? (
|
||||
<div className="w-full h-full p-10">
|
||||
<SimpleLineChart data={ticks} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center text-slate-300 gap-6">
|
||||
<RotateCw size={48} className="animate-spin" />
|
||||
<p className="text-sm font-black uppercase tracking-widest">분 단위 시세 데이터 수집 중...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Info */}
|
||||
<div className="flex-1 p-16 overflow-y-auto scrollbar-hide space-y-12">
|
||||
<section>
|
||||
<h4 className="text-[12px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8 flex items-center gap-3">
|
||||
<Layers size={20} className="text-blue-500" /> 연관 테마 분석
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{stock.themes?.map(theme => (
|
||||
<span key={theme} className="px-6 py-3 bg-blue-50 text-blue-600 rounded-2xl text-[12px] font-black uppercase tracking-widest shadow-sm border border-blue-100">
|
||||
#{theme}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4 className="text-[12px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8 flex items-center gap-3">
|
||||
<Building2 size={20} className="text-purple-500" /> 기업 분석 핵심 지표
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<DataCard label="PER" value={stock.per} desc="Price Earning Ratio" />
|
||||
<DataCard label="PBR" value={stock.pbr} desc="Price Book-value Ratio" />
|
||||
<DataCard label="ROE" value={stock.roe} suffix="%" desc="Return On Equity" />
|
||||
<DataCard label="시가총액" value={stock.marketCap} suffix="억" desc="Market Cap" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-slate-900 p-10 rounded-[2.5rem] shadow-2xl">
|
||||
<h4 className="text-[11px] font-black text-blue-400 uppercase tracking-[0.25em] mb-6">AI Strategy Insight</h4>
|
||||
<div className="space-y-6">
|
||||
<ScoreBar label="BUY" score={stock.aiScoreBuy} color="blue" />
|
||||
<ScoreBar label="SELL" score={stock.aiScoreSell} color="rose" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- Helpers ---
|
||||
const SimpleLineChart: React.FC<{ data: StockTick[] }> = ({ data }) => {
|
||||
const prices = data.map(d => d.price);
|
||||
const min = Math.min(...prices) * 0.9995;
|
||||
const max = Math.max(...prices) * 1.0005;
|
||||
const range = max - min || 1;
|
||||
const width = 800;
|
||||
const height = 400;
|
||||
|
||||
if (data.length <= 1) return <div className="flex items-center justify-center h-full text-slate-400 uppercase font-black text-xs tracking-widest">데이터 수집 부족</div>;
|
||||
|
||||
const points = data.map((d, i) => {
|
||||
const x = (i / (data.length - 1)) * width;
|
||||
const y = height - ((d.price - min) / range) * height;
|
||||
return `${x},${y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<svg viewBox={`0 0 ${width} ${height}`} className="w-full h-full overflow-visible">
|
||||
<polyline fill="none" stroke="#3b82f6" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" points={points} />
|
||||
<polygon points={`0,${height} ${points} ${width},${height}`} fill="url(#grad-dash)" />
|
||||
<circle cx={width} cy={height - ((data[data.length-1].price - min) / range) * height} r="6" fill="#3b82f6" className="animate-pulse" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const DataCard: React.FC<{ label: string, value?: number, suffix?: string, desc: string }> = ({ label, value, suffix = '', desc }) => (
|
||||
<div className="bg-white border border-slate-100 p-8 rounded-[2rem] shadow-sm hover:border-blue-100 transition-all group">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{label}</span>
|
||||
<Info size={14} className="text-slate-200 group-hover:text-blue-400 transition-colors" />
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1 mb-1">
|
||||
<span className="text-2xl font-black text-slate-900 leading-none">{value !== undefined ? value.toLocaleString() : 'N/A'}</span>
|
||||
<span className="text-[12px] font-black text-slate-400">{suffix}</span>
|
||||
</div>
|
||||
<p className="text-[9px] font-bold text-slate-300 uppercase tracking-widest">{desc}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ScoreBar: React.FC<{ label: string, score: number, color: 'rose' | 'blue' }> = ({ label, score, color }) => (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between items-center text-[10px] font-black">
|
||||
<span className={color === 'rose' ? 'text-rose-500' : 'text-blue-600'}>{label}</span>
|
||||
<span className="text-slate-400">{score}pt</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-white/10 rounded-full overflow-hidden">
|
||||
<div className={`h-full transition-all duration-1000 ${color === 'rose' ? 'bg-rose-500' : 'bg-blue-600'}`} style={{ width: `${score}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default StockDetailModal;
|
||||
90
components/StockRow.tsx
Normal file
90
components/StockRow.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Zap, ShoppingCart, Star } from 'lucide-react';
|
||||
import { StockItem, MarketType, OrderType } from '../types';
|
||||
|
||||
interface StockRowProps {
|
||||
stock: StockItem;
|
||||
rank?: number;
|
||||
showRank?: boolean;
|
||||
showActions?: boolean;
|
||||
showRatioBar?: boolean;
|
||||
showPL?: { pl: number; percent: number };
|
||||
isWatchlisted?: boolean;
|
||||
onClick?: () => void;
|
||||
onTrade?: (type: OrderType) => void;
|
||||
onToggleWatchlist?: () => void;
|
||||
}
|
||||
|
||||
export const StockRow: React.FC<StockRowProps> = ({
|
||||
stock, rank, showRank, showActions, showRatioBar, showPL, isWatchlisted, onClick, onTrade, onToggleWatchlist
|
||||
}) => {
|
||||
return (
|
||||
<tr
|
||||
onClick={onClick}
|
||||
className="group cursor-pointer transition-colors hover:bg-slate-50/70"
|
||||
>
|
||||
{showRank && (
|
||||
<td className="pl-6 py-3 font-mono font-black text-slate-400 group-hover:text-blue-600 transition-colors">
|
||||
{rank}
|
||||
</td>
|
||||
)}
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{onToggleWatchlist && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onToggleWatchlist(); }}
|
||||
className="text-slate-300 hover:text-amber-500 transition-colors"
|
||||
>
|
||||
<Star size={16} fill={isWatchlisted ? "currentColor" : "none"} className={isWatchlisted ? "text-amber-500" : ""} />
|
||||
</button>
|
||||
)}
|
||||
<div className="w-8 h-8 rounded-lg bg-slate-900 flex items-center justify-center text-white text-[10px] font-black shadow-sm overflow-hidden">
|
||||
{stock.name[0]}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-black text-slate-900 text-[13px] tracking-tight group-hover:text-blue-600">{stock.name}</span>
|
||||
<span className="text-[9px] text-slate-400 font-mono font-bold">{stock.code}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono font-black text-slate-800 text-[13px]">
|
||||
{stock.market === MarketType.DOMESTIC ? stock.price.toLocaleString() + '원' : '$' + stock.price}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
{showPL ? (
|
||||
<div className={showPL.pl >= 0 ? 'text-rose-500' : 'text-blue-600'}>
|
||||
<p className="font-black text-[13px]">{showPL.pl.toLocaleString()}</p>
|
||||
<p className="text-[10px] font-bold">({showPL.percent.toFixed(2)}%)</p>
|
||||
</div>
|
||||
) : (
|
||||
<span className={`font-black text-[13px] ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
|
||||
{stock.changePercent >= 0 ? '+' : ''}{stock.changePercent}%
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
{showRatioBar && (
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex flex-col items-end gap-0.5">
|
||||
<div className="w-full h-1 rounded-full bg-slate-100 overflow-hidden flex">
|
||||
<div className="h-full bg-rose-400" style={{ width: `${stock.buyRatio || 50}%` }} />
|
||||
<div className="h-full bg-blue-400" style={{ width: `${stock.sellRatio || 50}%` }} />
|
||||
</div>
|
||||
<div className="flex justify-between w-full text-[8px] font-black font-mono">
|
||||
<span className="text-rose-500">{stock.buyRatio || 50}</span>
|
||||
<span className="text-blue-500">{stock.sellRatio || 50}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
{showActions && onTrade && (
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex gap-1.5 justify-end opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button onClick={(e) => { e.stopPropagation(); onTrade(OrderType.BUY); }} className="p-1.5 bg-rose-50 text-rose-500 rounded-lg hover:bg-rose-500 hover:text-white transition-all"><Zap size={12} fill="currentColor" /></button>
|
||||
<button onClick={(e) => { e.stopPropagation(); onTrade(OrderType.SELL); }} className="p-1.5 bg-blue-50 text-blue-500 rounded-lg hover:bg-blue-500 hover:text-white transition-all"><ShoppingCart size={12} /></button>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
510
components/TradeModal.tsx
Normal file
510
components/TradeModal.tsx
Normal file
@@ -0,0 +1,510 @@
|
||||
|
||||
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<ReservedOrder, 'id' | 'status' | 'createdAt'>) => Promise<void>;
|
||||
onImmediateOrder?: (order: Omit<TradeOrder, 'id' | 'timestamp' | 'status'>) => Promise<void>;
|
||||
}
|
||||
|
||||
type StrategyType = 'NONE' | 'PROFIT' | 'LOSS' | 'TRAILING_STOP';
|
||||
|
||||
const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClose, onExecute, onImmediateOrder }) => {
|
||||
const [orderType, setOrderType] = useState<OrderType>(initialType);
|
||||
const isBuyMode = orderType === OrderType.BUY;
|
||||
|
||||
const [holding, setHolding] = useState<HoldingItem | null>(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<StrategyType>(isBuyMode ? 'TRAILING_STOP' : 'NONE');
|
||||
|
||||
// TS 설정
|
||||
const [tsValue, setTsValue] = useState<number>(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<number>(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<number>(stock.price * 1.1);
|
||||
const [lossValue, setLossValue] = useState<number>(stock.price * 0.9);
|
||||
|
||||
const [expiryDays, setExpiryDays] = useState<number>(1);
|
||||
const [customExpiryDate, setCustomExpiryDate] = useState<string>(
|
||||
new Date(Date.now() + 86400000).toISOString().split('T')[0]
|
||||
);
|
||||
|
||||
const [priceMethod, setPriceMethod] = useState<'CURRENT' | 'MARKET' | 'HIGH' | 'LOW'>('CURRENT');
|
||||
const [tickOffset, setTickOffset] = useState<number>(0);
|
||||
const [quantityMode, setQuantityMode] = useState<'DIRECT' | 'RATIO'>('DIRECT');
|
||||
const [quantity, setQuantity] = useState<number>(1);
|
||||
const [quantityRatio, setQuantityRatio] = useState<number>(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 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 (
|
||||
<div
|
||||
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'}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{isSelected ? <CheckCircle2 className="text-amber-600" size={18} /> : <Circle className="text-slate-300" size={18} />}
|
||||
<Icon className={isSelected ? 'text-amber-600' : 'text-slate-300'} size={18} />
|
||||
<span className="text-[13px] font-black text-slate-800 uppercase tracking-tight">{label}</span>
|
||||
</div>
|
||||
|
||||
{isSelected && type === 'TRAILING_STOP' && (
|
||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<input type="number" className="w-16 p-2 bg-white rounded-xl text-center font-black text-[14px] border border-amber-200 shadow-inner outline-none" value={tsValue} onChange={(e) => setTsValue(Number(e.target.value))} />
|
||||
<select value={tsUnit} onChange={(e) => setTsUnit(e.target.value as any)} className="bg-transparent font-black text-[12px] text-amber-600 outline-none">
|
||||
<option value="PERCENT">%</option>
|
||||
<option value="TICK">틱</option>
|
||||
<option value="AMOUNT">{currencySymbol}</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSelected && type === 'PROFIT' && (
|
||||
<div className="flex items-center gap-2" onClick={e => e.stopPropagation()}>
|
||||
<input type="number" className="w-20 p-2 bg-white rounded-xl text-center font-black text-[14px] border border-rose-200 shadow-inner outline-none" value={profitValue} onChange={e => setProfitValue(Number(e.target.value))} />
|
||||
<span className="text-[12px] font-black text-rose-500">{monTargetUnit === 'PERCENT' ? '%' : currencySymbol}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSelected && type === 'LOSS' && (
|
||||
<div className="flex items-center gap-2" onClick={e => e.stopPropagation()}>
|
||||
<input type="number" className="w-20 p-2 bg-white rounded-xl text-center font-black text-[14px] border border-blue-200 shadow-inner outline-none" value={lossValue} onChange={e => setLossValue(Number(e.target.value))} />
|
||||
<span className="text-[12px] font-black text-blue-500">{monTargetUnit === 'PERCENT' ? '%' : currencySymbol}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<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="px-6 py-3 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">
|
||||
<RotateCcw size={16} />
|
||||
<span className="text-[11px] font-black uppercase tracking-wider">전체 초기화</span>
|
||||
</button>
|
||||
|
||||
<div className="flex bg-slate-100 p-1 rounded-xl border border-slate-200 shadow-inner">
|
||||
<button onClick={() => setOrderType(OrderType.BUY)} className={`px-10 py-1.5 rounded-lg text-[11px] font-black transition-all ${isBuyMode ? 'bg-rose-500 text-white shadow-md' : 'text-slate-400 hover:text-slate-600'}`}>매수설정</button>
|
||||
<button onClick={() => setOrderType(OrderType.SELL)} className={`px-10 py-1.5 rounded-lg text-[11px] font-black transition-all ${!isBuyMode ? 'bg-blue-600 text-white shadow-md' : 'text-slate-400 hover:text-slate-600'}`}>매도설정</button>
|
||||
</div>
|
||||
|
||||
<button onClick={onClose} className="p-1.5 hover:bg-slate-100 rounded-full transition-colors text-slate-400"><X size={24} /></button>
|
||||
</div>
|
||||
|
||||
<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="flex items-center gap-6">
|
||||
<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="space-y-0.5">
|
||||
<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>
|
||||
<div className="flex items-center gap-2.5 text-[11px] font-black uppercase text-slate-400">
|
||||
<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>
|
||||
</div>
|
||||
<div className="ml-4 border-l border-slate-100 pl-6 space-y-0.5 flex flex-col justify-center">
|
||||
<p className={`text-2xl font-black font-mono tracking-tighter leading-none ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
|
||||
{stock.market === MarketType.DOMESTIC ? stock.price.toLocaleString() : `$${stock.price}`}
|
||||
</p>
|
||||
<div className={`text-[12px] font-black flex items-center gap-1.5 ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
|
||||
{stock.changePercent >= 0 ? <TrendingUp size={14} /> : <TrendingDown size={14} />}
|
||||
{Math.abs(stock.changePercent)}%
|
||||
</div>
|
||||
</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 className="grid grid-cols-2 gap-4 bg-slate-50 p-4 rounded-2xl border border-slate-100 shadow-sm">
|
||||
<div className="space-y-0.5 pr-4 border-r border-slate-200">
|
||||
<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>
|
||||
{holding ? (
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center opacity-20">
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">미보유 종목</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="flex items-center justify-between">
|
||||
<h4 className="text-[14px] font-black text-slate-800 uppercase tracking-tight flex items-center gap-2">
|
||||
<ShieldAlert size={18} className={monitoringEnabled ? 'text-rose-500' : 'text-slate-300'} />
|
||||
1. 자동 감시 전략
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => setMonitoringEnabled(!monitoringEnabled)}
|
||||
className={`flex items-center gap-2 px-4 py-1.5 rounded-full text-[10px] font-black transition-all border shadow-sm ${monitoringEnabled ? 'bg-slate-900 border-slate-900 text-white' : 'bg-white border-slate-200 text-slate-400 hover:border-slate-300'}`}
|
||||
>
|
||||
{monitoringEnabled ? <ToggleRight size={18} /> : <ToggleLeft size={18} />}
|
||||
{monitoringEnabled ? '감시 중' : '비활성'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{!isBuyMode ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between pl-1">
|
||||
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-1.5">
|
||||
<TrendingUp size={14} /> 스마트 수익/손실 관리
|
||||
</label>
|
||||
<div className="flex gap-1 bg-slate-100 p-0.5 rounded-lg border border-slate-200">
|
||||
{['PRICE', 'PERCENT', 'AMOUNT'].map(unit => (
|
||||
<button key={unit} onClick={() => setMonTargetUnit(unit as any)} className={`text-[9px] font-black px-2 py-1 rounded-md transition-all ${monTargetUnit === unit ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-400 hover:text-slate-600'}`}>
|
||||
{unit === 'PRICE' ? '현재가' : unit === 'PERCENT' ? '수익률(%)' : '수익금액'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<StrategyRadio type="PROFIT" label="이익실현 (TAKE PROFIT)" icon={Target} />
|
||||
<StrategyRadio type="LOSS" label="손실제한 (STOP LOSS)" icon={ShieldAlert} />
|
||||
<StrategyRadio type="TRAILING_STOP" label="반등 시 주문 (TRAILING STOP)" icon={PlayCircle} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<StrategyRadio type="TRAILING_STOP" label="반등 시 주문 (TRAILING STOP)" icon={PlayCircle} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TS 내부 감시 시작 조건 설정 (TS 선택 시에만 표시) */}
|
||||
{activeStrategy === 'TRAILING_STOP' && (
|
||||
<div className="mt-3 p-4 rounded-2xl border border-amber-100 bg-amber-50/20 space-y-3 animate-in slide-in-from-top-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div
|
||||
onClick={() => setTriggerEnabled(!triggerEnabled)}
|
||||
className="flex items-center gap-2 cursor-pointer group"
|
||||
>
|
||||
{triggerEnabled ? <CheckSquare size={16} className="text-amber-600" /> : <Square size={16} className="text-slate-300" />}
|
||||
<span className="text-[11px] font-black text-amber-700/80 uppercase tracking-widest">목표가 설정</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{triggerEnabled && (
|
||||
<div className="flex items-center gap-1.5 bg-white/70 p-2 rounded-xl border border-amber-100 shadow-sm overflow-hidden">
|
||||
<select
|
||||
value={triggerType}
|
||||
onChange={(e) => {
|
||||
const newType = e.target.value as any;
|
||||
setTriggerType(newType);
|
||||
applyAutoCondition(newType, isBuyMode);
|
||||
if (newType === 'VOLUME') {
|
||||
setTriggerValue(100000);
|
||||
} else {
|
||||
setTriggerValue(stock.price);
|
||||
}
|
||||
}}
|
||||
className="bg-white p-2 rounded-lg font-black text-[10px] outline-none text-slate-700 w-32 border border-slate-100 shadow-sm shrink-0"
|
||||
>
|
||||
<option value="CURRENT">현재가(\)</option>
|
||||
<option value="HIGH">고가(\)</option>
|
||||
<option value="LOW">저가(\)</option>
|
||||
<option value="VOLUME">거래량(주)</option>
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
className="flex-1 min-w-0 p-2 rounded-lg font-black text-[14px] text-center outline-none bg-white border border-slate-100 focus:border-amber-300 transition-all shadow-sm"
|
||||
value={triggerValue}
|
||||
onChange={(e) => setTriggerValue(Number(e.target.value))}
|
||||
/>
|
||||
<select
|
||||
value={monCondition}
|
||||
onChange={(e) => setMonCondition(e.target.value as any)}
|
||||
className={`p-2 rounded-lg font-black text-[10px] outline-none border border-slate-100 shadow-sm w-16 shrink-0 transition-colors ${monCondition === 'ABOVE' ? 'bg-blue-50 text-blue-600' : 'bg-rose-50 text-rose-500'}`}
|
||||
>
|
||||
<option value="ABOVE">이상</option>
|
||||
<option value="BELOW">이하</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!triggerEnabled && (
|
||||
<p className="text-[10px] font-bold text-amber-600/60 pl-1 italic">※ 조건 미설정 시 현재 시점부터 즉시 반등 감시를 시작합니다.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 pt-4 border-t border-slate-100">
|
||||
<span className="text-[11px] font-black text-slate-400 uppercase tracking-widest whitespace-nowrap">유효기간</span>
|
||||
<div className="flex flex-1 gap-2">
|
||||
{[1, 5, 30].map(d => (
|
||||
<button key={d} onClick={() => setExpiryDays(d)} className={`flex-1 py-2 rounded-xl text-[11px] font-black transition-all ${expiryDays === d ? 'bg-slate-900 text-white shadow-md' : 'bg-slate-100 border border-slate-200 text-slate-400 hover:border-slate-300'}`}>
|
||||
{d}일
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 space-y-6">
|
||||
<h4 className="text-[14px] font-black text-slate-800 uppercase tracking-tight flex items-center gap-2">
|
||||
<Zap size={18} className="text-blue-500" />
|
||||
2. 매매 실행 조건
|
||||
</h4>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-3">
|
||||
<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">
|
||||
{['CURRENT', 'MARKET', 'HIGH', 'LOW'].map((method) => (
|
||||
<button key={method} onClick={() => { setPriceMethod(method as any); if (method === 'MARKET') setTickOffset(0); }} className={`py-2 rounded-xl text-[11px] font-black transition-all border ${priceMethod === method ? 'bg-white text-slate-900 border-slate-200 shadow-md' : 'bg-transparent text-slate-400 border-transparent hover:text-slate-600'}`}>
|
||||
{method === 'CURRENT' ? '현재' : method === 'MARKET' ? '시장' : method === 'HIGH' ? '고가' : '저가'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest pl-1">가격 조정 (틱)</label>
|
||||
<div className="flex items-center gap-4 bg-slate-50 p-1.5 rounded-2xl border border-slate-100">
|
||||
<button disabled={priceMethod === 'MARKET'} onClick={() => setTickOffset(prev => prev - 1)} className="p-2.5 bg-white rounded-xl text-slate-400 hover:text-blue-500 disabled:opacity-20 shadow-sm transition-all"><Minus size={18} /></button>
|
||||
<div className="flex-1 text-center font-black font-mono text-[18px] text-slate-800">{tickOffset > 0 ? `+${tickOffset}` : tickOffset}</div>
|
||||
<button disabled={priceMethod === 'MARKET'} onClick={() => setTickOffset(prev => prev + 1)} className="p-2.5 bg-white rounded-xl text-slate-400 hover:text-blue-500 disabled:opacity-20 shadow-sm transition-all"><Plus size={18} /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between pl-1">
|
||||
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest">주문 수량</label>
|
||||
<span className="text-[9px] font-bold text-blue-500 bg-blue-50 px-2 py-0.5 rounded-full">
|
||||
{isBuyMode ? `예수금` : `보유수량`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-slate-50 p-3 rounded-2xl border border-slate-100 space-y-4">
|
||||
<div className="flex items-center gap-1.5 bg-white/60 p-1 rounded-xl border border-slate-100 shadow-inner">
|
||||
<button onClick={() => setQuantityMode('DIRECT')} className={`flex-1 py-2 rounded-lg text-[10px] font-black transition-all ${quantityMode === 'DIRECT' ? 'bg-white text-slate-900 shadow-sm border border-slate-200' : 'text-slate-400 hover:text-slate-600'}`}>직접입력</button>
|
||||
<button onClick={() => setQuantityMode('RATIO')} className={`flex-1 py-2 rounded-lg text-[10px] font-black transition-all ${quantityMode === 'RATIO' ? 'bg-white text-slate-900 shadow-sm border border-slate-200' : 'text-slate-400 hover:text-slate-600'}`}>비중(%)</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-2 pb-1 relative">
|
||||
{quantityMode === 'DIRECT' ? (
|
||||
<div className="flex items-center w-full gap-2 group">
|
||||
<input type="number" min="1" className="flex-1 bg-transparent text-center font-black text-[24px] outline-none placeholder:text-slate-200 text-slate-900 min-w-0" placeholder="0" value={quantity} onChange={e => setQuantity(Math.max(1, parseInt(e.target.value) || 0))} />
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<button onClick={handleMaxQuantity} className="px-3 py-1 bg-blue-600 text-white rounded-lg font-black text-[9px] uppercase tracking-tighter hover:bg-blue-700 transition-all shadow-md">MAX</button>
|
||||
<div className="px-2 py-1 bg-slate-900 text-white rounded-lg font-black text-[9px] uppercase tracking-wider shrink-0">EA / UNIT</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center w-full gap-2">
|
||||
<input type="number" min="0" max="100" className="flex-1 bg-transparent text-center font-black text-[24px] outline-none text-slate-900 min-w-0" value={quantityRatio} onChange={e => setQuantityRatio(Math.min(100, Math.max(0, parseInt(e.target.value) || 0)))} />
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<button onClick={handleMaxQuantity} className="px-3 py-1 bg-blue-600 text-white rounded-lg font-black text-[9px] uppercase tracking-tighter hover:bg-blue-700 transition-all shadow-md">MAX</button>
|
||||
<div className={`px-2 py-1 text-white rounded-lg font-black text-[9px] uppercase tracking-wider flex items-center gap-1 shrink-0 ${isBuyMode ? 'bg-rose-500' : 'bg-blue-600'}`}>
|
||||
<BadgePercent size={12} /> PERCENT
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-slate-900/5 rounded-2xl border border-slate-100 text-center">
|
||||
<p className="text-[12px] font-black text-slate-600 leading-tight">
|
||||
<span className={`${isBuyMode ? 'text-rose-500' : 'text-blue-600'}`}>{finalQuantity}주</span>를 {isBuyMode ? '매수' : '매도'}합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="px-8 py-6 bg-white border-t border-slate-100 flex items-center justify-between shrink-0 gap-6">
|
||||
{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 flex-col">
|
||||
<span className="text-[10px] font-black text-slate-800 uppercase tracking-tight leading-none">즉시 실행</span>
|
||||
<span className="text-[8px] text-slate-400 font-bold leading-none mt-1">엔진 자동 가동</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setImmediateStart(!immediateStart)}
|
||||
className={`relative inline-flex h-6 w-10 items-center rounded-full transition-all focus:outline-none shadow-inner ${immediateStart ? 'bg-emerald-500' : 'bg-slate-300'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform shadow-md ${immediateStart ? 'translate-x-5' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleExecute}
|
||||
className={`flex-1 py-4 rounded-2xl font-black text-[16px] text-white shadow-xl transition-all active:scale-[0.98] flex items-center justify-center gap-3 ${monitoringEnabled ? (isBuyMode ? 'bg-rose-500' : 'bg-blue-600') : 'bg-slate-900 hover:bg-slate-800'}`}
|
||||
>
|
||||
{monitoringEnabled ? <><Save className="w-5 h-5" /> 전략 저장 및 {immediateStart ? '즉시 활성화' : '대기 저장'}</> : <><Zap size={22} fill="currentColor" /> {isBuyMode ? '매수' : '매도'} 주문 즉시 전송</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradeModal;
|
||||
Reference in New Issue
Block a user