Files
goldenChart/frontend/src/components/paper/PaperTradeHistoryList.tsx
T
2026-05-31 17:34:26 +09:00

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;