전략편집기 수정

This commit is contained in:
Macbook
2026-06-15 00:31:59 +09:00
parent f35f17df36
commit 78d9dca870
5 changed files with 49 additions and 43 deletions
@@ -68,12 +68,6 @@ export default function PaletteDragOverlay() {
const s = sessionRef.current; const s = sessionRef.current;
if (!s || e.pointerId !== s.pointerId) return; if (!s || e.pointerId !== s.pointerId) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation();
const label = labelRef.current;
if (label) {
label.style.left = `${e.clientX}px`;
label.style.top = `${e.clientY}px`;
}
notifyPaletteDragMove(e.clientX, e.clientY); notifyPaletteDragMove(e.clientX, e.clientY);
}, []); }, []);
@@ -81,7 +75,6 @@ export default function PaletteDragOverlay() {
const s = sessionRef.current; const s = sessionRef.current;
if (!s || e.pointerId !== s.pointerId) return; if (!s || e.pointerId !== s.pointerId) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation();
notifyPaletteDragEnd(e.clientX, e.clientY); notifyPaletteDragEnd(e.clientX, e.clientY);
}, []); }, []);
@@ -89,7 +82,6 @@ export default function PaletteDragOverlay() {
const s = sessionRef.current; const s = sessionRef.current;
if (!s || e.pointerId !== s.pointerId) return; if (!s || e.pointerId !== s.pointerId) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation();
notifyPaletteDragCancel(e.clientX, e.clientY); notifyPaletteDragCancel(e.clientX, e.clientY);
}, []); }, []);
@@ -32,8 +32,6 @@ import {
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage'; import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
import { import {
isPaletteHtmlDrag, isPaletteHtmlDrag,
completePaletteDragAt,
getActivePaletteDrag,
needsPointerPaletteDrag, needsPointerPaletteDrag,
readPaletteHtmlDragData, readPaletteHtmlDragData,
setPaletteDragDropHandler, setPaletteDragDropHandler,
@@ -1282,6 +1280,7 @@ function StrategyEditorCanvasInner({
const resolveDropFromClientRef = useRef(resolveDropFromClient); const resolveDropFromClientRef = useRef(resolveDropFromClient);
resolveDropFromClientRef.current = resolveDropFromClient; resolveDropFromClientRef.current = resolveDropFromClient;
const lastPreviewAnchorRef = useRef<string | null>(null); const lastPreviewAnchorRef = useRef<string | null>(null);
const lastPreviewKeyRef = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
if (!needsPointerPaletteDrag()) return; if (!needsPointerPaletteDrag()) return;
@@ -1290,13 +1289,18 @@ function StrategyEditorCanvasInner({
if (!isOverStrategyBuilder(ev.x, ev.y)) { if (!isOverStrategyBuilder(ev.x, ev.y)) {
if (lastPreviewAnchorRef.current != null) { if (lastPreviewAnchorRef.current != null) {
lastPreviewAnchorRef.current = null; lastPreviewAnchorRef.current = null;
lastPreviewKeyRef.current = null;
clearPreviewRef.current(); clearPreviewRef.current();
} }
return; return;
} }
const { flowPos, anchorId } = resolveDropFromClientRef.current(ev.x, ev.y); const { flowPos, anchorId } = resolveDropFromClientRef.current(ev.x, ev.y);
if (anchorId === lastPreviewAnchorRef.current) return; const previewKey = anchorId
? `${anchorId}:${Math.round(flowPos.x)}:${Math.round(flowPos.y)}`
: '';
if (previewKey === lastPreviewKeyRef.current) return;
lastPreviewAnchorRef.current = anchorId; lastPreviewAnchorRef.current = anchorId;
lastPreviewKeyRef.current = previewKey;
if (anchorId) { if (anchorId) {
updatePreviewRef.current(anchorId, flowPos); updatePreviewRef.current(anchorId, flowPos);
} else { } else {
@@ -1306,6 +1310,7 @@ function StrategyEditorCanvasInner({
} }
if (ev.phase === 'end' || ev.phase === 'cancel') { if (ev.phase === 'end' || ev.phase === 'cancel') {
lastPreviewAnchorRef.current = null; lastPreviewAnchorRef.current = null;
lastPreviewKeyRef.current = null;
} }
if (ev.phase === 'cancel') { if (ev.phase === 'cancel') {
clearPreviewRef.current(); clearPreviewRef.current();
@@ -1351,21 +1356,11 @@ function StrategyEditorCanvasInner({
} }
}, [selectedNodeId, handleDelete, deleteSelectedNodes, handleDisconnectEdge]); }, [selectedNodeId, handleDelete, deleteSelectedNodes, handleDisconnectEdge]);
const handleCanvasPointerUp = useCallback((e: React.PointerEvent) => {
if (!needsPointerPaletteDrag()) return;
if (e.button !== 0) return;
if (!getActivePaletteDrag()) return;
e.preventDefault();
e.stopPropagation();
completePaletteDragAt(e.clientX, e.clientY);
}, []);
return ( return (
<div <div
ref={canvasWrapRef} ref={canvasWrapRef}
className={`se-canvas-wrap se-canvas-wrap--${signalTab} se-canvas-wrap--${interactionMode}`} className={`se-canvas-wrap se-canvas-wrap--${signalTab} se-canvas-wrap--${interactionMode}`}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onPointerUpCapture={handleCanvasPointerUp}
tabIndex={0} tabIndex={0}
> >
<ReactFlow <ReactFlow
+5 -1
View File
@@ -94,7 +94,11 @@ export function findFlowNodeIdAtClientPoint(clientX: number, clientY: number): s
for (const el of elementsUnderDragPoint(clientX, clientY)) { for (const el of elementsUnderDragPoint(clientX, clientY)) {
const nodeEl = el.closest('.react-flow__node') as HTMLElement | null; const nodeEl = el.closest('.react-flow__node') as HTMLElement | null;
if (nodeEl) { if (nodeEl) {
const id = nodeEl.getAttribute('data-id'); const id = nodeEl.getAttribute('data-id')
?? nodeEl.dataset.id
?? (nodeEl.id.startsWith('react-flow__node-')
? nodeEl.id.slice('react-flow__node-'.length)
: null);
if (id) return id; if (id) return id;
} }
const handleEl = el.closest('[data-nodeid]') as HTMLElement | null; const handleEl = el.closest('[data-nodeid]') as HTMLElement | null;
+10 -4
View File
@@ -53,6 +53,7 @@ let moveRafId: number | null = null;
let pendingMove: { x: number; y: number } | null = null; let pendingMove: { x: number; y: number } | null = null;
let unbindActiveDrag: (() => void) | null = null; let unbindActiveDrag: (() => void) | null = null;
let pendingPointerListeners: (() => void) | null = null; let pendingPointerListeners: (() => void) | null = null;
let lastDragClientXY: { x: number; y: number } | null = null;
function hasCoarsePointer(): boolean { function hasCoarsePointer(): boolean {
if (typeof window === 'undefined') return false; if (typeof window === 'undefined') return false;
@@ -136,6 +137,7 @@ function cleanupDrag() {
activePointerId = null; activePointerId = null;
activeTouchId = null; activeTouchId = null;
pendingMove = null; pendingMove = null;
lastDragClientXY = null;
if (moveRafId != null) { if (moveRafId != null) {
cancelAnimationFrame(moveRafId); cancelAnimationFrame(moveRafId);
moveRafId = null; moveRafId = null;
@@ -149,17 +151,20 @@ function cleanupDrag() {
function finishDrag(phase: 'end' | 'cancel', xy: { x: number; y: number }) { function finishDrag(phase: 'end' | 'cancel', xy: { x: number; y: number }) {
if (!activePayload) return; if (!activePayload) return;
const payload = activePayload; const payload = activePayload;
cleanupDrag(); const dropXY = lastDragClientXY ?? xy;
if (phase === 'end') { if (phase === 'end') {
suppressClickUntil = Date.now() + 400; suppressClickUntil = Date.now() + 400;
dispatchEnd(xy, payload); dispatchEnd(dropXY, payload);
} else { } else {
emit({ phase: 'cancel', x: xy.x, y: xy.y, payload }); emit({ phase: 'cancel', x: dropXY.x, y: dropXY.y, payload });
} }
cleanupDrag();
} }
export function notifyPaletteDragMove(clientX: number, clientY: number) { export function notifyPaletteDragMove(clientX: number, clientY: number) {
if (!activePayload) return; if (!activePayload) return;
lastDragClientXY = { x: clientX, y: clientY };
pendingMove = { x: clientX, y: clientY }; pendingMove = { x: clientX, y: clientY };
if (moveRafId != null) return; if (moveRafId != null) return;
moveRafId = requestAnimationFrame(() => { moveRafId = requestAnimationFrame(() => {
@@ -212,7 +217,7 @@ function bindActiveDragWindowListeners(pointerId: number) {
if (!eventMatchesActiveInput(ev)) return; if (!eventMatchesActiveInput(ev)) return;
finishDrag('end', xy); finishDrag('end', xy);
}, },
{ preventTouchScroll: true }, { preventTouchScroll: true, useCapture: true },
); );
} }
@@ -244,6 +249,7 @@ function beginDrag(
activePayload = payload; activePayload = payload;
activePointerId = pointerId; activePointerId = pointerId;
activeTouchId = touchId; activeTouchId = touchId;
lastDragClientXY = { x: xy.x, y: xy.y };
document.body.classList.add('se-palette-drag-active'); document.body.classList.add('se-palette-drag-active');
bindActiveDragWindowListeners(pointerId); bindActiveDragWindowListeners(pointerId);
+26 -17
View File
@@ -40,6 +40,8 @@ export function clientXYFromNative(e: Event): ClientXY | null {
export interface BindWindowDragOptions { export interface BindWindowDragOptions {
/** 터치 드래그 중 페이지 스크롤 방지 (기본 true) */ /** 터치 드래그 중 페이지 스크롤 방지 (기본 true) */
preventTouchScroll?: boolean; preventTouchScroll?: boolean;
/** capture 단계에서 수신 — overlay stopPropagation 우회 */
useCapture?: boolean;
} }
/** /**
@@ -51,9 +53,16 @@ export function bindWindowDrag(
onEnd: (xy: ClientXY, ev: Event) => void, onEnd: (xy: ClientXY, ev: Event) => void,
options: BindWindowDragOptions = {}, options: BindWindowDragOptions = {},
): () => void { ): () => void {
const { preventTouchScroll = true } = options; const { preventTouchScroll = true, useCapture = false } = options;
let lastXY: ClientXY = { x: 0, y: 0 }; let lastXY: ClientXY = { x: 0, y: 0 };
const moveOpts: AddEventListenerOptions | boolean = useCapture
? { capture: true, passive: !preventTouchScroll }
: { passive: !preventTouchScroll };
const endOpts: AddEventListenerOptions | boolean = useCapture
? { capture: true }
: false;
const move = (ev: Event) => { const move = (ev: Event) => {
const xy = clientXYFromNative(ev); const xy = clientXYFromNative(ev);
if (!xy) return; if (!xy) return;
@@ -69,24 +78,24 @@ export function bindWindowDrag(
onEnd(xy, ev); onEnd(xy, ev);
}; };
window.addEventListener('pointermove', move); window.addEventListener('pointermove', move, moveOpts);
window.addEventListener('pointerup', end); window.addEventListener('pointerup', end, endOpts);
window.addEventListener('pointercancel', end); window.addEventListener('pointercancel', end, endOpts);
window.addEventListener('mousemove', move); window.addEventListener('mousemove', move, moveOpts);
window.addEventListener('mouseup', end); window.addEventListener('mouseup', end, endOpts);
window.addEventListener('touchmove', move, { passive: !preventTouchScroll }); window.addEventListener('touchmove', move, moveOpts);
window.addEventListener('touchend', end); window.addEventListener('touchend', end, endOpts);
window.addEventListener('touchcancel', end); window.addEventListener('touchcancel', end, endOpts);
return () => { return () => {
window.removeEventListener('pointermove', move); window.removeEventListener('pointermove', move, moveOpts);
window.removeEventListener('pointerup', end); window.removeEventListener('pointerup', end, endOpts);
window.removeEventListener('pointercancel', end); window.removeEventListener('pointercancel', end, endOpts);
window.removeEventListener('mousemove', move); window.removeEventListener('mousemove', move, moveOpts);
window.removeEventListener('mouseup', end); window.removeEventListener('mouseup', end, endOpts);
window.removeEventListener('touchmove', move); window.removeEventListener('touchmove', move, moveOpts);
window.removeEventListener('touchend', end); window.removeEventListener('touchend', end, endOpts);
window.removeEventListener('touchcancel', end); window.removeEventListener('touchcancel', end, endOpts);
}; };
} }