Files
goldenChart/frontend/src/utils/resolveNotificationStrategy.ts
T
2026-06-11 01:54:14 +09:00

144 lines
5.1 KiB
TypeScript

/**
* 매매 시그널 알림 — 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<StrategyDto[]> {
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<ResolvedNotificationStrategy> {
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<StrategyDto[]> {
return getStrategiesList(true);
}