알림목록 지표없음 문제 수정
This commit is contained in:
@@ -75,9 +75,9 @@ public class LiveConditionStatusService {
|
|||||||
for (PendingCond p : pending) {
|
for (PendingCond p : pending) {
|
||||||
String tf = LiveStrategyTimeframeService.normalize(p.timeframe());
|
String tf = LiveStrategyTimeframeService.normalize(p.timeframe());
|
||||||
timeframes.add(tf);
|
timeframes.add(tf);
|
||||||
subscriptionManager.ensureMarketPinned(market, tf);
|
subscriptionManager.ensureBarsReadySync(market, tf, 60);
|
||||||
}
|
}
|
||||||
subscriptionManager.ensureMarketPinned(market, "1m");
|
subscriptionManager.ensureBarsReadySync(market, "1m", 60);
|
||||||
|
|
||||||
List<LiveConditionRowDto> rows = new ArrayList<>();
|
List<LiveConditionRowDto> rows = new ArrayList<>();
|
||||||
int met = 0;
|
int met = 0;
|
||||||
@@ -239,6 +239,13 @@ public class LiveConditionStatusService {
|
|||||||
if (seen.add(key)) {
|
if (seen.add(key)) {
|
||||||
out.add(new PendingCond(id, cond, timeframe, side));
|
out.add(new PendingCond(id, cond, timeframe, side));
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// START·구버전 DSL 등 — 자식 노드 재귀 (프론트 extractVirtualConditions 와 동일)
|
||||||
|
JsonNode children = node.path("children");
|
||||||
|
if (children.isArray()) {
|
||||||
|
for (JsonNode c : children) walk(c, timeframe, side, out, seen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import {
|
|||||||
loadStrategy,
|
loadStrategy,
|
||||||
runBacktest,
|
runBacktest,
|
||||||
type PaperSummaryDto,
|
type PaperSummaryDto,
|
||||||
|
type StrategyDto,
|
||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
|
import { prefetchNotificationStrategies } from '../utils/resolveNotificationStrategy';
|
||||||
import { loadAnalysisCandles } from '../utils/analysisChartData';
|
import { loadAnalysisCandles } from '../utils/analysisChartData';
|
||||||
import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
|
import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
|
||||||
import { candleTypeToTimeframe } from '../utils/strategyToChartIndicators';
|
import { candleTypeToTimeframe } from '../utils/strategyToChartIndicators';
|
||||||
@@ -145,6 +147,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
const [gridPreset, setGridPreset] = useState<TradeNotificationGridPresetId>(
|
const [gridPreset, setGridPreset] = useState<TradeNotificationGridPresetId>(
|
||||||
() => loadTradeNotificationGridPreset(),
|
() => loadTradeNotificationGridPreset(),
|
||||||
);
|
);
|
||||||
|
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
|
||||||
const [selectedMarket, setSelectedMarket] = useState(defaultMarket);
|
const [selectedMarket, setSelectedMarket] = useState(defaultMarket);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const [reportOpen, setReportOpen] = useState(false);
|
const [reportOpen, setReportOpen] = useState(false);
|
||||||
@@ -177,6 +180,14 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
void refreshPaperData();
|
void refreshPaperData();
|
||||||
}, [refreshPaperData]);
|
}, [refreshPaperData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
void prefetchNotificationStrategies().then(list => {
|
||||||
|
if (!cancelled) setStrategies(list);
|
||||||
|
});
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
const tradePrice = useMemo(
|
const tradePrice = useMemo(
|
||||||
() => coerceFiniteNumber(tickers?.get(selectedMarket)?.tradePrice),
|
() => coerceFiniteNumber(tickers?.get(selectedMarket)?.tradePrice),
|
||||||
[tickers, selectedMarket],
|
[tickers, selectedMarket],
|
||||||
@@ -433,6 +444,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
theme={theme}
|
theme={theme}
|
||||||
layoutMode={listLayout}
|
layoutMode={listLayout}
|
||||||
itemTag="div"
|
itemTag="div"
|
||||||
|
prefetchedStrategies={strategies}
|
||||||
isSelected={selectedId === item.id}
|
isSelected={selectedId === item.id}
|
||||||
chartsEnabled={isListView}
|
chartsEnabled={isListView}
|
||||||
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
chartLiveReceiveHighlight={chartLiveReceiveHighlight}
|
||||||
@@ -465,6 +477,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
handleReportFromAlert,
|
handleReportFromAlert,
|
||||||
reportLoadingId,
|
reportLoadingId,
|
||||||
tickers,
|
tickers,
|
||||||
|
strategies,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { TickerData } from '../../hooks/useMarketTicker';
|
|||||||
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
|
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
|
||||||
import { useTradeNotificationSignalSnapshot } from '../../hooks/useTradeNotificationSignalSnapshot';
|
import { useTradeNotificationSignalSnapshot } from '../../hooks/useTradeNotificationSignalSnapshot';
|
||||||
import type { StrategyDto } from '../../utils/backendApi';
|
import type { StrategyDto } from '../../utils/backendApi';
|
||||||
import { isStrategyMissingOnServer, loadStrategies, loadStrategy } from '../../utils/backendApi';
|
import { resolveNotificationStrategy } from '../../utils/resolveNotificationStrategy';
|
||||||
import type { TradeNotificationItem } from '../../contexts/TradeNotificationContext';
|
import type { TradeNotificationItem } from '../../contexts/TradeNotificationContext';
|
||||||
import {
|
import {
|
||||||
buildSignalDetailRows,
|
buildSignalDetailRows,
|
||||||
@@ -68,6 +68,8 @@ interface Props {
|
|||||||
reportLoading?: boolean;
|
reportLoading?: boolean;
|
||||||
/** 가상 스크롤 목록에서는 div + role=listitem */
|
/** 가상 스크롤 목록에서는 div + role=listitem */
|
||||||
itemTag?: 'li' | 'div';
|
itemTag?: 'li' | 'div';
|
||||||
|
/** 목록 화면에서 선로드한 전략 목록 (ID·이름 fallback) */
|
||||||
|
prefetchedStrategies?: StrategyDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_INDICATOR_CARDS = 8;
|
const MAX_INDICATOR_CARDS = 8;
|
||||||
@@ -88,6 +90,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
|||||||
onReport,
|
onReport,
|
||||||
reportLoading = false,
|
reportLoading = false,
|
||||||
itemTag = 'li',
|
itemTag = 'li',
|
||||||
|
prefetchedStrategies = [],
|
||||||
}) => {
|
}) => {
|
||||||
useTradeAlertTimeFormat();
|
useTradeAlertTimeFormat();
|
||||||
const isBuy = item.signalType === 'BUY';
|
const isBuy = item.signalType === 'BUY';
|
||||||
@@ -137,17 +140,20 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void loadStrategies().then(list => {
|
setStrategies(prefetchedStrategies);
|
||||||
if (!cancelled) setStrategies(list ?? []);
|
if (!item.strategyId && !item.strategyName?.trim()) {
|
||||||
});
|
|
||||||
if (!item.strategyId || isStrategyMissingOnServer(item.strategyId)) {
|
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}
|
}
|
||||||
void loadStrategy(item.strategyId).then(s => {
|
void resolveNotificationStrategy(
|
||||||
|
item.strategyId,
|
||||||
|
item.strategyName,
|
||||||
|
prefetchedStrategies,
|
||||||
|
).then(({ strategy: s, strategyId: sid }) => {
|
||||||
if (!cancelled && s) setStrategy(s);
|
if (!cancelled && s) setStrategy(s);
|
||||||
|
else if (!cancelled && !s && sid == null) setStrategy(undefined);
|
||||||
});
|
});
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [item.strategyId, panelActive, isListLayout]);
|
}, [item.strategyId, item.strategyName, panelActive, isListLayout, prefetchedStrategies]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExpandedChartKey(null);
|
setExpandedChartKey(null);
|
||||||
@@ -181,7 +187,9 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
|||||||
item.market,
|
item.market,
|
||||||
item.strategyId,
|
item.strategyId,
|
||||||
strategy,
|
strategy,
|
||||||
|
item.strategyName,
|
||||||
signalSnapshotEnabled,
|
signalSnapshotEnabled,
|
||||||
|
prefetchedStrategies,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [chartActivitySeq, setChartActivitySeq] = useState(0);
|
const [chartActivitySeq, setChartActivitySeq] = useState(0);
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ const TradeNotificationSignalSummary: React.FC<Props> = ({
|
|||||||
market,
|
market,
|
||||||
strategyId,
|
strategyId,
|
||||||
strategy,
|
strategy,
|
||||||
|
strategyLabel,
|
||||||
active && snapshotProp === undefined,
|
active && snapshotProp === undefined,
|
||||||
|
strategies,
|
||||||
);
|
);
|
||||||
const snapshot = snapshotProp ?? internal.snapshot;
|
const snapshot = snapshotProp ?? internal.snapshot;
|
||||||
const loading = signalLoadingProp ?? internal.loading;
|
const loading = signalLoadingProp ?? internal.loading;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
playTradeAlertSound,
|
playTradeAlertSound,
|
||||||
type TradeAlertSoundId,
|
type TradeAlertSoundId,
|
||||||
} from '../utils/tradeAlertSound';
|
} from '../utils/tradeAlertSound';
|
||||||
|
import { normalizeStrategyId } from '../utils/resolveNotificationStrategy';
|
||||||
|
|
||||||
import { getUiPreferences, patchUiPreferences } from '../utils/uiPreferencesDb';
|
import { getUiPreferences, patchUiPreferences } from '../utils/uiPreferencesDb';
|
||||||
import { useAppSettings } from '../hooks/useAppSettings';
|
import { useAppSettings } from '../hooks/useAppSettings';
|
||||||
@@ -87,7 +88,7 @@ function dtoToItem(dto: TradeSignalDto, readIds: Set<string>): TradeNotification
|
|||||||
price: dto.price,
|
price: dto.price,
|
||||||
candleTime: dto.candleTime,
|
candleTime: dto.candleTime,
|
||||||
strategyName: dto.strategyName,
|
strategyName: dto.strategyName,
|
||||||
strategyId: dto.strategyId,
|
strategyId: normalizeStrategyId(dto.strategyId) ?? undefined,
|
||||||
executionType: dto.executionType,
|
executionType: dto.executionType,
|
||||||
candleType: dto.candleType,
|
candleType: dto.candleType,
|
||||||
isRead: readIds.has(id),
|
isRead: readIds.has(id),
|
||||||
|
|||||||
@@ -2,14 +2,20 @@
|
|||||||
* 매매 시그널 알림 목록 행 — 종목×전략 실시간 신호 스냅샷 (가상매매 카드 요약형과 동일 소스)
|
* 매매 시그널 알림 목록 행 — 종목×전략 실시간 신호 스냅샷 (가상매매 카드 요약형과 동일 소스)
|
||||||
*/
|
*/
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { isStrategyMissingOnServer, loadStrategy, type StrategyDto } from '../utils/backendApi';
|
import type { StrategyDto } from '../utils/backendApi';
|
||||||
import { extractVirtualConditions } from '../utils/virtualStrategyConditions';
|
import { extractVirtualConditions } from '../utils/virtualStrategyConditions';
|
||||||
import { normalizeConditionRow } from '../utils/virtualSignalMetrics';
|
import { normalizeConditionRow } from '../utils/virtualSignalMetrics';
|
||||||
|
import {
|
||||||
|
resolveNotificationStrategy,
|
||||||
|
normalizeStrategyId,
|
||||||
|
} from '../utils/resolveNotificationStrategy';
|
||||||
import {
|
import {
|
||||||
fetchLiveSnapshot,
|
fetchLiveSnapshot,
|
||||||
type VirtualIndicatorSnapshot,
|
type VirtualIndicatorSnapshot,
|
||||||
} from './useVirtualIndicatorSnapshots';
|
} from './useVirtualIndicatorSnapshots';
|
||||||
|
|
||||||
|
const MAX_EMPTY_RETRIES = 10;
|
||||||
|
|
||||||
function staticFallback(
|
function staticFallback(
|
||||||
market: string,
|
market: string,
|
||||||
strategyId: number,
|
strategyId: number,
|
||||||
@@ -31,7 +37,9 @@ export function useTradeNotificationSignalSnapshot(
|
|||||||
market: string,
|
market: string,
|
||||||
strategyId: number | null | undefined,
|
strategyId: number | null | undefined,
|
||||||
strategy: StrategyDto | undefined,
|
strategy: StrategyDto | undefined,
|
||||||
|
strategyName: string | null | undefined,
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
|
prefetchedStrategies?: StrategyDto[],
|
||||||
pollMs = 3000,
|
pollMs = 3000,
|
||||||
): { snapshot: VirtualIndicatorSnapshot | undefined; loading: boolean } {
|
): { snapshot: VirtualIndicatorSnapshot | undefined; loading: boolean } {
|
||||||
const [snapshot, setSnapshot] = useState<VirtualIndicatorSnapshot | undefined>();
|
const [snapshot, setSnapshot] = useState<VirtualIndicatorSnapshot | undefined>();
|
||||||
@@ -39,13 +47,15 @@ export function useTradeNotificationSignalSnapshot(
|
|||||||
const emptyRetryRef = useRef(0);
|
const emptyRetryRef = useRef(0);
|
||||||
const genRef = useRef(0);
|
const genRef = useRef(0);
|
||||||
const refreshInFlightRef = useRef(false);
|
const refreshInFlightRef = useRef(false);
|
||||||
/** strategy prop 이 undefined 일 때 직접 로드한 전략 캐시 */
|
const resolvedStrategyRef = useRef<StrategyDto | undefined>();
|
||||||
const fallbackStrategyRef = useRef<StrategyDto | null>(null);
|
const effectiveStrategyIdRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
if (!enabled || strategyId == null) {
|
const rawId = normalizeStrategyId(strategyId);
|
||||||
|
if (!enabled || rawId == null) {
|
||||||
setSnapshot(undefined);
|
setSnapshot(undefined);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
emptyRetryRef.current = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (refreshInFlightRef.current) return;
|
if (refreshInFlightRef.current) return;
|
||||||
@@ -55,28 +65,32 @@ export function useTradeNotificationSignalSnapshot(
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let effectiveStrategy = strategy;
|
let effectiveStrategy = strategy ?? resolvedStrategyRef.current;
|
||||||
if (!effectiveStrategy) {
|
let effectiveId = effectiveStrategy?.id ?? effectiveStrategyIdRef.current ?? rawId;
|
||||||
if (fallbackStrategyRef.current) {
|
|
||||||
effectiveStrategy = fallbackStrategyRef.current;
|
if (!effectiveStrategy || effectiveStrategy.id !== rawId) {
|
||||||
} else if (!isStrategyMissingOnServer(strategyId)) {
|
const resolved = await resolveNotificationStrategy(
|
||||||
try {
|
rawId,
|
||||||
const loaded = await loadStrategy(strategyId);
|
strategyName,
|
||||||
if (loaded) {
|
prefetchedStrategies,
|
||||||
fallbackStrategyRef.current = loaded;
|
);
|
||||||
effectiveStrategy = loaded;
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
if (gen !== genRef.current) return;
|
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;
|
const pinFirst = emptyRetryRef.current === 0;
|
||||||
let snap = await fetchLiveSnapshot(market, strategyId, effectiveStrategy, pinFirst);
|
let snap = await fetchLiveSnapshot(market, effectiveId, effectiveStrategy, pinFirst);
|
||||||
if (gen !== genRef.current) return;
|
if (gen !== genRef.current) return;
|
||||||
|
|
||||||
if (!snap && effectiveStrategy) {
|
if (!snap && effectiveStrategy) {
|
||||||
snap = staticFallback(market, strategyId, effectiveStrategy) ?? null;
|
snap = staticFallback(market, effectiveId, effectiveStrategy) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snap && snap.rows.length > 0) {
|
if (snap && snap.rows.length > 0) {
|
||||||
@@ -87,24 +101,28 @@ export function useTradeNotificationSignalSnapshot(
|
|||||||
|
|
||||||
if (gen === genRef.current) {
|
if (gen === genRef.current) {
|
||||||
setSnapshot(snap ?? undefined);
|
setSnapshot(snap ?? undefined);
|
||||||
|
const stillCollecting = (!snap || snap.rows.length === 0)
|
||||||
|
&& emptyRetryRef.current < MAX_EMPTY_RETRIES;
|
||||||
|
setLoading(stillCollecting);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
refreshInFlightRef.current = false;
|
refreshInFlightRef.current = false;
|
||||||
if (gen === genRef.current) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
}
|
}, [market, strategyId, strategy, strategyName, enabled, prefetchedStrategies]);
|
||||||
}, [market, strategyId, strategy, enabled]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled || strategyId == null) {
|
const rawId = normalizeStrategyId(strategyId);
|
||||||
|
if (!enabled || rawId == null) {
|
||||||
setSnapshot(undefined);
|
setSnapshot(undefined);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
emptyRetryRef.current = 0;
|
emptyRetryRef.current = 0;
|
||||||
|
resolvedStrategyRef.current = undefined;
|
||||||
|
effectiveStrategyIdRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emptyRetryRef.current = 0;
|
emptyRetryRef.current = 0;
|
||||||
fallbackStrategyRef.current = null;
|
resolvedStrategyRef.current = strategy;
|
||||||
|
effectiveStrategyIdRef.current = strategy?.id ?? rawId;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
void refresh();
|
void refresh();
|
||||||
const id = window.setInterval(() => void refresh(), pollMs);
|
const id = window.setInterval(() => void refresh(), pollMs);
|
||||||
@@ -113,7 +131,7 @@ export function useTradeNotificationSignalSnapshot(
|
|||||||
genRef.current += 1;
|
genRef.current += 1;
|
||||||
refreshInFlightRef.current = false;
|
refreshInFlightRef.current = false;
|
||||||
};
|
};
|
||||||
}, [market, strategyId, enabled, pollMs, refresh]);
|
}, [market, strategyId, strategyName, enabled, pollMs, refresh, strategy?.id]);
|
||||||
|
|
||||||
return { snapshot, loading };
|
return { snapshot, loading };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
|
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
|
||||||
import { formatIndicatorDisplayLabel } from '../utils/indicatorRegistry';
|
import { formatIndicatorDisplayLabel } from '../utils/indicatorRegistry';
|
||||||
import { coerceFiniteNumber } from '../utils/safeFormat';
|
import { coerceFiniteNumber } from '../utils/safeFormat';
|
||||||
|
import { normalizeStrategyId } from '../utils/resolveNotificationStrategy';
|
||||||
import {
|
import {
|
||||||
extractVirtualConditions,
|
extractVirtualConditions,
|
||||||
type VirtualConditionRow,
|
type VirtualConditionRow,
|
||||||
@@ -111,10 +112,11 @@ function staticSnapshot(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusMatches(status: { market?: string; strategyId?: number }, market: string, strategyId: number): boolean {
|
function statusMatches(status: { market?: string; strategyId?: unknown }, market: string, strategyId: number): boolean {
|
||||||
if (!status?.market || status.market !== market) return false;
|
if (!status?.market || status.market !== market) return false;
|
||||||
const sid = coerceFiniteNumber(status.strategyId);
|
const sid = normalizeStrategyId(status.strategyId);
|
||||||
return sid === strategyId;
|
const expected = normalizeStrategyId(strategyId);
|
||||||
|
return sid != null && expected != null && sid === expected;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 백엔드가 업비트 REST·WS로 캔들 warm-up 후 Ta4j 평가 */
|
/** 백엔드가 업비트 REST·WS로 캔들 warm-up 후 Ta4j 평가 */
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 매매 시그널 알림 — strategyId·전략명으로 StrategyDto 해석
|
||||||
|
* (서버 DB와 알림 이력의 strategyId 불일치 시 전략명 fallback)
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
isStrategyMissingOnServer,
|
||||||
|
loadStrategies,
|
||||||
|
loadStrategy,
|
||||||
|
type StrategyDto,
|
||||||
|
} from './backendApi';
|
||||||
|
import { reconcileStrategyMissingWithValidIds } from './strategyMissingCache';
|
||||||
|
import { coerceFiniteNumber } from './safeFormat';
|
||||||
|
|
||||||
|
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 && now - strategiesCacheAt < CACHE_MS) {
|
||||||
|
return strategiesCache;
|
||||||
|
}
|
||||||
|
const list = await loadStrategies();
|
||||||
|
strategiesCache = list ?? [];
|
||||||
|
strategiesCacheAt = now;
|
||||||
|
reconcileStrategyMissingWithValidIds(
|
||||||
|
strategiesCache.map(s => s.id).filter((id): id is number => id != null && id > 0),
|
||||||
|
);
|
||||||
|
return strategiesCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedNotificationStrategy {
|
||||||
|
strategy: StrategyDto | undefined;
|
||||||
|
/** live-conditions·차트에 사용할 유효 strategyId */
|
||||||
|
strategyId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveNotificationStrategy(
|
||||||
|
strategyId: unknown,
|
||||||
|
strategyName?: string | null,
|
||||||
|
prefetched?: StrategyDto[],
|
||||||
|
): Promise<ResolvedNotificationStrategy> {
|
||||||
|
const normalizedId = normalizeStrategyId(strategyId);
|
||||||
|
const list = prefetched ?? await getStrategiesList();
|
||||||
|
|
||||||
|
if (normalizedId != null && !isStrategyMissingOnServer(normalizedId)) {
|
||||||
|
const fromList = list.find(s => s.id === normalizedId);
|
||||||
|
if (fromList) {
|
||||||
|
return { strategy: fromList, strategyId: normalizedId };
|
||||||
|
}
|
||||||
|
const loaded = await loadStrategy(normalizedId);
|
||||||
|
if (loaded) {
|
||||||
|
return { strategy: loaded, strategyId: normalizedId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = strategyName?.trim();
|
||||||
|
if (name) {
|
||||||
|
const byName = list.find(s => s.name?.trim() === name);
|
||||||
|
if (byName?.id != null && byName.id > 0) {
|
||||||
|
return { strategy: byName, strategyId: byName.id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { strategy: undefined, strategyId: normalizedId };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 알림 목록 진입 시 전략 목록 선로드 */
|
||||||
|
export async function prefetchNotificationStrategies(): Promise<StrategyDto[]> {
|
||||||
|
return getStrategiesList(true);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user