전략편집기 수정1

This commit is contained in:
Macbook
2026-06-15 01:49:33 +09:00
parent 12c556a95e
commit dbad00aa0f
4 changed files with 135 additions and 32 deletions
@@ -33,9 +33,11 @@ import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorag
import { import {
isPaletteHtmlDrag, isPaletteHtmlDrag,
isPaletteHtmlDragActive, isPaletteHtmlDragActive,
getLastHtmlPaletteDragClientXY,
markPaletteHtmlDragEnd, markPaletteHtmlDragEnd,
needsPointerPaletteDrag, needsPointerPaletteDrag,
readPaletteHtmlDragData, readPaletteHtmlDragData,
setHtmlPaletteDropRouter,
setPaletteDragDropHandler, setPaletteDragDropHandler,
subscribePaletteDrag, subscribePaletteDrag,
type PaletteDragEvent, type PaletteDragEvent,
@@ -1281,6 +1283,15 @@ function StrategyEditorCanvasInner({
const clearPreviewRef = useRef(clearDropPreview); const clearPreviewRef = useRef(clearDropPreview);
clearPreviewRef.current = clearDropPreview; clearPreviewRef.current = clearDropPreview;
useLayoutEffect(() => {
if (needsPointerPaletteDrag()) return undefined;
setHtmlPaletteDropRouter((x, y, payload) => {
clearPreviewRef.current();
applyDropRef.current(payload, x, y);
});
return () => setHtmlPaletteDropRouter(null);
}, []);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!needsPointerPaletteDrag()) return undefined; if (!needsPointerPaletteDrag()) return undefined;
setPaletteDragDropHandler((ev) => { setPaletteDragDropHandler((ev) => {
@@ -1299,6 +1310,37 @@ function StrategyEditorCanvasInner({
const lastPreviewAnchorRef = useRef<string | null>(null); const lastPreviewAnchorRef = useRef<string | null>(null);
const lastPreviewKeyRef = useRef<string | null>(null); const lastPreviewKeyRef = useRef<string | null>(null);
useEffect(() => {
if (needsPointerPaletteDrag()) return undefined;
let rafId = 0;
const tick = () => {
if (!isPaletteHtmlDragActive()) {
rafId = 0;
return;
}
const xy = getLastHtmlPaletteDragClientXY();
if (xy) {
const { flowPos, anchorId } = resolveDropFromClientRef.current(xy.x, xy.y);
if (anchorId) {
updatePreviewRef.current(anchorId, flowPos);
lastPreviewAnchorRef.current = anchorId;
} else if (lastPreviewAnchorRef.current != null) {
lastPreviewAnchorRef.current = null;
clearPreviewRef.current();
}
}
rafId = requestAnimationFrame(tick);
};
const onDragStart = () => {
if (!rafId) rafId = requestAnimationFrame(tick);
};
document.addEventListener('dragstart', onDragStart, true);
return () => {
document.removeEventListener('dragstart', onDragStart, true);
if (rafId) cancelAnimationFrame(rafId);
};
}, []);
useEffect(() => { useEffect(() => {
if (needsPointerPaletteDrag()) return; if (needsPointerPaletteDrag()) return;
const onDocDragOver = (e: DragEvent) => { const onDocDragOver = (e: DragEvent) => {
@@ -32,10 +32,11 @@ import StartTimeframeCheckboxes from './StartTimeframeCheckboxes';
import LogicGateOpToggle from './LogicGateOpToggle'; import LogicGateOpToggle from './LogicGateOpToggle';
import { formatStartCandleTypesLabel } from '../../utils/strategyStartNodes'; import { formatStartCandleTypesLabel } from '../../utils/strategyStartNodes';
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage'; import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
import { isOverListEditor } from '../../utils/flowScreenProjection';
import { import {
findPaletteDropKeyAtPoint, findPaletteDropKeyAtPoint,
needsPointerPaletteDrag, needsPointerPaletteDrag,
readPaletteHtmlDragData,
setHtmlPaletteDropRouter,
setPaletteDragDropHandler, setPaletteDragDropHandler,
subscribePaletteDrag, subscribePaletteDrag,
type PaletteDragPayload, type PaletteDragPayload,
@@ -303,12 +304,9 @@ function StartSectionBlock({
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setDragOverKey(null); setDragOverKey(null);
try { const data = readPaletteHtmlDragData(e);
const data = JSON.parse(e.dataTransfer.getData('application/json')); if (!data) return;
applyDrop(null, data); applyDrop(null, data);
} catch {
/* ignore */
}
}; };
const handleDropOnNode = (targetId: string, data: { type: string; value: string; label: string; composite?: boolean }) => { const handleDropOnNode = (targetId: string, data: { type: string; value: string; label: string; composite?: boolean }) => {
@@ -480,13 +478,22 @@ export default function StrategyListEditor({
const applyDropRef = useRef(applyPaletteDropAtKey); const applyDropRef = useRef(applyPaletteDropAtKey);
applyDropRef.current = applyPaletteDropAtKey; applyDropRef.current = applyPaletteDropAtKey;
useLayoutEffect(() => {
if (needsPointerPaletteDrag()) return undefined;
setHtmlPaletteDropRouter((x, y, payload) => {
dragOverKeyRef.current = null;
setDragOverKey(null);
applyDropRef.current(findPaletteDropKeyAtPoint(x, y), payload);
});
return () => setHtmlPaletteDropRouter(null);
}, []);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!needsPointerPaletteDrag()) return undefined; if (!needsPointerPaletteDrag()) return undefined;
setPaletteDragDropHandler(ev => { setPaletteDragDropHandler(ev => {
if (ev.phase !== 'end') return; if (ev.phase !== 'end') return;
dragOverKeyRef.current = null; dragOverKeyRef.current = null;
setDragOverKey(null); setDragOverKey(null);
if (!isOverListEditor(ev.x, ev.y)) return;
applyDropRef.current(findPaletteDropKeyAtPoint(ev.x, ev.y), ev.payload); applyDropRef.current(findPaletteDropKeyAtPoint(ev.x, ev.y), ev.payload);
}); });
return () => setPaletteDragDropHandler(null); return () => setPaletteDragDropHandler(null);
@@ -495,10 +502,6 @@ export default function StrategyListEditor({
useEffect(() => { useEffect(() => {
return subscribePaletteDrag(ev => { return subscribePaletteDrag(ev => {
if (ev.phase === 'move') { if (ev.phase === 'move') {
if (!isOverListEditor(ev.x, ev.y)) {
setDragOverKeyThrottled(null);
return;
}
setDragOverKeyThrottled(findPaletteDropKeyAtPoint(ev.x, ev.y)); setDragOverKeyThrottled(findPaletteDropKeyAtPoint(ev.x, ev.y));
return; return;
} }
@@ -519,14 +522,9 @@ export default function StrategyListEditor({
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setDragOverKey(null); setDragOverKey(null);
try { const data = readPaletteHtmlDragData(e);
const data = JSON.parse(e.dataTransfer.getData('application/json')); if (!data) return;
if (data.type === 'start') { applyDropRef.current(findPaletteDropKeyAtPoint(e.clientX, e.clientY) ?? LIST_DROP_KEY, data);
handleAddStart();
}
} catch {
/* ignore */
}
}; };
return ( return (
+21 -4
View File
@@ -77,16 +77,33 @@ export function projectScreenToFlowPosition(
); );
} }
/** 목록 방식 편집기 위인지 */ /** 목록 방식 편집기 위인지 (rect hit-test) */
export function isOverListEditor(clientX: number, clientY: number): boolean { export function isOverListEditor(clientX: number, clientY: number): boolean {
for (const el of elementsUnderDragPoint(clientX, clientY)) { const editors = document.querySelectorAll<HTMLElement>('.se-list-editor');
if (el.closest('.se-list-editor')) return true; for (const el of editors) {
const rect = el.getBoundingClientRect();
if (
clientX >= rect.left && clientX <= rect.right
&& clientY >= rect.top && clientY <= rect.bottom
) {
return true;
}
} }
return false; return false;
} }
/** 팔레트 드롭 — 전략 빌더 캔버스 위인지 */ /** 팔레트 드롭 — 전략 빌더 캔버스 위인지 */
export function isOverStrategyBuilder(clientX: number, clientY: number): boolean { export function isOverStrategyBuilder(clientX: number, clientY: number): boolean {
const canvas = document.querySelector<HTMLElement>('.se-canvas-wrap');
if (canvas) {
const rect = canvas.getBoundingClientRect();
if (
clientX >= rect.left && clientX <= rect.right
&& clientY >= rect.top && clientY <= rect.bottom
) {
return true;
}
}
for (const el of elementsUnderDragPoint(clientX, clientY)) { for (const el of elementsUnderDragPoint(clientX, clientY)) {
if (el.closest('.se-palette-panel, .se-side-wrap--right')) continue; if (el.closest('.se-palette-panel, .se-side-wrap--right')) continue;
if (el.closest('.se-canvas-wrap, .react-flow__pane, .react-flow__renderer')) return true; if (el.closest('.se-canvas-wrap, .react-flow__pane, .react-flow__renderer')) return true;
@@ -171,7 +188,7 @@ export function resolvePaletteDropClientPoint(
return { flowPos, anchorId: domId }; return { flowPos, anchorId: domId };
} }
if (getActivePaletteDrag()) { if (getActivePaletteDrag() || isPaletteHtmlDragActive()) {
const flowHit = findAtFlow(flowPos, nodes); const flowHit = findAtFlow(flowPos, nodes);
if (flowHit && isConnectTarget(flowHit.id)) { if (flowHit && isConnectTarget(flowHit.id)) {
return { flowPos, anchorId: flowHit.id }; return { flowPos, anchorId: flowHit.id };
+54 -8
View File
@@ -57,6 +57,23 @@ let lastDragClientXY: { x: number; y: number } | null = null;
let htmlDragMouseTrackUnbind: (() => void) | null = null; let htmlDragMouseTrackUnbind: (() => void) | null = null;
let htmlPaletteDragActive = false; let htmlPaletteDragActive = false;
let lastHtmlDragClientXY: { x: number; y: number } | null = null; let lastHtmlDragClientXY: { x: number; y: number } | null = null;
let activeHtmlDragPayload: PaletteDragPayload | null = null;
type HtmlPaletteDropRouter = (
clientX: number,
clientY: number,
payload: PaletteDragPayload,
) => void;
let htmlDropRouter: HtmlPaletteDropRouter | null = null;
export function setHtmlPaletteDropRouter(router: HtmlPaletteDropRouter | null): void {
htmlDropRouter = router;
}
export function getActiveHtmlDragPayload(): PaletteDragPayload | null {
return activeHtmlDragPayload;
}
function installHtmlPaletteDragGlobals() { function installHtmlPaletteDragGlobals() {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
@@ -84,7 +101,19 @@ function installHtmlPaletteDragGlobals() {
stopHtmlDragMouseTracking(); stopHtmlDragMouseTracking();
htmlPaletteDragActive = false; htmlPaletteDragActive = false;
lastHtmlDragClientXY = null; lastHtmlDragClientXY = null;
activeHtmlDragPayload = null;
}); });
document.addEventListener('drop', (e) => {
if (!htmlPaletteDragActive) return;
e.preventDefault();
e.stopPropagation();
const payload = readPaletteHtmlDragDataFromEvent(e) ?? activeHtmlDragPayload;
if (!payload || !htmlDropRouter) return;
const x = e.clientX !== 0 || e.clientY !== 0 ? e.clientX : (lastHtmlDragClientXY?.x ?? 0);
const y = e.clientX !== 0 || e.clientY !== 0 ? e.clientY : (lastHtmlDragClientXY?.y ?? 0);
htmlDropRouter(x, y, payload);
}, true);
} }
installHtmlPaletteDragGlobals(); installHtmlPaletteDragGlobals();
@@ -388,6 +417,7 @@ export function startHtmlDragMouseTracking(
clientY: number, clientY: number,
payload: PaletteDragPayload, payload: PaletteDragPayload,
): void { ): void {
activeHtmlDragPayload = payload;
stopHtmlDragMouseTracking(); stopHtmlDragMouseTracking();
if (clientX !== 0 || clientY !== 0) { if (clientX !== 0 || clientY !== 0) {
lastHtmlDragClientXY = { x: clientX, y: clientY }; lastHtmlDragClientXY = { x: clientX, y: clientY };
@@ -418,6 +448,18 @@ export function markPaletteHtmlDragEnd(): void {
stopHtmlDragMouseTracking(); stopHtmlDragMouseTracking();
htmlPaletteDragActive = false; htmlPaletteDragActive = false;
lastHtmlDragClientXY = null; lastHtmlDragClientXY = null;
activeHtmlDragPayload = null;
}
function readPaletteHtmlDragDataFromEvent(e: DragEvent): PaletteDragPayload | null {
const raw = e.dataTransfer?.getData('text/plain')
|| e.dataTransfer?.getData('application/json');
if (!raw) return null;
try {
return JSON.parse(raw) as PaletteDragPayload;
} catch {
return null;
}
} }
export function getLastHtmlPaletteDragClientXY(): { x: number; y: number } | null { export function getLastHtmlPaletteDragClientXY(): { x: number; y: number } | null {
@@ -429,6 +471,7 @@ export function isPaletteHtmlDragActive(): boolean {
} }
export function writePaletteHtmlDragData(e: React.DragEvent, payload: PaletteDragPayload): void { export function writePaletteHtmlDragData(e: React.DragEvent, payload: PaletteDragPayload): void {
activeHtmlDragPayload = payload;
markPaletteHtmlDragStart(); markPaletteHtmlDragStart();
const json = JSON.stringify(payload); const json = JSON.stringify(payload);
e.dataTransfer.setData('application/json', json); e.dataTransfer.setData('application/json', json);
@@ -445,17 +488,20 @@ export function isPaletteHtmlDrag(e: React.DragEvent): boolean {
} }
export function readPaletteHtmlDragData(e: React.DragEvent): PaletteDragPayload | null { export function readPaletteHtmlDragData(e: React.DragEvent): PaletteDragPayload | null {
const raw = e.dataTransfer.getData('text/plain') return readPaletteHtmlDragDataFromEvent(e.nativeEvent) ?? activeHtmlDragPayload;
|| e.dataTransfer.getData('application/json');
if (!raw) return null;
try {
return JSON.parse(raw) as PaletteDragPayload;
} catch {
return null;
}
} }
export function findPaletteDropKeyAtPoint(x: number, y: number): string | null { export function findPaletteDropKeyAtPoint(x: number, y: number): string | null {
const hosts = Array.from(document.querySelectorAll<HTMLElement>('[data-palette-drop-key]'));
for (let i = hosts.length - 1; i >= 0; i--) {
const host = hosts[i];
const key = host.dataset.paletteDropKey;
if (!key) continue;
const rect = host.getBoundingClientRect();
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
return key;
}
}
for (const el of elementsUnderDragPoint(x, y)) { for (const el of elementsUnderDragPoint(x, y)) {
const host = el.closest('[data-palette-drop-key]') as HTMLElement | null; const host = el.closest('[data-palette-drop-key]') as HTMLElement | null;
if (host?.dataset.paletteDropKey) return host.dataset.paletteDropKey; if (host?.dataset.paletteDropKey) return host.dataset.paletteDropKey;