170 lines
7.2 KiB
TypeScript
170 lines
7.2 KiB
TypeScript
import React from 'react';
|
|
import type { PaperTradeDto } from '../../utils/backendApi';
|
|
import type { TickerData } from '../../hooks/useMarketTicker';
|
|
import { resolveVirtualTargetNames } from '../../utils/virtualTargetNames';
|
|
import { fmtKrw } from '../TradeOrderPanel';
|
|
|
|
function coinCode(symbol: string): string {
|
|
return symbol.replace(/^KRW-/, '');
|
|
}
|
|
|
|
function fmtTradeTime(iso: string | null | undefined): string {
|
|
if (!iso) return '—';
|
|
const d = new Date(iso);
|
|
if (Number.isNaN(d.getTime())) return iso.slice(0, 19).replace('T', ' ');
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
return `${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
}
|
|
|
|
function sourceLabel(source: string | null | undefined): string {
|
|
if (source === 'STRATEGY') return '자동';
|
|
return '수동';
|
|
}
|
|
|
|
interface Props {
|
|
trades: PaperTradeDto[];
|
|
strategyNames?: Record<number, string>;
|
|
tickers?: Map<string, TickerData>;
|
|
emptyText?: string;
|
|
className?: string;
|
|
selectedTradeId?: number | null;
|
|
/** 투자관리 — 체결 행 전체 전달(탭 유지·선택 표시) */
|
|
onSelectTrade?: (trade: PaperTradeDto) => void;
|
|
/** 가상투자 등 — 종목만 (기존 동작) */
|
|
onSelectMarket?: (market: string) => void;
|
|
}
|
|
|
|
const PaperTradeHistoryList: React.FC<Props> = ({
|
|
trades,
|
|
strategyNames = {},
|
|
tickers,
|
|
emptyText = '거래 내역이 없습니다.',
|
|
className = '',
|
|
selectedTradeId = null,
|
|
onSelectTrade,
|
|
onSelectMarket,
|
|
}) => (
|
|
<div className={`ptd-trade-history vtd-trade-history${className ? ` ${className}` : ''}`}>
|
|
<section className="vtd-target-section vtd-trade-section">
|
|
<div className="vtd-target-list-head">
|
|
<span>거래 내역</span>
|
|
<span className="vtd-target-count">{trades.length}건</span>
|
|
</div>
|
|
|
|
{trades.length === 0 ? (
|
|
<p className="vtd-muted ptd-trade-history-empty">{emptyText}</p>
|
|
) : (
|
|
<div className="ptd-trade-history-scroll">
|
|
<div className="vtd-target-list vtd-trade-list">
|
|
{trades.map(t => {
|
|
const { koreanName: ko, englishName: en } = resolveVirtualTargetNames(
|
|
t.symbol,
|
|
tickers?.get(t.symbol)?.koreanName,
|
|
);
|
|
const strategyName = t.strategyId != null
|
|
? (strategyNames[t.strategyId] ?? `#${t.strategyId}`)
|
|
: '—';
|
|
const isBuy = t.side === 'BUY';
|
|
const clickable = Boolean(onSelectTrade || onSelectMarket);
|
|
const selected = selectedTradeId != null && selectedTradeId === t.id;
|
|
const handleActivate = () => {
|
|
if (onSelectTrade) onSelectTrade(t);
|
|
else onSelectMarket?.(t.symbol);
|
|
};
|
|
const sideCls = isBuy ? 'vtd-trade-item--buy' : 'vtd-trade-item--sell';
|
|
const colorCls = isBuy ? 'vtd-target-up' : 'vtd-target-dn';
|
|
|
|
return (
|
|
<div
|
|
key={t.id}
|
|
className={[
|
|
'vtd-target-item',
|
|
'vtd-trade-item',
|
|
sideCls,
|
|
clickable ? 'vtd-trade-item--click' : '',
|
|
selected ? 'vtd-trade-item--selected' : '',
|
|
].filter(Boolean).join(' ')}
|
|
role={clickable ? 'button' : undefined}
|
|
tabIndex={clickable ? 0 : undefined}
|
|
aria-pressed={clickable ? selected : undefined}
|
|
onClick={clickable ? handleActivate : undefined}
|
|
onKeyDown={clickable ? e => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
handleActivate();
|
|
}
|
|
} : undefined}
|
|
>
|
|
<div className="vtd-target-item-main">
|
|
<div className="vtd-target-names">
|
|
<span className="vtd-target-ko">{ko}</span>
|
|
<span className="vtd-target-en">{en}</span>
|
|
</div>
|
|
<div className="vtd-target-item-actions">
|
|
<span className={`vtd-trade-side-badge vtd-trade-side-badge--${isBuy ? 'buy' : 'sell'}`}>
|
|
{isBuy ? '매수' : '매도'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="vtd-target-quote vtd-trade-quote" aria-label="체결 정보">
|
|
<div className="vtd-target-quote-left">
|
|
<span className={`vtd-target-arrow vtd-target-arrow--${isBuy ? 'rise' : 'fall'}`}>
|
|
{isBuy ? '▲' : '▼'}
|
|
</span>
|
|
<span className="vtd-target-quote-pair">{coinCode(t.symbol)}/KRW</span>
|
|
</div>
|
|
<div className="vtd-target-quote-right">
|
|
<span className={`vtd-target-price ${colorCls}`}>{fmtKrw(t.price)}</span>
|
|
<span className={`vtd-target-change ${colorCls}`}>{fmtKrw(t.netAmount)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="vtd-target-strategy-field vtd-trade-meta-field">
|
|
<span className="vtd-target-strategy-label">거래구분</span>
|
|
<span
|
|
className={`vtd-trade-meta-value${t.source === 'STRATEGY' ? ' vtd-trade-meta-value--auto' : ''}`}
|
|
>
|
|
{sourceLabel(t.source)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="vtd-target-strategy-field vtd-trade-meta-field">
|
|
<span className="vtd-target-strategy-label">거래시간</span>
|
|
<span className="vtd-trade-meta-value vtd-trade-meta-value--time">
|
|
{fmtTradeTime(t.createdAt)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="vtd-target-strategy-field vtd-trade-meta-field">
|
|
<span className="vtd-target-strategy-label">투자전략</span>
|
|
<span className="vtd-trade-meta-value vtd-trade-meta-value--strategy" title={strategyName}>
|
|
{strategyName}
|
|
</span>
|
|
</div>
|
|
|
|
{!isBuy && t.realizedPnlDelta != null && (
|
|
<div className="vtd-target-strategy-field vtd-trade-meta-field">
|
|
<span className="vtd-target-strategy-label">실현손익</span>
|
|
<span className={`vtd-trade-meta-value ${t.realizedPnlDelta >= 0 ? 'vtd-target-up' : 'vtd-target-dn'}`}>
|
|
{t.realizedPnlDelta >= 0 ? '+' : ''}{fmtKrw(t.realizedPnlDelta)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="vtd-target-strategy-field vtd-trade-meta-field">
|
|
<span className="vtd-target-strategy-label">체결 후 예수금</span>
|
|
<span className="vtd-trade-meta-value">{fmtKrw(t.cashAfter)}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
);
|
|
|
|
export default PaperTradeHistoryList;
|