Files
goldenChart/frontend/src/utils/strategyEditorLayoutStorage.ts
T
2026-05-28 14:44:19 +09:00

110 lines
3.5 KiB
TypeScript

import type { LogicNode } from './strategyTypes';
import type { EdgeHandleBinding } from './strategyFlowLayout';
import { defaultStartMeta, type StartCombineOp, type StartNodeMeta } from './strategyStartNodes';
export type SignalFlowLayoutSnapshot = {
positions: Record<string, { x: number; y: number }>;
edgeHandles: Record<string, EdgeHandleBinding>;
orphans?: LogicNode[];
/** START 노드별 조건 판별 시간봉 */
startMeta?: Record<string, StartNodeMeta>;
/** 기본 START 외 추가 START 노드 ID (순서 유지) */
extraStartIds?: string[];
/** 추가 START에 연결된 서브트리 루트 */
extraRoots?: Record<string, LogicNode | null>;
/** START 2개 이상일 때 분기 결합 (AND | OR) */
startCombineOp?: StartCombineOp;
};
export type FlowLayoutChangePayload = Omit<SignalFlowLayoutSnapshot, 'orphans'> & {
tab: 'buy' | 'sell';
};
export type StrategyFlowLayoutStore = {
buy: SignalFlowLayoutSnapshot;
sell: SignalFlowLayoutSnapshot;
};
/** @deprecated 레거시 localStorage 키 — 신규 저장은 DB flow_layout_json만 사용 */
const LEGACY_STORAGE_KEY = 'gc_se_flow_layout_v1';
export function emptySignalFlowLayout(): SignalFlowLayoutSnapshot {
return {
positions: {},
edgeHandles: {},
orphans: [],
startMeta: defaultStartMeta(),
extraStartIds: [],
extraRoots: {},
};
}
export function emptyStrategyFlowLayout(): StrategyFlowLayoutStore {
return { buy: emptySignalFlowLayout(), sell: emptySignalFlowLayout() };
}
export function normalizeSignalFlowLayout(
snap: SignalFlowLayoutSnapshot | null | undefined,
): SignalFlowLayoutSnapshot {
if (!snap) return emptySignalFlowLayout();
return {
positions: snap.positions ?? {},
edgeHandles: snap.edgeHandles ?? {},
orphans: snap.orphans ?? [],
startMeta: snap.startMeta ?? defaultStartMeta(),
extraStartIds: snap.extraStartIds ?? [],
extraRoots: snap.extraRoots ?? {},
startCombineOp: snap.startCombineOp,
};
}
export function normalizeStrategyFlowLayout(
store: StrategyFlowLayoutStore | null | undefined,
): StrategyFlowLayoutStore | null {
if (!store) return null;
return {
buy: normalizeSignalFlowLayout(store.buy),
sell: normalizeSignalFlowLayout(store.sell),
};
}
export function buildStrategyFlowLayoutStore(
buy: Omit<SignalFlowLayoutSnapshot, 'orphans'>,
sell: Omit<SignalFlowLayoutSnapshot, 'orphans'>,
buyOrphans: LogicNode[],
sellOrphans: LogicNode[],
): StrategyFlowLayoutStore {
return {
buy: { ...buy, orphans: buyOrphans },
sell: { ...sell, orphans: sellOrphans },
};
}
/** DB에 flow layout이 없을 때만 1회 읽기 — 이후 저장은 서버로 이전 */
export function readLegacyFlowLayoutFromLocalStorage(
strategyKey: string,
): StrategyFlowLayoutStore | null {
try {
const raw = localStorage.getItem(LEGACY_STORAGE_KEY);
if (!raw) return null;
const all = JSON.parse(raw) as Record<string, StrategyFlowLayoutStore>;
return normalizeStrategyFlowLayout(all[strategyKey] ?? null);
} catch {
return null;
}
}
export function clearLegacyFlowLayoutFromLocalStorage(strategyKey: string): void {
try {
const raw = localStorage.getItem(LEGACY_STORAGE_KEY);
if (!raw) return;
const all = JSON.parse(raw) as Record<string, StrategyFlowLayoutStore>;
if (!(strategyKey in all)) return;
delete all[strategyKey];
if (Object.keys(all).length === 0) localStorage.removeItem(LEGACY_STORAGE_KEY);
else localStorage.setItem(LEGACY_STORAGE_KEY, JSON.stringify(all));
} catch {
/* ignore */
}
}