197 lines
7.7 KiB
TypeScript
197 lines
7.7 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,
|
|
} from '../../utils/virtualTradingStorage';
|
|
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
|
|
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
|
|
import type { TickerData } from '../../hooks/useMarketTicker';
|
|
import { resolveVirtualTargetStrategyId } from '../../utils/virtualTargetStrategy';
|
|
import {
|
|
getTradeNotificationGridPreset,
|
|
type TradeNotificationGridPresetId,
|
|
} from '../../utils/tradeNotificationGridPresets';
|
|
|
|
interface Props {
|
|
targets: VirtualTargetItem[];
|
|
strategies: StrategyDto[];
|
|
snapshots: Record<string, VirtualIndicatorSnapshot>;
|
|
snapshotLoadingByMarket?: Record<string, boolean>;
|
|
session: Pick<VirtualSessionConfig, 'globalStrategyId' | 'executionType' | 'positionMode' | 'running'>;
|
|
onTargetStrategyChange: (market: string, strategyId: number | null) => void;
|
|
liveStatusByMarket?: Record<string, VirtualLiveStatus>;
|
|
lastTickAtByMarket?: Record<string, number>;
|
|
selectedMarket?: string;
|
|
onSelectMarket?: (market: string) => void;
|
|
theme?: Theme;
|
|
chartRealtimeSource?: ChartRealtimeSource;
|
|
chartCandleAreaPriceLabels?: boolean;
|
|
chartSeriesPriceLabels?: boolean;
|
|
chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions;
|
|
chartLiveReceiveHighlight?: boolean;
|
|
tickers?: Map<string, TickerData>;
|
|
viewMode: VirtualCardViewMode;
|
|
globalDisplayMode: VirtualCardDisplayMode;
|
|
onFocusMarketChange?: (market: string | null) => void;
|
|
gridPreset: TradeNotificationGridPresetId;
|
|
}
|
|
|
|
const VirtualTargetGrid: React.FC<Props> = ({
|
|
targets, strategies, snapshots, snapshotLoadingByMarket = {}, session,
|
|
onTargetStrategyChange,
|
|
liveStatusByMarket = {}, lastTickAtByMarket = {}, selectedMarket,
|
|
onSelectMarket,
|
|
theme = 'dark',
|
|
chartRealtimeSource = 'BACKEND_STOMP',
|
|
chartCandleAreaPriceLabels = true,
|
|
chartSeriesPriceLabels = true,
|
|
chartPaneSeparator,
|
|
chartLiveReceiveHighlight = true,
|
|
tickers,
|
|
viewMode,
|
|
globalDisplayMode,
|
|
onFocusMarketChange,
|
|
gridPreset,
|
|
}) => {
|
|
const gridCols = getTradeNotificationGridPreset(gridPreset).cols;
|
|
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 === resolveVirtualTargetStrategyId(focusTarget, session.globalStrategyId))}
|
|
strategies={strategies}
|
|
strategyId={focusTarget.strategyId}
|
|
globalStrategyId={session.globalStrategyId}
|
|
onStrategyChange={id => onTargetStrategyChange(focusTarget.market, id)}
|
|
snapshot={snapshots[focusTarget.market]}
|
|
signalLoading={snapshotLoadingByMarket[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}
|
|
chartCandleAreaPriceLabels={chartCandleAreaPriceLabels}
|
|
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
|
chartPaneSeparator={chartPaneSeparator}
|
|
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
|
ticker={tickers?.get(focusTarget.market)}
|
|
koreanName={focusTarget.koreanName}
|
|
englishName={focusTarget.englishName}
|
|
/>
|
|
) : (
|
|
<div
|
|
className={[
|
|
'vtd-grid',
|
|
'vtd-grid--layout-preset',
|
|
`vtd-grid--${viewMode}`,
|
|
`vtd-grid--cols-${gridCols}`,
|
|
].join(' ')}
|
|
style={{ ['--vtd-grid-cols' as string]: gridCols }}
|
|
>
|
|
{targets.map(t => {
|
|
const strat = strategies.find(s => s.id === resolveVirtualTargetStrategyId(t, 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)}
|
|
snapshot={snapshots[t.market]}
|
|
signalLoading={snapshotLoadingByMarket[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}
|
|
chartCandleAreaPriceLabels={chartCandleAreaPriceLabels}
|
|
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
|
chartPaneSeparator={chartPaneSeparator}
|
|
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
|
ticker={tickers?.get(t.market)}
|
|
koreanName={t.koreanName}
|
|
englishName={t.englishName}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default VirtualTargetGrid;
|