/** * 매매 시그널 알림 목록 행 — 종목×전략 실시간 신호 스냅샷 (가상매매 카드 요약형과 동일 소스) */ import { useCallback, useEffect, useRef, useState } from 'react'; import type { StrategyDto } from '../utils/backendApi'; import { extractVirtualConditions } from '../utils/virtualStrategyConditions'; import { normalizeConditionRow } from '../utils/virtualSignalMetrics'; import { resolveNotificationStrategy, normalizeStrategyId, } from '../utils/resolveNotificationStrategy'; import { hydrateStrategyDto, normalizeMarketCode } from '../utils/strategyHydrate'; import { fetchLiveSnapshot, type VirtualIndicatorSnapshot, } from './useVirtualIndicatorSnapshots'; const MAX_EMPTY_RETRIES = 15; function staticFallback( market: string, strategyId: number, strategy: StrategyDto, ): VirtualIndicatorSnapshot | undefined { const hydrated = hydrateStrategyDto(strategy); const baseRows = extractVirtualConditions(hydrated); if (baseRows.length === 0) return undefined; return { market: normalizeMarketCode(market), strategyId, timeframe: [...new Set(baseRows.map(r => r.timeframe))].join(', '), rows: baseRows.map(r => normalizeConditionRow({ ...r, currentValue: null, satisfied: null })), updatedAt: Date.now(), matchRate: 0, }; } export function useTradeNotificationSignalSnapshot( market: string, strategyId: number | null | undefined, strategy: StrategyDto | undefined, strategyName: string | null | undefined, enabled: boolean, prefetchedStrategies?: StrategyDto[], pollMs = 3000, ): { snapshot: VirtualIndicatorSnapshot | undefined; loading: boolean } { const [snapshot, setSnapshot] = useState(); const [loading, setLoading] = useState(false); const emptyRetryRef = useRef(0); const genRef = useRef(0); const refreshInFlightRef = useRef(false); const resolvedStrategyRef = useRef(); const effectiveStrategyIdRef = useRef(null); const normalizedMarket = normalizeMarketCode(market); const refresh = useCallback(async () => { const rawId = normalizeStrategyId(strategyId); if (!enabled || rawId == null) { setSnapshot(undefined); setLoading(false); emptyRetryRef.current = 0; return; } if (refreshInFlightRef.current) return; const gen = ++genRef.current; refreshInFlightRef.current = true; setLoading(true); try { let effectiveStrategy = strategy ? hydrateStrategyDto(strategy) : resolvedStrategyRef.current; let effectiveId = effectiveStrategy?.id ?? effectiveStrategyIdRef.current ?? rawId; const needsResolve = !effectiveStrategy || extractVirtualConditions(effectiveStrategy).length === 0 || effectiveStrategy.id !== rawId; if (needsResolve) { const resolved = await resolveNotificationStrategy( rawId, strategyName, prefetchedStrategies, normalizedMarket, ); if (gen !== genRef.current) return; if (resolved.strategy) { effectiveStrategy = resolved.strategy; resolvedStrategyRef.current = resolved.strategy; } if (resolved.strategyId != null) { effectiveId = resolved.strategyId; effectiveStrategyIdRef.current = resolved.strategyId; } } const pinFirst = emptyRetryRef.current === 0; let snap = await fetchLiveSnapshot( normalizedMarket, effectiveId, effectiveStrategy, pinFirst, { readOnlyPin: true }, ); if (gen !== genRef.current) return; if ((!snap || snap.rows.length === 0) && effectiveStrategy) { snap = staticFallback(normalizedMarket, effectiveId, effectiveStrategy) ?? snap; } if (snap && snap.rows.length > 0) { emptyRetryRef.current = 0; } else { emptyRetryRef.current += 1; } if (gen === genRef.current) { setSnapshot(snap ?? undefined); const stillCollecting = (!snap || snap.rows.length === 0) && emptyRetryRef.current < MAX_EMPTY_RETRIES; setLoading(stillCollecting); } } finally { refreshInFlightRef.current = false; } }, [ market, normalizedMarket, strategyId, strategy, strategyName, enabled, prefetchedStrategies, ]); useEffect(() => { const rawId = normalizeStrategyId(strategyId); if (!enabled || rawId == null) { setSnapshot(undefined); setLoading(false); emptyRetryRef.current = 0; resolvedStrategyRef.current = undefined; effectiveStrategyIdRef.current = null; return; } emptyRetryRef.current = 0; resolvedStrategyRef.current = strategy ? hydrateStrategyDto(strategy) : undefined; effectiveStrategyIdRef.current = strategy?.id ?? rawId; setLoading(true); void refresh(); const id = window.setInterval(() => void refresh(), pollMs); return () => { clearInterval(id); genRef.current += 1; refreshInFlightRef.current = false; }; }, [market, strategyId, strategyName, enabled, pollMs, refresh, strategy?.id]); return { snapshot, loading }; }