From dbad00aa0f999c36fac6cb56ad9008208b37ffa0 Mon Sep 17 00:00:00 2001 From: Macbook Date: Mon, 15 Jun 2026 01:49:33 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=ED=8E=B8=EC=A7=91=EA=B8=B0?= =?UTF-8?q?=20=EC=88=98=EC=A0=951?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../strategyEditor/StrategyEditorCanvas.tsx | 42 +++++++++++++ .../strategyEditor/StrategyListEditor.tsx | 38 ++++++------ frontend/src/utils/flowScreenProjection.ts | 25 ++++++-- frontend/src/utils/paletteDragSession.ts | 62 ++++++++++++++++--- 4 files changed, 135 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx index 1373f55..83f9594 100644 --- a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx +++ b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx @@ -33,9 +33,11 @@ import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorag import { isPaletteHtmlDrag, isPaletteHtmlDragActive, + getLastHtmlPaletteDragClientXY, markPaletteHtmlDragEnd, needsPointerPaletteDrag, readPaletteHtmlDragData, + setHtmlPaletteDropRouter, setPaletteDragDropHandler, subscribePaletteDrag, type PaletteDragEvent, @@ -1281,6 +1283,15 @@ function StrategyEditorCanvasInner({ const clearPreviewRef = useRef(clearDropPreview); clearPreviewRef.current = clearDropPreview; + useLayoutEffect(() => { + if (needsPointerPaletteDrag()) return undefined; + setHtmlPaletteDropRouter((x, y, payload) => { + clearPreviewRef.current(); + applyDropRef.current(payload, x, y); + }); + return () => setHtmlPaletteDropRouter(null); + }, []); + useLayoutEffect(() => { if (!needsPointerPaletteDrag()) return undefined; setPaletteDragDropHandler((ev) => { @@ -1299,6 +1310,37 @@ function StrategyEditorCanvasInner({ const lastPreviewAnchorRef = useRef(null); const lastPreviewKeyRef = useRef(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(() => { if (needsPointerPaletteDrag()) return; const onDocDragOver = (e: DragEvent) => { diff --git a/frontend/src/components/strategyEditor/StrategyListEditor.tsx b/frontend/src/components/strategyEditor/StrategyListEditor.tsx index fda05d0..2d1ea53 100644 --- a/frontend/src/components/strategyEditor/StrategyListEditor.tsx +++ b/frontend/src/components/strategyEditor/StrategyListEditor.tsx @@ -32,10 +32,11 @@ 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, + readPaletteHtmlDragData, + setHtmlPaletteDropRouter, setPaletteDragDropHandler, subscribePaletteDrag, type PaletteDragPayload, @@ -303,12 +304,9 @@ function StartSectionBlock({ e.preventDefault(); e.stopPropagation(); setDragOverKey(null); - try { - const data = JSON.parse(e.dataTransfer.getData('application/json')); - applyDrop(null, data); - } catch { - /* ignore */ - } + const data = readPaletteHtmlDragData(e); + if (!data) return; + applyDrop(null, data); }; 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); 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(() => { 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); @@ -495,10 +502,6 @@ export default function StrategyListEditor({ useEffect(() => { return subscribePaletteDrag(ev => { if (ev.phase === 'move') { - if (!isOverListEditor(ev.x, ev.y)) { - setDragOverKeyThrottled(null); - return; - } setDragOverKeyThrottled(findPaletteDropKeyAtPoint(ev.x, ev.y)); return; } @@ -519,14 +522,9 @@ export default function StrategyListEditor({ e.preventDefault(); e.stopPropagation(); setDragOverKey(null); - try { - const data = JSON.parse(e.dataTransfer.getData('application/json')); - if (data.type === 'start') { - handleAddStart(); - } - } catch { - /* ignore */ - } + const data = readPaletteHtmlDragData(e); + if (!data) return; + applyDropRef.current(findPaletteDropKeyAtPoint(e.clientX, e.clientY) ?? LIST_DROP_KEY, data); }; return ( diff --git a/frontend/src/utils/flowScreenProjection.ts b/frontend/src/utils/flowScreenProjection.ts index 0050160..d079f0c 100644 --- a/frontend/src/utils/flowScreenProjection.ts +++ b/frontend/src/utils/flowScreenProjection.ts @@ -77,16 +77,33 @@ export function projectScreenToFlowPosition( ); } -/** 목록 방식 편집기 위인지 */ +/** 목록 방식 편집기 위인지 (rect hit-test) */ export function isOverListEditor(clientX: number, clientY: number): boolean { - for (const el of elementsUnderDragPoint(clientX, clientY)) { - if (el.closest('.se-list-editor')) return true; + const editors = document.querySelectorAll('.se-list-editor'); + 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; } /** 팔레트 드롭 — 전략 빌더 캔버스 위인지 */ export function isOverStrategyBuilder(clientX: number, clientY: number): boolean { + const canvas = document.querySelector('.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)) { 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; @@ -171,7 +188,7 @@ export function resolvePaletteDropClientPoint( return { flowPos, anchorId: domId }; } - if (getActivePaletteDrag()) { + if (getActivePaletteDrag() || isPaletteHtmlDragActive()) { const flowHit = findAtFlow(flowPos, nodes); if (flowHit && isConnectTarget(flowHit.id)) { return { flowPos, anchorId: flowHit.id }; diff --git a/frontend/src/utils/paletteDragSession.ts b/frontend/src/utils/paletteDragSession.ts index 0975c93..070b4cd 100644 --- a/frontend/src/utils/paletteDragSession.ts +++ b/frontend/src/utils/paletteDragSession.ts @@ -57,6 +57,23 @@ let lastDragClientXY: { x: number; y: number } | null = null; let htmlDragMouseTrackUnbind: (() => void) | null = null; let htmlPaletteDragActive = false; 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() { if (typeof window === 'undefined') return; @@ -84,7 +101,19 @@ function installHtmlPaletteDragGlobals() { stopHtmlDragMouseTracking(); htmlPaletteDragActive = false; 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(); @@ -388,6 +417,7 @@ export function startHtmlDragMouseTracking( clientY: number, payload: PaletteDragPayload, ): void { + activeHtmlDragPayload = payload; stopHtmlDragMouseTracking(); if (clientX !== 0 || clientY !== 0) { lastHtmlDragClientXY = { x: clientX, y: clientY }; @@ -418,6 +448,18 @@ export function markPaletteHtmlDragEnd(): void { stopHtmlDragMouseTracking(); htmlPaletteDragActive = false; 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 { @@ -429,6 +471,7 @@ export function isPaletteHtmlDragActive(): boolean { } export function writePaletteHtmlDragData(e: React.DragEvent, payload: PaletteDragPayload): void { + activeHtmlDragPayload = payload; markPaletteHtmlDragStart(); const json = JSON.stringify(payload); 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 { - 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; - } + return readPaletteHtmlDragDataFromEvent(e.nativeEvent) ?? activeHtmlDragPayload; } export function findPaletteDropKeyAtPoint(x: number, y: number): string | null { + const hosts = Array.from(document.querySelectorAll('[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)) { const host = el.closest('[data-palette-drop-key]') as HTMLElement | null; if (host?.dataset.paletteDropKey) return host.dataset.paletteDropKey;