Compare commits
3 Commits
f1301de543
...
01acc19401
| Author | SHA1 | Date | |
|---|---|---|---|
| 01acc19401 | |||
| 83491c08b9 | |||
| f25b6f538f |
@@ -16,7 +16,6 @@ COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
|
||||
# 서버 파일 및 빌드 결과물 복사
|
||||
COPY server.js ./
|
||||
COPY --from=build /app/dist ./dist
|
||||
|
||||
# 환경변수 포트 노출
|
||||
|
||||
@@ -11,18 +11,18 @@ interface StatCardProps {
|
||||
}
|
||||
|
||||
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>
|
||||
<div className="bg-white p-4 rounded-2xl shadow-sm border border-slate-100 flex items-center justify-between group hover:border-blue-100 transition-all">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[10px] font-black text-slate-400 mb-1 uppercase tracking-widest truncate">{title}</p>
|
||||
<h4 className="text-[18px] font-black text-slate-900 leading-none truncate">{value}</h4>
|
||||
{change && (
|
||||
<p className={`text-[11px] font-black mt-4 flex items-center gap-2 ${isUp ? 'text-emerald-500' : 'text-rose-500'}`}>
|
||||
<p className={`text-[10px] font-black mt-2 flex items-center gap-1.5 ${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 className="bg-slate-50 p-3 rounded-xl group-hover:bg-blue-50 transition-colors shrink-0 ml-3">
|
||||
{React.cloneElement(icon as React.ReactElement, { size: 18 })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,11 +25,11 @@ export const StockRow: React.FC<StockRowProps> = ({
|
||||
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">
|
||||
<td className="pl-4 py-2 font-mono font-black text-slate-400 group-hover:text-blue-600 transition-colors text-[12px]">
|
||||
{rank}
|
||||
</td>
|
||||
)}
|
||||
<td className="px-4 py-3">
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-3">
|
||||
{onToggleWatchlist && (
|
||||
<button
|
||||
@@ -43,34 +43,34 @@ export const StockRow: React.FC<StockRowProps> = ({
|
||||
{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="font-black text-slate-900 text-[12.5px] tracking-tight group-hover:text-blue-600 leading-tight">{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]">
|
||||
<td className="px-3 py-2 text-right font-mono font-black text-slate-800 text-[12.5px]">
|
||||
{stock.market === MarketType.DOMESTIC ? stock.price.toLocaleString() + '원' : '$' + stock.price}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<td className="px-3 py-2 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>
|
||||
<p className="font-black text-[12.5px] leading-tight">{showPL.pl.toLocaleString()}</p>
|
||||
<p className="text-[9px] font-bold opacity-80 mt-0.5">({showPL.percent.toFixed(2)}%)</p>
|
||||
</div>
|
||||
) : (
|
||||
<span className={`font-black text-[13px] ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
|
||||
<span className={`font-black text-[12.5px] ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
|
||||
{stock.changePercent >= 0 ? '+' : ''}{stock.changePercent}%
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
{showRatioBar && (
|
||||
<td className="px-4 py-3">
|
||||
<td className="px-3 py-2">
|
||||
<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">
|
||||
<div className="flex justify-between w-full text-[7.5px] font-black font-mono opacity-60">
|
||||
<span className="text-rose-500">{stock.buyRatio || 50}</span>
|
||||
<span className="text-blue-500">{stock.sellRatio || 50}</span>
|
||||
</div>
|
||||
@@ -78,10 +78,10 @@ export const StockRow: React.FC<StockRowProps> = ({
|
||||
</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>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex gap-1 justify-end opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button onClick={(e) => { e.stopPropagation(); onTrade(OrderType.BUY); }} className="p-1 px-1.5 bg-rose-50 text-rose-500 rounded-md hover:bg-rose-500 hover:text-white transition-all"><Zap size={10} fill="currentColor" /></button>
|
||||
<button onClick={(e) => { e.stopPropagation(); onTrade(OrderType.SELL); }} className="p-1 px-1.5 bg-blue-50 text-blue-500 rounded-md hover:bg-blue-500 hover:text-white transition-all"><ShoppingCart size={10} /></button>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
|
||||
140
components/StockSearchModal.tsx
Normal file
140
components/StockSearchModal.tsx
Normal 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;
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
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 { DbService, HoldingItem } from '../services/dbService';
|
||||
import StockSearchModal from './StockSearchModal';
|
||||
|
||||
interface TradeModalProps {
|
||||
stock: StockItem;
|
||||
@@ -14,7 +15,9 @@ interface TradeModalProps {
|
||||
|
||||
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 isBuyMode = orderType === OrderType.BUY;
|
||||
|
||||
@@ -25,21 +28,21 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
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);
|
||||
|
||||
const summary = await dbService.getAccountSummary();
|
||||
setBuyingPower(summary.buyingPower);
|
||||
};
|
||||
fetchData();
|
||||
}, [stock.code, dbService, orderType]);
|
||||
}, [localStock.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;
|
||||
const pl = (localStock.price - holding.avgPrice) * holding.quantity;
|
||||
const percent = ((localStock.price - holding.avgPrice) / holding.avgPrice) * 100;
|
||||
return { pl, percent };
|
||||
}, [holding, stock.price]);
|
||||
}, [holding, localStock.price]);
|
||||
|
||||
const [monitoringEnabled, setMonitoringEnabled] = useState(false);
|
||||
const [immediateStart, setImmediateStart] = useState(true);
|
||||
@@ -54,7 +57,7 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
|
||||
// TS 내부 감시 시작 조건 (Trigger)
|
||||
const [triggerEnabled, setTriggerEnabled] = useState(false);
|
||||
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');
|
||||
|
||||
// 자동 조건 결정 로직
|
||||
@@ -80,13 +83,13 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
|
||||
}
|
||||
applyAutoCondition(triggerType, isBuyMode);
|
||||
if (triggerType !== 'VOLUME') {
|
||||
setTriggerValue(stock.price);
|
||||
setTriggerValue(localStock.price);
|
||||
}
|
||||
}, [isBuyMode, stock.price]);
|
||||
}, [isBuyMode, localStock.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 [profitValue, setProfitValue] = useState<number>(localStock.price * 1.1);
|
||||
const [lossValue, setLossValue] = useState<number>(localStock.price * 0.9);
|
||||
|
||||
const [expiryDays, setExpiryDays] = useState<number>(1);
|
||||
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 [quantityRatio, setQuantityRatio] = useState<number>(isBuyMode ? 10 : 100);
|
||||
|
||||
const currencySymbol = stock.market === MarketType.DOMESTIC ? '원' : '$';
|
||||
const currencySymbol = localStock.market === MarketType.DOMESTIC ? '원' : '$';
|
||||
|
||||
const handleMaxQuantity = () => {
|
||||
if (quantityMode === 'RATIO') {
|
||||
setQuantityRatio(100);
|
||||
} else {
|
||||
if (isBuyMode) {
|
||||
const maxQty = Math.floor(buyingPower / stock.price);
|
||||
const maxQty = Math.floor(buyingPower / localStock.price);
|
||||
setQuantity(maxQty > 0 ? maxQty : 1);
|
||||
} else if (holding) {
|
||||
setQuantity(holding.quantity);
|
||||
@@ -120,7 +123,7 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
|
||||
setActiveStrategy(isBuyMode ? 'TRAILING_STOP' : 'NONE');
|
||||
setTriggerEnabled(false);
|
||||
setTriggerType('CURRENT');
|
||||
setTriggerValue(stock.price);
|
||||
setTriggerValue(localStock.price);
|
||||
applyAutoCondition('CURRENT', isBuyMode);
|
||||
setPriceMethod('CURRENT');
|
||||
setTickOffset(0);
|
||||
@@ -133,13 +136,65 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
|
||||
if (quantityMode === 'RATIO') {
|
||||
if (isBuyMode) {
|
||||
const targetBudget = buyingPower * (quantityRatio / 100);
|
||||
return Math.floor(targetBudget / stock.price) || 0;
|
||||
return Math.floor(targetBudget / localStock.price) || 0;
|
||||
} else if (holding) {
|
||||
return Math.floor(holding.quantity * (quantityRatio / 100));
|
||||
}
|
||||
}
|
||||
return quantity;
|
||||
}, [isBuyMode, quantityMode, quantity, quantityRatio, holding, buyingPower, stock.price]);
|
||||
}, [isBuyMode, quantityMode, quantity, quantityRatio, holding, buyingPower, localStock.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) {
|
||||
@@ -148,24 +203,24 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
|
||||
else finalExpiry = new Date(customExpiryDate);
|
||||
|
||||
onExecute({
|
||||
stockCode: stock.code,
|
||||
stockName: stock.name,
|
||||
stockCode: localStock.code,
|
||||
stockName: localStock.name,
|
||||
type: orderType,
|
||||
quantity: finalQuantity,
|
||||
monitoringType: activeStrategy === 'TRAILING_STOP' ? 'TRAILING_STOP' : 'PRICE_TRIGGER',
|
||||
triggerPrice: triggerEnabled ? triggerValue : stock.price,
|
||||
triggerPrice: triggerEnabled ? triggerValue : localStock.price,
|
||||
trailingType: tsUnit === 'PERCENT' ? 'PERCENT' : 'AMOUNT',
|
||||
trailingValue: tsValue,
|
||||
market: stock.market,
|
||||
market: localStock.market,
|
||||
expiryDate: finalExpiry
|
||||
});
|
||||
} else {
|
||||
if (onImmediateOrder) {
|
||||
onImmediateOrder({
|
||||
stockCode: stock.code,
|
||||
stockName: stock.name,
|
||||
stockCode: localStock.code,
|
||||
stockName: localStock.name,
|
||||
type: orderType,
|
||||
price: priceMethod === 'MARKET' ? 0 : stock.price,
|
||||
price: priceMethod === 'MARKET' ? 0 : localStock.price,
|
||||
quantity: finalQuantity
|
||||
});
|
||||
}
|
||||
@@ -178,12 +233,12 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
|
||||
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'}`}
|
||||
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">
|
||||
{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 className="flex items-center gap-2">
|
||||
{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={16} />
|
||||
<span className="text-[12px] font-black text-slate-800 uppercase tracking-tight">{label}</span>
|
||||
</div>
|
||||
|
||||
{isSelected && type === 'TRAILING_STOP' && (
|
||||
@@ -216,10 +271,10 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
|
||||
|
||||
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="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">
|
||||
<RotateCcw size={16} />
|
||||
<span className="text-[11px] font-black uppercase tracking-wider">전체 초기화</span>
|
||||
@@ -235,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="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 className="px-5 py-3 bg-white border-b border-slate-100 flex items-center justify-between gap-4">
|
||||
{/* 1. 종목 정보 */}
|
||||
<div className="flex items-center gap-3 min-w-0 group relative">
|
||||
<button
|
||||
onClick={() => setIsSearchOpen(true)}
|
||||
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"
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Search size={18} />
|
||||
</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)}%
|
||||
<span className="group-hover:opacity-0 transition-opacity">{localStock.name[0]}</span>
|
||||
</button>
|
||||
<div className="min-w-0 cursor-pointer" onClick={() => setIsSearchOpen(true)}>
|
||||
<h3 className="text-[15px] font-black text-slate-900 truncate flex items-center gap-1 leading-tight group-hover:text-blue-600 transition-colors">
|
||||
{localStock.name} <ChevronDown size={14} className="text-slate-300" />
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5 text-[9px] font-black uppercase text-slate-400 mt-0.5">
|
||||
<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>
|
||||
<span className="tracking-widest">{localStock.code}</span>
|
||||
</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>
|
||||
{/* 2. 시세 및 계좌 정보 (가로 나열) */}
|
||||
<div className="flex items-center gap-4 shrink-0">
|
||||
{/* 시세 */}
|
||||
<div className="border-l border-slate-100 pl-4 flex flex-col justify-center">
|
||||
<p className={`text-[17px] font-black font-mono leading-none ${localStock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
|
||||
{localStock.market === MarketType.DOMESTIC ? localStock.price.toLocaleString() : `$${localStock.price}`}
|
||||
</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>
|
||||
{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 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 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 className="py-1 px-2 bg-slate-50 rounded border border-dashed border-slate-200 text-center">
|
||||
<p className="text-[9px] font-black text-slate-300 uppercase tracking-widest">미보유</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>
|
||||
|
||||
<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">
|
||||
<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'} />
|
||||
<h4 className="text-[13px] font-black text-slate-800 uppercase tracking-tight flex items-center gap-2">
|
||||
<ShieldAlert size={16} className={monitoringEnabled ? 'text-rose-500' : 'text-slate-300'} />
|
||||
1. 자동 감시 전략
|
||||
</h4>
|
||||
<button
|
||||
@@ -403,13 +479,13 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
|
||||
</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" />
|
||||
<div className="bg-white p-5 space-y-5">
|
||||
<h4 className="text-[13px] font-black text-slate-800 uppercase tracking-tight flex items-center gap-2">
|
||||
<Zap size={16} className="text-blue-500" />
|
||||
2. 매매 실행 조건
|
||||
</h4>
|
||||
|
||||
<div className="space-y-8">
|
||||
<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="grid grid-cols-4 gap-2 bg-slate-50 p-1 rounded-2xl border border-slate-100">
|
||||
@@ -468,9 +544,9 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
|
||||
</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">
|
||||
<span className={`${isBuyMode ? 'text-rose-500' : 'text-blue-600'}`}>{finalQuantity}주</span>를 {isBuyMode ? '매수' : '매도'}합니다.
|
||||
{summaryMessage}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -479,7 +555,7 @@ const TradeModal: React.FC<TradeModalProps> = ({ stock, type: initialType, onClo
|
||||
</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 && (
|
||||
<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">
|
||||
@@ -502,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 ? '매수' : '매도'} 주문 즉시 전송</>}
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
TrendingUp, Wallet, Activity, Briefcase, PieChart, Database, Zap, Timer, Trash2
|
||||
TrendingUp, TrendingDown, Wallet, Activity, Briefcase, PieChart, Database, Zap, Timer, Trash2, Globe, ArrowUpRight, ArrowDownRight, ChevronRight
|
||||
} from 'lucide-react';
|
||||
import { StockItem, TradeOrder, MarketType, WatchlistGroup, OrderType, AutoTradeConfig, ReservedOrder } from '../types';
|
||||
import { DbService, HoldingItem } from '../services/dbService';
|
||||
@@ -23,6 +23,40 @@ interface DashboardProps {
|
||||
onRefreshHoldings: () => void;
|
||||
}
|
||||
|
||||
const IndexBar = () => {
|
||||
const indices = [
|
||||
{ name: '코스피', value: '2,561.32', change: '+12.45', percent: '0.49%', isUp: true },
|
||||
{ name: '코스닥', value: '842.11', change: '-3.21', percent: '0.38%', isUp: false },
|
||||
{ name: '나스닥', value: '15,628.95', change: '+215.12', percent: '1.40%', isUp: true },
|
||||
{ name: 'S&P 500', value: '4,850.12', change: '+45.23', percent: '0.94%', isUp: true },
|
||||
{ name: 'USD/KRW', value: '1,324.50', change: '+2.10', percent: '0.16%', isUp: true },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 overflow-x-auto pb-2 scrollbar-hide -mx-2 px-2 items-center">
|
||||
{indices.map((idx, i) => (
|
||||
<div key={i} className="flex items-center gap-3 bg-white px-4 py-2 rounded-xl border border-slate-100 shadow-sm shrink-0 hover:border-blue-200 transition-colors cursor-pointer">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-tighter">{idx.name}</span>
|
||||
<span className="text-[14px] font-black text-slate-900 font-mono tracking-tighter">{idx.value}</span>
|
||||
</div>
|
||||
<div className={`flex flex-col items-end ${idx.isUp ? 'text-rose-500' : 'text-blue-600'}`}>
|
||||
<span className="text-[10px] font-bold flex items-center gap-0.5">
|
||||
{idx.isUp ? <ArrowUpRight size={10} /> : <ArrowDownRight size={10} />}
|
||||
{idx.percent}
|
||||
</span>
|
||||
<span className="text-[9px] font-bold opacity-70">{idx.change}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button className="flex items-center gap-1 text-slate-400 hover:text-blue-500 transition-colors shrink-0 px-2">
|
||||
<span className="text-[11px] font-black">더보기</span>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Dashboard: React.FC<DashboardProps> = ({
|
||||
marketMode, watchlistGroups, stocks, reservedOrders, onAddReservedOrder, onDeleteReservedOrder, onRefreshHoldings, orders
|
||||
}) => {
|
||||
@@ -86,29 +120,33 @@ const Dashboard: React.FC<DashboardProps> = ({
|
||||
const selectedGroup = activeMarketGroups.find(g => g.id === activeGroupId) || activeMarketGroups[0];
|
||||
|
||||
return (
|
||||
<div className="space-y-12 animate-in fade-in duration-500 pb-20">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<StatCard title={`총 자산 (${marketMode === MarketType.DOMESTIC ? '원' : '달러'})`} value={`${marketMode === MarketType.DOMESTIC ? '₩' : '$'} ${summary.totalAssets.toLocaleString()}`} change="+4.2%" isUp={true} icon={<Wallet className="text-blue-500" />} />
|
||||
<StatCard title="총 평가손익" value={`${marketMode === MarketType.DOMESTIC ? '₩' : '$'} ${totalLiquidationSummary.totalPL.toLocaleString()}`} change={`${aggregatePLPercent.toFixed(2)}%`} isUp={totalLiquidationSummary.totalPL >= 0} icon={<TrendingUp className={totalLiquidationSummary.totalPL >= 0 ? "text-emerald-500" : "text-rose-500"} />} />
|
||||
<StatCard title="보유 종목수" value={`${holdings.length}개`} change="마켓 필터" isUp={true} icon={<Briefcase className="text-orange-500" />} />
|
||||
<StatCard title="예수금" value={`${marketMode === MarketType.DOMESTIC ? '₩' : '$'} ${summary.buyingPower.toLocaleString()}`} change="인출 가능" isUp={true} icon={<Activity className="text-purple-500" />} />
|
||||
<div className="space-y-6 animate-in fade-in duration-500 pb-20">
|
||||
{/* 1. 지수 및 환율 바 */}
|
||||
<IndexBar />
|
||||
|
||||
{/* 2. 자산 현황 요약 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard title={`총 자산 (${marketMode === MarketType.DOMESTIC ? '원' : '달러'})`} value={`${marketMode === MarketType.DOMESTIC ? '₩' : '$'} ${summary.totalAssets.toLocaleString()}`} change="+4.2%" isUp={true} icon={<Wallet />} />
|
||||
<StatCard title="총 평가손익" value={`${marketMode === MarketType.DOMESTIC ? '₩' : '$'} ${totalLiquidationSummary.totalPL.toLocaleString()}`} change={`${aggregatePLPercent.toFixed(2)}%`} isUp={totalLiquidationSummary.totalPL >= 0} icon={<TrendingUp />} />
|
||||
<StatCard title="보유 종목수" value={`${holdings.length}개`} change="마켓 필터" isUp={true} icon={<Briefcase />} />
|
||||
<StatCard title="예수금" value={`${marketMode === MarketType.DOMESTIC ? '₩' : '$'} ${summary.buyingPower.toLocaleString()}`} change="인출 가능" isUp={true} icon={<Activity />} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10">
|
||||
<div className="bg-white p-10 rounded-[3.5rem] shadow-sm border border-slate-100 flex flex-col h-[800px] lg:col-span-1">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h3 className="text-2xl font-black text-slate-800 flex items-center gap-3 uppercase tracking-tighter">
|
||||
<PieChart size={28} className="text-blue-600" /> 관심 그룹
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[650px] lg:col-span-1">
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<h3 className="text-[15px] font-black text-slate-800 flex items-center gap-2 uppercase tracking-tighter">
|
||||
<PieChart size={20} className="text-blue-600" /> 관심 그룹
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex gap-3 mb-10 overflow-x-auto pb-4 scrollbar-hide">
|
||||
<div className="flex gap-2 mb-6 overflow-x-auto pb-2 scrollbar-hide">
|
||||
{activeMarketGroups.map(group => (
|
||||
<button key={group.id} onClick={() => setActiveGroupId(group.id)} className={`relative px-7 py-3 rounded-full font-black text-[12px] transition-all border-2 whitespace-nowrap ${activeGroupId === group.id ? 'bg-white border-blue-500 text-blue-600 shadow-md' : 'bg-transparent border-slate-100 text-slate-400'}`}>
|
||||
<button key={group.id} onClick={() => setActiveGroupId(group.id)} className={`relative px-4 py-2 rounded-xl font-black text-[11px] transition-all border-2 whitespace-nowrap ${activeGroupId === group.id ? 'bg-white border-blue-500 text-blue-600 shadow-sm' : 'bg-transparent border-slate-50 text-slate-400 hover:border-slate-200'}`}>
|
||||
{group.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto pr-2 scrollbar-hide">
|
||||
<div className="flex-1 overflow-y-auto pr-1 scrollbar-hide">
|
||||
<table className="w-full">
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{selectedGroup?.codes.map(code => stocks.find(s => s.code === code)).filter(s => s?.market === marketMode).map(stock => {
|
||||
@@ -128,21 +166,21 @@ const Dashboard: React.FC<DashboardProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2 space-y-10">
|
||||
<div className="bg-white p-10 rounded-[3.5rem] shadow-sm border border-slate-100 flex flex-col h-[450px]">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h3 className="text-2xl font-black text-slate-800 flex items-center gap-3 tracking-tighter">
|
||||
<Database size={28} className="text-emerald-600" /> 보유 포트폴리오
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[350px]">
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<h3 className="text-[15px] font-black text-slate-800 flex items-center gap-2 tracking-tighter">
|
||||
<Database size={20} className="text-emerald-600" /> 보유 포트폴리오
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto flex-1 scrollbar-hide">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="text-[11px] font-black text-slate-400 uppercase tracking-[0.25em] border-b">
|
||||
<th className="pb-5 px-6">종목</th>
|
||||
<th className="pb-5 px-6 text-right">현재가</th>
|
||||
<th className="pb-5 px-6 text-right">수익금 (%)</th>
|
||||
<th className="pb-5 px-6 text-right">주문</th>
|
||||
<tr className="text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
|
||||
<th className="pb-3 px-3">종목</th>
|
||||
<th className="pb-3 px-3 text-right">현재가</th>
|
||||
<th className="pb-3 px-3 text-right">수익금 (%)</th>
|
||||
<th className="pb-3 px-3 text-right">주문</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
@@ -165,19 +203,19 @@ const Dashboard: React.FC<DashboardProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-10 rounded-[3.5rem] shadow-sm border border-slate-100 flex flex-col h-[350px] overflow-hidden">
|
||||
<h3 className="text-2xl font-black text-slate-800 flex items-center gap-3 mb-8">
|
||||
<Timer size={28} className="text-blue-600" /> 실시간 감시 목록
|
||||
<div className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[274px] overflow-hidden">
|
||||
<h3 className="text-[15px] font-black text-slate-800 flex items-center gap-2 mb-5">
|
||||
<Timer size={20} className="text-blue-600" /> 실시간 감시 목록
|
||||
</h3>
|
||||
<div className="flex-1 overflow-y-auto space-y-4 scrollbar-hide">
|
||||
<div className="flex-1 overflow-y-auto space-y-2 scrollbar-hide">
|
||||
{reservedOrders.filter(o => o.market === marketMode).map(order => (
|
||||
<div key={order.id} className="bg-slate-50 p-6 rounded-[2.5rem] border border-slate-100 flex justify-between items-center group">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className={`p-4 rounded-2xl ${order.type === OrderType.BUY ? 'bg-rose-50 text-rose-500' : 'bg-blue-50 text-blue-600'}`}><Zap size={20} fill="currentColor" /></div>
|
||||
<div><p className="font-black text-lg text-slate-800">{order.stockName}</p></div>
|
||||
<div key={order.id} className="bg-slate-50 p-3 rounded-xl border border-slate-100 flex justify-between items-center group">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${order.type === OrderType.BUY ? 'bg-rose-50 text-rose-500' : 'bg-blue-50 text-blue-600'}`}><Zap size={14} fill="currentColor" /></div>
|
||||
<div><p className="font-black text-[13px] text-slate-800">{order.stockName}</p></div>
|
||||
</div>
|
||||
<button onClick={() => onDeleteReservedOrder(order.id)} className="p-3 bg-white hover:bg-rose-50 rounded-2xl text-slate-300 hover:text-rose-500 transition-all shadow-sm">
|
||||
<Trash2 size={22} />
|
||||
<button onClick={() => onDeleteReservedOrder(order.id)} className="p-2 bg-white hover:bg-rose-50 rounded-lg text-slate-300 hover:text-rose-500 transition-all shadow-sm">
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user