전략편집기 수정

This commit is contained in:
Macbook
2026-06-14 23:56:27 +09:00
parent 198ebbb3d7
commit 55a517ba1c
5 changed files with 163 additions and 13 deletions
@@ -10,18 +10,24 @@ import {
} from '../../utils/paletteDragSession';
/**
* Tauri WebView — 드래그 중 전체 화면 overlay 가 pointer 이벤트·drop 을 수신
* Tauri WebView — 드래그 overlay (라벨 위치는 ref 갱신, move 마다 React re-render 없음)
*/
export default function PaletteDragOverlay() {
const [session, setSession] = useState<PaletteDragOverlaySession | null>(null);
const [cursor, setCursor] = useState({ x: 0, y: 0 });
const overlayRef = useRef<HTMLDivElement | null>(null);
const labelRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
registerPaletteDragOverlay({
mount: next => {
setCursor({ x: next.x, y: next.y });
setSession(next);
requestAnimationFrame(() => {
const label = labelRef.current;
if (label) {
label.style.left = `${next.x}px`;
label.style.top = `${next.y}px`;
}
});
},
unmount: () => setSession(null),
bindElement: el => {
@@ -41,7 +47,7 @@ export default function PaletteDragOverlay() {
try {
overlayRef.current.setPointerCapture(session.pointerId);
} catch {
/* body capture fallback */
/* ignore */
}
}, [session]);
@@ -49,7 +55,11 @@ export default function PaletteDragOverlay() {
if (!session || e.pointerId !== session.pointerId) return;
e.preventDefault();
e.stopPropagation();
setCursor({ x: e.clientX, y: e.clientY });
const label = labelRef.current;
if (label) {
label.style.left = `${e.clientX}px`;
label.style.top = `${e.clientY}px`;
}
notifyPaletteDragMove(e.clientX, e.clientY);
}, [session]);
@@ -78,8 +88,9 @@ export default function PaletteDragOverlay() {
onPointerCancel={onPointerCancel}
>
<div
ref={labelRef}
className="se-palette-drag-overlay-label"
style={{ left: cursor.x, top: cursor.y }}
style={{ left: session.x, top: session.y }}
>
{session.payload.label}
</div>
@@ -1281,16 +1281,22 @@ function StrategyEditorCanvasInner({
updatePreviewRef.current = updateDropPreview;
const resolveDropFromClientRef = useRef(resolveDropFromClient);
resolveDropFromClientRef.current = resolveDropFromClient;
const lastPreviewAnchorRef = useRef<string | null>(null);
useEffect(() => {
if (!needsPointerPaletteDrag()) return;
return subscribePaletteDrag((ev: PaletteDragEvent) => {
if (ev.phase === 'move') {
if (!isOverStrategyBuilder(ev.x, ev.y)) {
clearPreviewRef.current();
if (lastPreviewAnchorRef.current != null) {
lastPreviewAnchorRef.current = null;
clearPreviewRef.current();
}
return;
}
const { flowPos, anchorId } = resolveDropFromClientRef.current(ev.x, ev.y);
if (anchorId === lastPreviewAnchorRef.current) return;
lastPreviewAnchorRef.current = anchorId;
if (anchorId) {
updatePreviewRef.current(anchorId, flowPos);
} else {
@@ -1298,6 +1304,9 @@ function StrategyEditorCanvasInner({
}
return;
}
if (ev.phase === 'end' || ev.phase === 'cancel') {
lastPreviewAnchorRef.current = null;
}
if (ev.phase === 'cancel') {
clearPreviewRef.current();
}
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { LogicNode } from '../../utils/strategyTypes';
import {
CondEditor,
@@ -31,6 +31,15 @@ import {
import StartTimeframeCheckboxes from './StartTimeframeCheckboxes';
import LogicGateOpToggle from './LogicGateOpToggle';
import { formatStartCandleTypesLabel } from '../../utils/strategyStartNodes';
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
import { isOverListEditor } from '../../utils/flowScreenProjection';
import {
findPaletteDropKeyAtPoint,
needsPointerPaletteDrag,
setPaletteDragDropHandler,
subscribePaletteDrag,
type PaletteDragPayload,
} from '../../utils/paletteDragSession';
const NODE_COLORS: Record<string, string> = {
AND: '#4caf50',
@@ -134,6 +143,7 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
<div
className={`sp-tree-node${dragOverKey === nodeDropKey ? ' sp-tree-node--over' : ''}`}
style={{ marginLeft: depth > 0 ? 12 : 0 }}
data-palette-drop-key={isLogic ? nodeDropKey : undefined}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
@@ -327,6 +337,7 @@ function StartSectionBlock({
<div
className={`sp-start-body${dragOverKey === sectionDropKey ? ' sp-start-body--over' : ''}`}
data-palette-drop-key={sectionDropKey}
onDragOver={e => {
e.preventDefault();
e.stopPropagation();
@@ -380,8 +391,15 @@ export default function StrategyListEditor({
orphans = [],
}: Props) {
const [dragOverKey, setDragOverKey] = useState<string | null>(null);
const dragOverKeyRef = useRef<string | null>(null);
const sections = collectStartSections(editorState);
const setDragOverKeyThrottled = useCallback((key: string | null) => {
if (dragOverKeyRef.current === key) return;
dragOverKeyRef.current = key;
setDragOverKey(key);
}, []);
const handleAddStart = useCallback(() => {
if (onAddStart) {
onAddStart();
@@ -406,6 +424,92 @@ export default function StrategyListEditor({
}
}, [editorState, onEditorStateChange, onOrphansChange, orphans]);
const resolvePaletteNode = useCallback((
data: { type: string; value: string; label: string; composite?: boolean },
) => {
if (data.type === 'sidewaysFilter') {
return buildSidewaysFilterNode(data.value, def);
}
return makeNode(data.type, data.value, signalTab, def, makeNodeOptionsFromPalette(data));
}, [signalTab, def]);
const applyPaletteDropAtKey = useCallback((
dropKey: string | null,
data: PaletteDragPayload,
) => {
if (!dropKey) return;
if (dropKey === LIST_DROP_KEY) {
if (data.type === 'start') handleAddStart();
return;
}
const rootMatch = dropKey.match(/^([^:]+):__root__$/);
const nodeMatch = dropKey.match(/^([^:]+):([^:]+)$/);
const startId = rootMatch?.[1] ?? nodeMatch?.[1];
const targetId = rootMatch ? null : nodeMatch?.[2];
if (!startId) return;
if (data.type === 'start') {
handleAddStart();
return;
}
const section = collectStartSections(editorState).find(s => s.startId === startId);
if (!section) return;
const newNode = resolvePaletteNode(data);
if (!newNode) return;
const root = section.root;
if (!root) {
handleRootChange(startId, newNode);
return;
}
if (!targetId) {
handleRootChange(startId, mergeAtRoot(root, newNode, data.type === 'operator'));
return;
}
handleRootChange(startId, addChild(root, targetId, newNode));
}, [
editorState,
handleAddStart,
handleRootChange,
resolvePaletteNode,
]);
const applyDropRef = useRef(applyPaletteDropAtKey);
applyDropRef.current = applyPaletteDropAtKey;
useLayoutEffect(() => {
if (!needsPointerPaletteDrag()) return undefined;
setPaletteDragDropHandler(ev => {
if (ev.phase !== 'end') return;
dragOverKeyRef.current = null;
setDragOverKey(null);
if (!isOverListEditor(ev.x, ev.y)) return;
applyDropRef.current(findPaletteDropKeyAtPoint(ev.x, ev.y), ev.payload);
});
return () => setPaletteDragDropHandler(null);
}, []);
useEffect(() => {
if (!needsPointerPaletteDrag()) return;
return subscribePaletteDrag(ev => {
if (ev.phase === 'move') {
if (!isOverListEditor(ev.x, ev.y)) {
setDragOverKeyThrottled(null);
return;
}
setDragOverKeyThrottled(findPaletteDropKeyAtPoint(ev.x, ev.y));
return;
}
if (ev.phase === 'cancel') {
dragOverKeyRef.current = null;
setDragOverKey(null);
}
});
}, [setDragOverKeyThrottled]);
const handleListDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
@@ -429,6 +533,7 @@ export default function StrategyListEditor({
return (
<div
className={`se-list-editor sp-dropzone${dragOverKey === LIST_DROP_KEY ? ' se-list-editor--over' : ''}`}
data-palette-drop-key={LIST_DROP_KEY}
onDragOver={handleListDragOver}
onDragLeave={() => setDragOverKey(null)}
onDrop={handleListDrop}