Files
goldenChart/frontend/src/components/analysisReport/LiveTradeTimelineViews.tsx
T
2026-06-10 21:13:53 +09:00

188 lines
6.3 KiB
TypeScript

/**
* 분석레포트 — 거래 이력 타임라인 뷰 (목록 / 좌우 분할)
*/
import React from 'react';
import {
LIVE_TIMELINE_KIND_LABEL,
LIVE_TIMELINE_SIDE_LABEL,
type LiveTimelineEvent,
} from '../../utils/liveTradeTimeline';
import { formatSignalPrice } from '../../utils/tradeSignalDisplay';
import { resolveVirtualTargetNames } from '../../utils/virtualTargetNames';
import TimelineBarTimeBox, { eventToBarTimeSec } from './TimelineBarTimeBox';
import TimelineSignalCardShell from './TimelineSignalCardShell';
import type { Timeframe } from '../../types';
export type LiveTimelineViewMode = 'list' | 'split' | 'axis';
export const LIVE_TIMELINE_VIEW_OPTIONS: { id: LiveTimelineViewMode; label: string }[] = [
{ id: 'list', label: '세로 목록' },
{ id: 'split', label: '좌우 분할' },
{ id: 'axis', label: '봉 시간축' },
];
function kindClass(kind: LiveTimelineEvent['kind']): string {
if (kind === 'SIGNAL') return 'signal';
if (kind === 'ORDER_PENDING') return 'pending';
return 'fill';
}
function sideClass(side: LiveTimelineEvent['side']): 'buy' | 'sell' {
return side === 'BUY' ? 'buy' : 'sell';
}
interface EventBodyProps {
ev: LiveTimelineEvent;
compact?: boolean;
}
export const TimelineEventBody: React.FC<EventBodyProps> = ({ ev, compact = false }) => {
const { koreanName } = resolveVirtualTargetNames(ev.symbol);
const kCls = kindClass(ev.kind);
const sCls = sideClass(ev.side);
return (
<>
<div className="arp-timeline-card-tags">
<span className={`arp-timeline-kind arp-timeline-kind--${kCls}`}>
{LIVE_TIMELINE_KIND_LABEL[ev.kind]}
</span>
<span className={`arp-timeline-side arp-timeline-side--${sCls}`}>
{LIVE_TIMELINE_SIDE_LABEL[ev.side]}
</span>
</div>
<div className={`arp-timeline-card-title${compact ? ' arp-timeline-card-title--compact' : ''}`}>
{koreanName || ev.symbol}
<span className="arp-timeline-symbol">{ev.symbol.replace(/^KRW-/, '')}</span>
</div>
<div className="arp-timeline-meta">
{ev.price > 0 && <span>{formatSignalPrice(ev.price)}</span>}
{ev.quantity != null && ev.quantity > 0 && (
<span>{ev.quantity.toLocaleString(undefined, { maximumFractionDigits: 8 })}</span>
)}
{ev.strategyName && <span>{ev.strategyName}</span>}
{ev.sourceLabel && <span>{ev.sourceLabel}</span>}
{ev.detail && <span className="arp-timeline-detail">{ev.detail}</span>}
</div>
</>
);
};
interface ListViewProps {
events: LiveTimelineEvent[];
timeframe: Timeframe;
onSignalDetail?: (ev: LiveTimelineEvent) => void;
}
export const TimelineListView: React.FC<ListViewProps> = ({ events, timeframe, onSignalDetail }) => (
<div className="btd-timeline arp-timeline-scroll arp-timeline-scroll--list" aria-label="거래 이력 타임라인">
<div className="btd-timeline-line" aria-hidden />
{events.map(ev => {
const kCls = kindClass(ev.kind);
const sCls = sideClass(ev.side);
const barTimeSec = eventToBarTimeSec(ev.ts, timeframe);
return (
<article
key={ev.id}
className={[
'btd-timeline-item',
'arp-timeline-item',
'arp-timeline-item--bar-anchor',
`arp-timeline-item--${kCls}`,
`arp-timeline-item--${sCls}`,
].join(' ')}
>
<TimelineBarTimeBox
barTimeSec={barTimeSec}
timeframe={timeframe}
side={sCls}
variant="compact"
className="arp-timeline-bar-time--list-node"
/>
<TimelineSignalCardShell
ev={ev}
className="btd-timeline-card"
onSignalDetail={onSignalDetail}
>
<TimelineEventBody ev={ev} />
</TimelineSignalCardShell>
</article>
);
})}
</div>
);
interface SplitViewProps {
events: LiveTimelineEvent[];
timeframe: Timeframe;
onSignalDetail?: (ev: LiveTimelineEvent) => void;
}
export const TimelineSplitView: React.FC<SplitViewProps> = ({ events, timeframe, onSignalDetail }) => (
<div className="arp-timeline-split-scroll" aria-label="좌우 분할 거래 이력 타임라인">
<div className="arp-timeline-split-track">
<div className="arp-timeline-split-axis" aria-hidden />
{events.map(ev => {
const isBuy = ev.side === 'BUY';
const kCls = kindClass(ev.kind);
const sCls = sideClass(ev.side);
const barTimeSec = eventToBarTimeSec(ev.ts, timeframe);
const cardCls = [
'arp-timeline-split-card',
`arp-timeline-split-card--${sCls}`,
`arp-timeline-split-card--${kCls}`,
isBuy ? 'arp-timeline-split-card--left' : 'arp-timeline-split-card--right',
].join(' ');
const card = (
<TimelineSignalCardShell
ev={ev}
className={cardCls}
onSignalDetail={onSignalDetail}
>
<TimelineEventBody ev={ev} compact />
</TimelineSignalCardShell>
);
return (
<div
key={ev.id}
className={`arp-timeline-split-row arp-timeline-split-row--${sCls}`}
>
<div className="arp-timeline-split-col arp-timeline-split-col--left">
{isBuy ? (
<div className="arp-timeline-split-stack arp-timeline-split-stack--buy">
{card}
<div
className="arp-timeline-split-connector arp-timeline-split-connector--buy"
aria-hidden
/>
</div>
) : null}
</div>
<div className="arp-timeline-split-col arp-timeline-split-col--center">
<TimelineBarTimeBox
barTimeSec={barTimeSec}
timeframe={timeframe}
side={sCls}
variant="compact"
className="arp-timeline-bar-time--split-node"
/>
</div>
<div className="arp-timeline-split-col arp-timeline-split-col--right">
{!isBuy ? (
<div className="arp-timeline-split-stack arp-timeline-split-stack--sell">
<div
className="arp-timeline-split-connector arp-timeline-split-connector--sell"
aria-hidden
/>
{card}
</div>
) : null}
</div>
</div>
);
})}
</div>
</div>
);