/** * 매매 시그널 알림 — strategyId·전략명·종목별 live 설정으로 StrategyDto 해석 */ import { loadLiveStrategySettings, loadStrategies, loadStrategyForNotification, type StrategyDto, } from './backendApi'; import { isStrategyMissingOnServer, reconcileStrategyMissingWithValidIds, unmarkStrategyMissingOnServer, } from './strategyMissingCache'; import { coerceFiniteNumber } from './safeFormat'; import { hydrateStrategyDto, normalizeMarketCode } from './strategyHydrate'; import { extractVirtualConditions } from './virtualStrategyConditions'; let strategiesCache: StrategyDto[] | null = null; let strategiesCacheAt = 0; const CACHE_MS = 30_000; export function normalizeStrategyId(id: unknown): number | null { const n = coerceFiniteNumber(id); return n != null && n > 0 ? Math.trunc(n) : null; } async function getStrategiesList(force = false): Promise { const now = Date.now(); // 빈 배열은 캐시하지 않는다 — 백엔드 미준비로 빈 결과가 반환된 경우 재시도 허용 if (!force && strategiesCache && strategiesCache.length > 0 && now - strategiesCacheAt < CACHE_MS) { return strategiesCache; } const list = await loadStrategies(); const result = list ?? []; if (result.length > 0) { strategiesCache = result; strategiesCacheAt = now; reconcileStrategyMissingWithValidIds( result.map(s => s.id).filter((id): id is number => id != null && id > 0), ); } return result; } function findStrategyByName(list: StrategyDto[], name: string): StrategyDto | undefined { const trimmed = name.trim(); if (!trimmed) return undefined; const exact = list.find(s => s.name?.trim() === trimmed); if (exact) return exact; const lower = trimmed.toLowerCase(); return list.find(s => { const n = s.name?.trim().toLowerCase(); return n === lower || n?.includes(lower) || lower.includes(n ?? ''); }); } function pickStrategy(dto: StrategyDto, strategyId: number): ResolvedNotificationStrategy { const hydrated = hydrateStrategyDto(dto); unmarkStrategyMissingOnServer(strategyId); return { strategy: hydrated, strategyId }; } function hasExtractableConditions(strategy: StrategyDto): boolean { return extractVirtualConditions(hydrateStrategyDto(strategy)).length > 0; } export interface ResolvedNotificationStrategy { strategy: StrategyDto | undefined; /** live-conditions·차트에 사용할 유효 strategyId */ strategyId: number | null; } export async function resolveNotificationStrategy( strategyId: unknown, strategyName?: string | null, prefetched?: StrategyDto[], market?: string | null, ): Promise { const normalizedId = normalizeStrategyId(strategyId); // prefetched가 빈 배열이면 직접 API로 재시도 (백엔드 미준비 시 prefetch가 빈 채로 캐시될 수 있음) const list = (prefetched != null && prefetched.length > 0) ? prefetched : await getStrategiesList(); console.debug('[resolveStrategy] id=%s name=%s listSize=%d list=%s', normalizedId, strategyName, list.length, list.slice(0, 5).map(s => `#${s.id}:${s.name?.substring(0, 20)}`).join(' | ')); if (normalizedId != null) { const fromList = list.find(s => s.id === normalizedId); if (fromList?.id != null && hasExtractableConditions(fromList)) { return pickStrategy(fromList, normalizedId); } if (!isStrategyMissingOnServer(normalizedId)) { const loaded = await loadStrategyForNotification(normalizedId); if (loaded && extractVirtualConditions(loaded).length > 0) { return { strategy: loaded, strategyId: normalizedId }; } if (loaded) { return { strategy: loaded, strategyId: normalizedId }; } } } const name = strategyName?.trim(); if (name) { const byName = findStrategyByName(list, name); if (byName?.id != null && byName.id > 0) { if (hasExtractableConditions(byName)) { return pickStrategy(byName, byName.id); } if (!isStrategyMissingOnServer(byName.id)) { const loaded = await loadStrategyForNotification(byName.id); if (loaded) { return { strategy: loaded, strategyId: byName.id }; } } } } const normalizedMarket = market ? normalizeMarketCode(market) : null; if (normalizedMarket) { try { const live = await loadLiveStrategySettings(normalizedMarket); const liveId = normalizeStrategyId(live?.strategyId); console.debug('[resolveStrategy] liveSettings %s → strategyId=%s', normalizedMarket, liveId); if (liveId != null) { const loaded = await loadStrategyForNotification(liveId); if (loaded) { return { strategy: loaded, strategyId: liveId }; } } } catch (e) { console.debug('[resolveStrategy] liveSettings error:', e); } } console.warn('[resolveStrategy] 전략 찾기 실패 id=%s name=%s market=%s listSize=%d', normalizedId, strategyName, market, list.length); return { strategy: undefined, strategyId: normalizedId }; } /** 알림 목록 진입 시 전략 목록 선로드 */ export async function prefetchNotificationStrategies(): Promise { return getStrategiesList(true); }