144 lines
6.3 KiB
TypeScript
144 lines
6.3 KiB
TypeScript
|
|
import React from 'react';
|
|
import { Zap, ShoppingCart, Star, TrendingUp, TrendingDown } from 'lucide-react';
|
|
import { StockItem, MarketType, OrderType } from '../types';
|
|
|
|
interface StockMasterRowProps {
|
|
stock: StockItem;
|
|
rank?: number;
|
|
isWatchlisted?: boolean;
|
|
onClick?: () => void;
|
|
onTrade?: (type: OrderType) => void;
|
|
onToggleWatchlist?: () => void;
|
|
}
|
|
|
|
export const StockMasterRow: React.FC<StockMasterRowProps> = ({
|
|
stock, rank, isWatchlisted, onClick, onTrade, onToggleWatchlist
|
|
}) => {
|
|
const formatValue = (val?: number) => {
|
|
if (val === undefined) return '-';
|
|
return stock.market === MarketType.DOMESTIC ? val.toLocaleString() : '$' + val;
|
|
};
|
|
|
|
const formatVolume = (vol: number) => {
|
|
if (vol >= 1000000) return (vol / 1000000).toFixed(1) + 'M';
|
|
if (vol >= 1000) return (vol / 1000).toFixed(1) + 'K';
|
|
return vol.toString();
|
|
};
|
|
|
|
return (
|
|
<tr
|
|
onClick={onClick}
|
|
className="group cursor-pointer transition-colors hover:bg-blue-50/40 border-b border-slate-50 last:border-0"
|
|
>
|
|
{/* 1. 번호 (순위) */}
|
|
<td className="px-5 py-4 font-mono font-bold text-slate-300 group-hover:text-blue-500 text-[11px]">
|
|
{rank !== undefined ? rank.toString().padStart(2, '0') : '-'}
|
|
</td>
|
|
|
|
{/* 2. 종목 기본 정보 */}
|
|
<td className="px-5 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-slate-600 text-[10px] font-black group-hover:bg-slate-900 group-hover:text-white transition-all">
|
|
{stock.name[0]}
|
|
</div>
|
|
<div className="flex flex-col min-w-0">
|
|
<span className="font-black text-slate-800 text-[13px] truncate tracking-tight">{stock.name}</span>
|
|
<span className="text-[10px] text-slate-400 font-mono font-bold">{stock.code}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
{/* 3. 현재가 및 등락 */}
|
|
<td className="px-5 py-4">
|
|
<div className="flex flex-col items-end">
|
|
<span className="font-mono font-black text-slate-900 text-[13px]">{formatValue(stock.price)}</span>
|
|
<div className={`flex items-center gap-1 text-[10px] font-bold ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
|
|
{stock.changePercent >= 0 ? <TrendingUp size={10} /> : <TrendingDown size={10} />}
|
|
{Math.abs(stock.changePercent)}%
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
{/* 4. 시가 / 고가 / 저가 */}
|
|
<td className="px-5 py-4 text-right">
|
|
<div className="flex flex-col">
|
|
<span className="text-[10px] text-slate-400 font-bold">시: {formatValue(stock.openPrice)}</span>
|
|
<div className="flex gap-2 justify-end text-[10px] font-bold">
|
|
<span className="text-rose-400">고: {formatValue(stock.highPrice)}</span>
|
|
<span className="text-blue-400">저: {formatValue(stock.lowPrice)}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
{/* 5. 거래량 및 거래대금 */}
|
|
<td className="px-5 py-4 text-right">
|
|
<div className="flex flex-col">
|
|
<span className="text-[11px] font-black text-slate-700 font-mono">{formatVolume(stock.volume)}</span>
|
|
{stock.tradingValue && (
|
|
<span className="text-[9px] text-slate-400 font-bold uppercase tracking-tighter">
|
|
{stock.market === MarketType.DOMESTIC ? (stock.tradingValue / 100000000).toFixed(1) + '억' : '$' + (stock.tradingValue / 1000000).toFixed(1) + 'M'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</td>
|
|
|
|
{/* 6. 기업 건강상태 (Fundamental) */}
|
|
<td className="px-5 py-4">
|
|
<div className="flex flex-col gap-1">
|
|
<div className="flex justify-between items-center gap-2">
|
|
<span className="text-[9px] font-black text-slate-400 uppercase">PER/PBR</span>
|
|
<span className="text-[11px] font-mono font-bold text-slate-700">{stock.per || '-'} / {stock.pbr || '-'}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center gap-2">
|
|
<span className="text-[9px] font-black text-slate-400 uppercase">ROE/DY</span>
|
|
<span className="text-[11px] font-mono font-bold text-slate-700">{stock.roe || '-'}% / {stock.dividendYield || '-'}%</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
{/* 7. AI 스코어 (매수/매도) */}
|
|
<td className="px-5 py-4">
|
|
<div className="flex items-center justify-end gap-3">
|
|
<div className="flex flex-col items-center">
|
|
<span className="text-[8px] font-black text-rose-300 uppercase">Buy</span>
|
|
<span className="text-[14px] font-black text-rose-500 font-mono italic">{stock.aiScoreBuy}</span>
|
|
</div>
|
|
<div className="w-[1px] h-6 bg-slate-100" />
|
|
<div className="flex flex-col items-center">
|
|
<span className="text-[8px] font-black text-blue-300 uppercase">Sell</span>
|
|
<span className="text-[14px] font-black text-blue-500 font-mono italic">{stock.aiScoreSell}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
{/* 7. 액션 버튼 */}
|
|
<td className="px-5 py-4 text-right">
|
|
<div className="flex gap-2 justify-end">
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); onToggleWatchlist?.(); }}
|
|
className={`p-2 rounded-xl transition-all ${isWatchlisted ? 'bg-amber-50 text-amber-500' : 'bg-slate-50 text-slate-300 hover:text-amber-500'}`}
|
|
>
|
|
<Star size={14} fill={isWatchlisted ? "currentColor" : "none"} />
|
|
</button>
|
|
|
|
<div className="w-[1px] h-4 bg-slate-200 self-center mx-1" />
|
|
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); onTrade?.(OrderType.BUY); }}
|
|
className="px-3 py-1.5 bg-slate-900 text-white rounded-xl hover:bg-rose-600 transition-all text-[11px] font-black flex items-center gap-1.5"
|
|
>
|
|
<Zap size={10} fill="currentColor" /> 매수
|
|
</button>
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); onTrade?.(OrderType.SELL); }}
|
|
className="px-3 py-1.5 border-2 border-slate-900 text-slate-900 rounded-xl hover:bg-blue-600 hover:border-blue-600 hover:text-white transition-all text-[11px] font-black flex items-center gap-1.5"
|
|
>
|
|
<ShoppingCart size={10} /> 매도
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
};
|