173 lines
6.8 KiB
TypeScript
173 lines
6.8 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import VirtualTargetCard, { type VirtualCardDisplayMode } from './VirtualTargetCard';
|
|
import VirtualTargetFocusView from './VirtualTargetFocusView';
|
|
import type { StrategyDto } from '../../utils/backendApi';
|
|
import type { Theme } from '../../types';
|
|
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
|
import {
|
|
type VirtualTargetItem,
|
|
type VirtualCardViewMode,
|
|
type VirtualSessionConfig,
|
|
resolveTargetCandleType,
|
|
} from '../../utils/virtualTradingStorage';
|
|
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
|
|
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
|
|
import type { TickerData } from '../../hooks/useMarketTicker';
|
|
|
|
interface Props {
|
|
targets: VirtualTargetItem[];
|
|
strategies: StrategyDto[];
|
|
snapshots: Record<string, VirtualIndicatorSnapshot>;
|
|
session: Pick<VirtualSessionConfig, 'globalStrategyId' | 'executionType' | 'positionMode' | 'running'>;
|
|
onTargetStrategyChange: (market: string, strategyId: number | null) => void;
|
|
onTargetCandleTypeChange: (market: string, candleType: string) => void;
|
|
liveStatusByMarket?: Record<string, VirtualLiveStatus>;
|
|
lastTickAtByMarket?: Record<string, number>;
|
|
selectedMarket?: string;
|
|
onSelectMarket?: (market: string) => void;
|
|
theme?: Theme;
|
|
chartRealtimeSource?: ChartRealtimeSource;
|
|
chartSeriesPriceLabels?: boolean;
|
|
chartLiveReceiveHighlight?: boolean;
|
|
tickers?: Map<string, TickerData>;
|
|
viewMode: VirtualCardViewMode;
|
|
globalDisplayMode: VirtualCardDisplayMode;
|
|
onFocusMarketChange?: (market: string | null) => void;
|
|
}
|
|
|
|
const VirtualTargetGrid: React.FC<Props> = ({
|
|
targets, strategies, snapshots, session,
|
|
onTargetStrategyChange,
|
|
onTargetCandleTypeChange,
|
|
liveStatusByMarket = {}, lastTickAtByMarket = {}, selectedMarket,
|
|
onSelectMarket,
|
|
theme = 'dark',
|
|
chartRealtimeSource = 'BACKEND_STOMP',
|
|
chartSeriesPriceLabels = true,
|
|
chartLiveReceiveHighlight = true,
|
|
tickers,
|
|
viewMode,
|
|
globalDisplayMode,
|
|
onFocusMarketChange,
|
|
}) => {
|
|
const [cardDisplayOverrides, setCardDisplayOverrides] = useState<Record<string, VirtualCardDisplayMode>>({});
|
|
const [focusMarket, setFocusMarket] = useState<string | null>(null);
|
|
|
|
const activeMarkets = useMemo(() => new Set(targets.map(t => t.market)), [targets]);
|
|
|
|
useEffect(() => {
|
|
if (focusMarket && !activeMarkets.has(focusMarket)) {
|
|
setFocusMarket(null);
|
|
onFocusMarketChange?.(null);
|
|
}
|
|
}, [activeMarkets, focusMarket, onFocusMarketChange]);
|
|
|
|
useEffect(() => {
|
|
onFocusMarketChange?.(focusMarket);
|
|
}, [focusMarket, onFocusMarketChange]);
|
|
|
|
useEffect(() => {
|
|
setCardDisplayOverrides(prev => {
|
|
const next: Record<string, VirtualCardDisplayMode> = {};
|
|
let changed = false;
|
|
for (const [market, mode] of Object.entries(prev)) {
|
|
if (activeMarkets.has(market)) next[market] = mode;
|
|
else changed = true;
|
|
}
|
|
return changed ? next : prev;
|
|
});
|
|
}, [activeMarkets]);
|
|
|
|
useEffect(() => {
|
|
setCardDisplayOverrides({});
|
|
}, [globalDisplayMode]);
|
|
|
|
const handleCardDisplayMode = useCallback((market: string, mode: VirtualCardDisplayMode) => {
|
|
setCardDisplayOverrides(prev => ({ ...prev, [market]: mode }));
|
|
}, []);
|
|
|
|
const resolveCardDisplayMode = useCallback(
|
|
(market: string) => cardDisplayOverrides[market] ?? globalDisplayMode,
|
|
[cardDisplayOverrides, globalDisplayMode],
|
|
);
|
|
|
|
const focusTarget = useMemo(
|
|
() => (focusMarket ? targets.find(t => t.market === focusMarket) : undefined),
|
|
[focusMarket, targets],
|
|
);
|
|
|
|
if (targets.length === 0) {
|
|
return (
|
|
<div className="vtd-grid-wrap">
|
|
<div className="vtd-grid-empty">
|
|
<p className="vtd-muted">좌측에서 투자대상 종목을 추가하면 카드가 표시됩니다.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`vtd-grid-wrap${focusMarket ? ' vtd-grid-wrap--focus' : ''}`}>
|
|
{focusMarket && focusTarget ? (
|
|
<VirtualTargetFocusView
|
|
market={focusTarget.market}
|
|
strategy={strategies.find(s => s.id === (focusTarget.strategyId ?? session.globalStrategyId))}
|
|
strategies={strategies}
|
|
strategyId={focusTarget.strategyId}
|
|
globalStrategyId={session.globalStrategyId}
|
|
onStrategyChange={id => onTargetStrategyChange(focusTarget.market, id)}
|
|
candleType={resolveTargetCandleType(focusTarget, snapshots[focusTarget.market]?.timeframe)}
|
|
onCandleTypeChange={ct => onTargetCandleTypeChange(focusTarget.market, ct)}
|
|
snapshot={snapshots[focusTarget.market]}
|
|
running={session.running}
|
|
liveStatus={liveStatusByMarket[focusTarget.market] ?? (session.running ? 'connecting' : 'idle')}
|
|
lastTickAt={lastTickAtByMarket[focusTarget.market]}
|
|
viewMode={viewMode}
|
|
onExit={() => setFocusMarket(null)}
|
|
theme={theme}
|
|
chartRealtimeSource={chartRealtimeSource}
|
|
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
|
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
|
ticker={tickers?.get(focusTarget.market)}
|
|
/>
|
|
) : (
|
|
<div className={`vtd-grid vtd-grid--${viewMode}`}>
|
|
{targets.map(t => {
|
|
const strat = strategies.find(s => s.id === (t.strategyId ?? session.globalStrategyId));
|
|
return (
|
|
<VirtualTargetCard
|
|
key={t.market}
|
|
market={t.market}
|
|
strategy={strat}
|
|
strategies={strategies}
|
|
strategyId={t.strategyId}
|
|
globalStrategyId={session.globalStrategyId}
|
|
onStrategyChange={id => onTargetStrategyChange(t.market, id)}
|
|
candleType={resolveTargetCandleType(t, snapshots[t.market]?.timeframe)}
|
|
onCandleTypeChange={ct => onTargetCandleTypeChange(t.market, ct)}
|
|
snapshot={snapshots[t.market]}
|
|
running={session.running}
|
|
liveStatus={liveStatusByMarket[t.market] ?? (session.running ? 'connecting' : 'idle')}
|
|
lastTickAt={lastTickAtByMarket[t.market]}
|
|
viewMode={viewMode}
|
|
displayMode={resolveCardDisplayMode(t.market)}
|
|
onDisplayModeChange={mode => handleCardDisplayMode(t.market, mode)}
|
|
onEnterFocus={() => setFocusMarket(t.market)}
|
|
selected={t.market === selectedMarket}
|
|
onSelect={onSelectMarket ? () => onSelectMarket(t.market) : undefined}
|
|
theme={theme}
|
|
chartRealtimeSource={chartRealtimeSource}
|
|
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
|
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
|
ticker={tickers?.get(t.market)}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default VirtualTargetGrid;
|