404에러 패치
This commit is contained in:
@@ -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 { loadStrategies, loadStrategy } from '../../utils/backendApi';
|
import { isStrategyMissingOnServer, loadStrategies, loadStrategy } from '../../utils/backendApi';
|
||||||
import type { TradeNotificationItem } from '../../contexts/TradeNotificationContext';
|
import type { TradeNotificationItem } from '../../contexts/TradeNotificationContext';
|
||||||
import {
|
import {
|
||||||
buildSignalDetailRows,
|
buildSignalDetailRows,
|
||||||
@@ -118,7 +118,9 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
|||||||
void loadStrategies().then(list => {
|
void loadStrategies().then(list => {
|
||||||
if (!cancelled) setStrategies(list ?? []);
|
if (!cancelled) setStrategies(list ?? []);
|
||||||
});
|
});
|
||||||
if (!item.strategyId) return () => { cancelled = true; };
|
if (!item.strategyId || isStrategyMissingOnServer(item.strategyId)) {
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}
|
||||||
void loadStrategy(item.strategyId).then(s => {
|
void loadStrategy(item.strategyId).then(s => {
|
||||||
if (!cancelled && s) setStrategy(s);
|
if (!cancelled && s) setStrategy(s);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,9 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
hasRegisteredUser,
|
||||||
loadAppSettings,
|
loadAppSettings,
|
||||||
|
loadStrategies,
|
||||||
saveAppSettings,
|
saveAppSettings,
|
||||||
type AppSettingsDto,
|
type AppSettingsDto,
|
||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
@@ -106,6 +108,13 @@ function ensureLoaded(): Promise<AppSettingsDto> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (hasRegisteredUser()) {
|
||||||
|
try {
|
||||||
|
await loadStrategies();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[useAppSettings] strategies preload failed', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
_loadPromise = null;
|
_loadPromise = null;
|
||||||
if (generation === _loadGeneration) {
|
if (generation === _loadGeneration) {
|
||||||
notifyAppSettingsListeners();
|
notifyAppSettingsListeners();
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { StrategyFlowLayoutStore } from './strategyEditorLayoutStorage';
|
import type { StrategyFlowLayoutStore } from './strategyEditorLayoutStorage';
|
||||||
|
import {
|
||||||
|
isStrategyMissingOnServer,
|
||||||
|
markStrategyMissingFromPath,
|
||||||
|
} from './strategyMissingCache';
|
||||||
|
|
||||||
|
export { isStrategyMissingOnServer } from './strategyMissingCache';
|
||||||
|
|
||||||
/** 백엔드 REST base (Docker 빌드: `/api`, 로컬 Vite: `.env` 또는 localhost:8080) */
|
/** 백엔드 REST base (Docker 빌드: `/api`, 로컬 Vite: `.env` 또는 localhost:8080) */
|
||||||
export const API_BASE = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8080/api';
|
export const API_BASE = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8080/api';
|
||||||
@@ -90,18 +96,6 @@ async function requestOrThrow<T>(path: string, init?: RequestInit): Promise<T> {
|
|||||||
return JSON.parse(text) as T;
|
return JSON.parse(text) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 서버에 없는 전략 ID — 삭제·만료 후 반복 404 방지 (태블릿·Safari 네트워크 로그 완화) */
|
|
||||||
const strategyMissingIds = new Set<number>();
|
|
||||||
|
|
||||||
export function isStrategyMissingOnServer(strategyId: number): boolean {
|
|
||||||
return strategyId > 0 && strategyMissingIds.has(strategyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function markStrategyMissingFromPath(pathOnly: string): void {
|
|
||||||
const m = pathOnly.match(/^\/strategies\/(\d+)(?:\/|$)/);
|
|
||||||
if (m) strategyMissingIds.add(Number(m[1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function request<T>(path: string, init?: RequestInit): Promise<T | null> {
|
async function request<T>(path: string, init?: RequestInit): Promise<T | null> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}${path}`, {
|
const res = await fetch(`${API_BASE}${path}`, {
|
||||||
@@ -1073,15 +1067,22 @@ export interface StrategyDto {
|
|||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 전략 목록 로드 */
|
/** 전략 목록 로드 + localStorage·설정의 stale strategyId 정리 */
|
||||||
export async function loadStrategies(): Promise<StrategyDto[]> {
|
export async function loadStrategies(): Promise<StrategyDto[]> {
|
||||||
if (!hasRegisteredUser()) return [];
|
if (!hasRegisteredUser()) return [];
|
||||||
return (await request<StrategyDto[]>('/strategies')) ?? [];
|
const list = (await request<StrategyDto[]>('/strategies')) ?? [];
|
||||||
|
try {
|
||||||
|
const { applyStrategyReferenceSanitize } = await import('./strategyReferenceSanitize');
|
||||||
|
await applyStrategyReferenceSanitize(list);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[backendApi] strategy reference sanitize failed', e);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 단일 전략 로드 */
|
/** 단일 전략 로드 */
|
||||||
export async function loadStrategy(id: number): Promise<StrategyDto | null> {
|
export async function loadStrategy(id: number): Promise<StrategyDto | null> {
|
||||||
if (!hasRegisteredUser() || id <= 0 || strategyMissingIds.has(id)) return null;
|
if (!hasRegisteredUser() || id <= 0 || isStrategyMissingOnServer(id)) return null;
|
||||||
return request<StrategyDto>(`/strategies/${id}`);
|
return request<StrategyDto>(`/strategies/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1426,7 +1427,7 @@ const repairInflight = new Set<number>();
|
|||||||
|
|
||||||
/** DB DSL TIMEFRAME 래핑 보정 — 전략 없으면 호출 안 함(404 방지) */
|
/** DB DSL TIMEFRAME 래핑 보정 — 전략 없으면 호출 안 함(404 방지) */
|
||||||
export async function repairStrategyTimeframes(strategyId: number): Promise<boolean> {
|
export async function repairStrategyTimeframes(strategyId: number): Promise<boolean> {
|
||||||
if (!hasRegisteredUser() || strategyId <= 0 || strategyMissingIds.has(strategyId)) return false;
|
if (!hasRegisteredUser() || strategyId <= 0 || isStrategyMissingOnServer(strategyId)) return false;
|
||||||
if (repairSkipIds.has(strategyId) || repairInflight.has(strategyId)) return false;
|
if (repairSkipIds.has(strategyId) || repairInflight.has(strategyId)) return false;
|
||||||
|
|
||||||
repairInflight.add(strategyId);
|
repairInflight.add(strategyId);
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* 서버에 없는 전략 ID — 세션 간 404 반복 방지 (sessionStorage)
|
||||||
|
*/
|
||||||
|
const STORAGE_KEY = 'gc_strategy_missing_ids_v1';
|
||||||
|
const MAX_STORED = 64;
|
||||||
|
|
||||||
|
const strategyMissingIds = new Set<number>();
|
||||||
|
|
||||||
|
function persistStrategyMissingCache(): void {
|
||||||
|
try {
|
||||||
|
const arr = [...strategyMissingIds].filter(id => id > 0).slice(-MAX_STORED);
|
||||||
|
if (arr.length === 0) sessionStorage.removeItem(STORAGE_KEY);
|
||||||
|
else sessionStorage.setItem(STORAGE_KEY, JSON.stringify(arr));
|
||||||
|
} catch {
|
||||||
|
/* private mode · 용량 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 앱 기동 시 이전 탭에서 기록한 missing ID 복원 */
|
||||||
|
export function hydrateStrategyMissingCache(): void {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
const parsed = JSON.parse(raw) as unknown;
|
||||||
|
if (!Array.isArray(parsed)) return;
|
||||||
|
for (const n of parsed) {
|
||||||
|
const id = Number(n);
|
||||||
|
if (id > 0) strategyMissingIds.add(id);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
try { sessionStorage.removeItem(STORAGE_KEY); } catch { /* ok */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStrategyMissingOnServer(strategyId: number): boolean {
|
||||||
|
return strategyId > 0 && strategyMissingIds.has(strategyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markStrategyMissingOnServer(strategyId: number): void {
|
||||||
|
if (strategyId <= 0) return;
|
||||||
|
strategyMissingIds.add(strategyId);
|
||||||
|
persistStrategyMissingCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 전략 목록에 다시 나타난 ID 는 missing 에서 제거 */
|
||||||
|
export function reconcileStrategyMissingWithValidIds(validIds: Iterable<number>): void {
|
||||||
|
const valid = new Set(validIds);
|
||||||
|
let changed = false;
|
||||||
|
for (const id of strategyMissingIds) {
|
||||||
|
if (valid.has(id)) {
|
||||||
|
strategyMissingIds.delete(id);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) persistStrategyMissingCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markStrategyMissingFromPath(pathOnly: string): void {
|
||||||
|
const m = pathOnly.match(/^\/strategies\/(\d+)(?:\/|$)/);
|
||||||
|
if (m) markStrategyMissingOnServer(Number(m[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateStrategyMissingCache();
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* 삭제·만료된 전략 ID — localStorage·ui_preferences·app-settings 정리
|
||||||
|
*/
|
||||||
|
import type { StrategyDto, AppSettingsDto } from './backendApi';
|
||||||
|
import { saveAppSettings } from './backendApi';
|
||||||
|
import {
|
||||||
|
markStrategyMissingOnServer,
|
||||||
|
reconcileStrategyMissingWithValidIds,
|
||||||
|
} from './strategyMissingCache';
|
||||||
|
import { getAppSettingsCache } from '../hooks/useAppSettings';
|
||||||
|
import { getUiPreferences, patchUiPreferences } from './uiPreferencesDb';
|
||||||
|
import type { UiPreferences } from '../types/uiPreferences';
|
||||||
|
import type { VirtualSessionConfig, VirtualTargetItem } from './virtualTradingStorage';
|
||||||
|
|
||||||
|
const LEGACY_TARGETS_KEY = 'gc_virtual_targets_v1';
|
||||||
|
|
||||||
|
function validStrategyIdSet(list: StrategyDto[]): Set<number> {
|
||||||
|
return new Set(
|
||||||
|
list.map(s => s.id).filter((id): id is number => id != null && id > 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readLegacyTargets(): VirtualTargetItem[] {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LEGACY_TARGETS_KEY);
|
||||||
|
if (!raw) return [];
|
||||||
|
const parsed = JSON.parse(raw) as VirtualTargetItem[];
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLegacyTargets(items: VirtualTargetItem[]): void {
|
||||||
|
try {
|
||||||
|
if (items.length === 0) localStorage.removeItem(LEGACY_TARGETS_KEY);
|
||||||
|
else localStorage.setItem(LEGACY_TARGETS_KEY, JSON.stringify(items));
|
||||||
|
} catch {
|
||||||
|
/* ok */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeLegacyTargets(validIds: Set<number>): boolean {
|
||||||
|
const legacy = readLegacyTargets();
|
||||||
|
if (legacy.length === 0) return false;
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
const next = legacy.map(t => {
|
||||||
|
if (t.strategyId != null && t.strategyId > 0 && !validIds.has(t.strategyId)) {
|
||||||
|
markStrategyMissingOnServer(t.strategyId);
|
||||||
|
changed = true;
|
||||||
|
return { ...t, strategyId: null };
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changed) writeLegacyTargets(next);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVirtualPatch(
|
||||||
|
validIds: Set<number>,
|
||||||
|
): Partial<NonNullable<UiPreferences['virtual']>> | null {
|
||||||
|
const ui = getUiPreferences();
|
||||||
|
const virtual = ui.virtual;
|
||||||
|
if (!virtual) return null;
|
||||||
|
|
||||||
|
const patch: Partial<NonNullable<UiPreferences['virtual']>> = {};
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
const session = virtual.session;
|
||||||
|
const gid = session?.globalStrategyId;
|
||||||
|
if (session && gid != null && gid > 0 && !validIds.has(gid)) {
|
||||||
|
markStrategyMissingOnServer(gid);
|
||||||
|
const nextSession: VirtualSessionConfig = {
|
||||||
|
globalStrategyId: null,
|
||||||
|
executionType: session.executionType === 'REALTIME_TICK' ? 'REALTIME_TICK' : 'CANDLE_CLOSE',
|
||||||
|
positionMode: session.positionMode === 'SIGNAL_ONLY' ? 'SIGNAL_ONLY' : 'LONG_ONLY',
|
||||||
|
running: session.running === true,
|
||||||
|
};
|
||||||
|
patch.session = nextSession;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = virtual.targets;
|
||||||
|
if (targets?.length) {
|
||||||
|
let targetsChanged = false;
|
||||||
|
const nextTargets = targets.map(t => {
|
||||||
|
if (t.strategyId != null && t.strategyId > 0 && !validIds.has(t.strategyId)) {
|
||||||
|
markStrategyMissingOnServer(t.strategyId);
|
||||||
|
targetsChanged = true;
|
||||||
|
return { ...t, strategyId: null };
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
if (targetsChanged) {
|
||||||
|
patch.targets = nextTargets;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed ? patch : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAppSettingsPatch(validIds: Set<number>): AppSettingsDto | null {
|
||||||
|
const cache = getAppSettingsCache();
|
||||||
|
const sid = cache.liveStrategyId;
|
||||||
|
if (sid == null || sid <= 0 || validIds.has(sid)) return null;
|
||||||
|
|
||||||
|
markStrategyMissingOnServer(sid);
|
||||||
|
return {
|
||||||
|
liveStrategyId: null,
|
||||||
|
liveStrategyCheck: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전략 목록 로드 직후 호출 — stale ID 제거·missing 캐시 동기화
|
||||||
|
*/
|
||||||
|
export async function applyStrategyReferenceSanitize(strategies: StrategyDto[]): Promise<void> {
|
||||||
|
const validIds = validStrategyIdSet(strategies);
|
||||||
|
reconcileStrategyMissingWithValidIds(validIds);
|
||||||
|
|
||||||
|
const legacyChanged = sanitizeLegacyTargets(validIds);
|
||||||
|
const virtualPatch = buildVirtualPatch(validIds);
|
||||||
|
const appPatch = buildAppSettingsPatch(validIds);
|
||||||
|
|
||||||
|
if (virtualPatch) {
|
||||||
|
patchUiPreferences({ virtual: virtualPatch }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appPatch) {
|
||||||
|
try {
|
||||||
|
const saved = await saveAppSettings(appPatch);
|
||||||
|
if (saved) Object.assign(getAppSettingsCache(), saved);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[strategySanitize] app-settings 저장 실패:', e);
|
||||||
|
Object.assign(getAppSettingsCache(), appPatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyChanged || virtualPatch || appPatch) {
|
||||||
|
console.info('[strategySanitize] 삭제된 전략 ID 참조를 정리했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user