전략편집기 수정

This commit is contained in:
Macbook
2026-05-24 12:25:17 +09:00
parent ea39f7df27
commit 1465cb2255
10 changed files with 714 additions and 83 deletions
@@ -0,0 +1,71 @@
import {
getNodeDimensions,
pickSideFromPoint,
START_NODE_ID,
type HandleSide,
} from './strategyFlowLayout';
export type EdgeDragZone = 'source' | 'middle' | 'target';
/** 연결선 상 클릭/드래그 위치 → 좌(소스) / 중(경로) / 우(타겟) 구간 */
export function getEdgeDragZone(
sourceX: number,
sourceY: number,
targetX: number,
targetY: number,
pointerX: number,
pointerY: number,
): EdgeDragZone {
const dx = targetX - sourceX;
const dy = targetY - sourceY;
const len2 = dx * dx + dy * dy;
if (len2 < 4) return 'middle';
const t = ((pointerX - sourceX) * dx + (pointerY - sourceY) * dy) / len2;
const clamped = Math.max(0, Math.min(1, t));
if (clamped < 1 / 3) return 'source';
if (clamped > 2 / 3) return 'target';
return 'middle';
}
function sideToSourceHandle(side: HandleSide): string {
return `s-${side}`;
}
function sideToTargetHandle(side: HandleSide): string {
return `t-${side}`;
}
/** 드래그 위치에 맞는 소스(출발) 핸들 */
export function pickSourceHandleAtPoint(
nodeId: string,
flowPos: { x: number; y: number },
positions: Map<string, { x: number; y: number }>,
): string {
const pos = positions.get(nodeId) ?? { x: 0, y: 0 };
const dim = getNodeDimensions(nodeId);
return sideToSourceHandle(pickSideFromPoint(flowPos, pos, dim));
}
/** 드래그 위치에 맞는 타겟(도착) 핸들 */
export function pickTargetHandleAtPoint(
nodeId: string,
flowPos: { x: number; y: number },
positions: Map<string, { x: number; y: number }>,
): string {
const pos = positions.get(nodeId) ?? { x: 0, y: 0 };
const dim = getNodeDimensions(nodeId);
return sideToTargetHandle(pickSideFromPoint(flowPos, pos, dim));
}
export function buildPositionsFromNodes(
nodes: { id: string; position: { x: number; y: number } }[],
): Map<string, { x: number; y: number }> {
const map = new Map<string, { x: number; y: number }>();
for (const n of nodes) {
map.set(n.id, { x: n.position.x, y: n.position.y });
}
if (!map.has(START_NODE_ID)) {
map.set(START_NODE_ID, { x: 0, y: 220 });
}
return map;
}
@@ -0,0 +1,69 @@
import type { LogicNode } from './strategyTypes';
import type { EdgeHandleBinding } from './strategyFlowLayout';
export type SignalFlowLayoutSnapshot = {
positions: Record<string, { x: number; y: number }>;
edgeHandles: Record<string, EdgeHandleBinding>;
orphans?: LogicNode[];
};
export type FlowLayoutChangePayload = Omit<SignalFlowLayoutSnapshot, 'orphans'> & {
tab: 'buy' | 'sell';
};
export type StrategyFlowLayoutStore = {
buy: SignalFlowLayoutSnapshot;
sell: SignalFlowLayoutSnapshot;
};
const STORAGE_KEY = 'gc_se_flow_layout_v1';
export function emptySignalFlowLayout(): SignalFlowLayoutSnapshot {
return { positions: {}, edgeHandles: {}, orphans: [] };
}
export function emptyStrategyFlowLayout(): StrategyFlowLayoutStore {
return { buy: emptySignalFlowLayout(), sell: emptySignalFlowLayout() };
}
export function loadStrategyFlowLayout(strategyKey: string): StrategyFlowLayoutStore | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const all = JSON.parse(raw) as Record<string, StrategyFlowLayoutStore>;
return all[strategyKey] ?? null;
} catch {
return null;
}
}
export function saveStrategyFlowLayout(strategyKey: string, layout: StrategyFlowLayoutStore): void {
try {
const raw = localStorage.getItem(STORAGE_KEY);
const all: Record<string, StrategyFlowLayoutStore> = raw ? JSON.parse(raw) : {};
all[strategyKey] = layout;
localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
} catch {
/* ignore quota / private mode */
}
}
export function deleteStrategyFlowLayout(strategyKey: string): void {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return;
const all = JSON.parse(raw) as Record<string, StrategyFlowLayoutStore>;
delete all[strategyKey];
localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
} catch {
/* ignore */
}
}
export function migrateStrategyFlowLayout(fromKey: string, toKey: string): void {
if (fromKey === toKey) return;
const layout = loadStrategyFlowLayout(fromKey);
if (!layout) return;
saveStrategyFlowLayout(toKey, layout);
deleteStrategyFlowLayout(fromKey);
}
+62 -8
View File
@@ -301,7 +301,13 @@ export function buildConnectionFromPositions(
export type EdgeHandleBinding = {
sourceHandle?: string | null;
targetHandle?: string | null;
/** smooth-step 경로 굽힘 중심 (중간 영역 드래그) */
centerX?: number | null;
centerY?: number | null;
manual?: boolean;
manualSource?: boolean;
manualTarget?: boolean;
manualCenter?: boolean;
};
export const FLOW_NODE_W = 200;
@@ -340,21 +346,69 @@ export function pickHandleSides(
: { sourceHandle: 's-top', targetHandle: 't-bottom' };
}
/** 경로 굽힘 중심 기준 — 양쪽 노드에 가장 자연스러운 핸들 */
export function pickHandlesFromCenter(
sourceId: string,
targetId: string,
center: { x: number; y: number },
positions: Map<string, { x: number; y: number }>,
): { sourceHandle: string; targetHandle: string } {
const sourcePos = positions.get(sourceId) ?? { x: 0, y: 0 };
const targetPos = positions.get(targetId) ?? { x: 0, y: 0 };
const sourceDim = getNodeDimensions(sourceId);
const targetDim = getNodeDimensions(targetId);
const sourceSide = pickSideFromPoint(center, sourcePos, sourceDim);
const targetSide = pickSideFromPoint(center, targetPos, targetDim);
return {
sourceHandle: `s-${sourceSide}`,
targetHandle: `t-${targetSide}`,
};
}
export function applyEdgeHandles(
edges: Edge[],
positions: Map<string, { x: number; y: number }>,
saved: Map<string, EdgeHandleBinding>,
): Edge[] {
return edges.map(edge => {
const binding = saved.get(edge.id);
if (binding?.manual && binding.sourceHandle && binding.targetHandle) {
return { ...edge, sourceHandle: binding.sourceHandle, targetHandle: binding.targetHandle };
}
const binding = saved.get(edge.id) ?? {};
const picked = pickHandleSides(edge.source, edge.target, positions);
const sourceHandle = binding?.sourceHandle ?? picked.sourceHandle;
const targetHandle = binding?.targetHandle ?? picked.targetHandle;
saved.set(edge.id, { sourceHandle, targetHandle, manual: binding?.manual });
return { ...edge, sourceHandle, targetHandle };
let sourceHandle = picked.sourceHandle;
let targetHandle = picked.targetHandle;
if (
binding.manualCenter
&& binding.centerX != null
&& binding.centerY != null
) {
const natural = pickHandlesFromCenter(
edge.source,
edge.target,
{ x: binding.centerX, y: binding.centerY },
positions,
);
if (!binding.manualSource) sourceHandle = natural.sourceHandle;
else sourceHandle = binding.sourceHandle ?? picked.sourceHandle;
if (!binding.manualTarget) targetHandle = natural.targetHandle;
else targetHandle = binding.targetHandle ?? picked.targetHandle;
} else {
if (binding.manualSource) sourceHandle = binding.sourceHandle ?? picked.sourceHandle;
if (binding.manualTarget) targetHandle = binding.targetHandle ?? picked.targetHandle;
}
const nextBinding: EdgeHandleBinding = {
...binding,
sourceHandle,
targetHandle,
};
saved.set(edge.id, nextBinding);
return {
...edge,
sourceHandle,
targetHandle,
data: { ...(edge.data as object), binding: nextBinding },
};
});
}