Files
goldenChart/frontend/src/components/virtual/VirtualTargetGrid.tsx
T
2026-06-03 02:04:30 +09:00

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;