diff --git a/frontend/src/components/strategyEditor/PaletteChip.tsx b/frontend/src/components/strategyEditor/PaletteChip.tsx index fc3deb7..6641f72 100644 --- a/frontend/src/components/strategyEditor/PaletteChip.tsx +++ b/frontend/src/components/strategyEditor/PaletteChip.tsx @@ -51,6 +51,10 @@ export default function PaletteChip({ }); const onDragStart = (e: React.DragEvent) => { + if (needsPointerPaletteDrag()) { + e.preventDefault(); + return; + } writePaletteHtmlDragData(e, buildPayload()); }; @@ -95,7 +99,7 @@ export default function PaletteChip({ return (
{ hideHint(); + if (needsPointerPaletteDrag()) { + e.preventDefault(); + return; + } const payload: SidewaysFilterDragPayload = { type: 'sidewaysFilter', value: item.id, @@ -127,7 +131,7 @@ function SidewaysFilterChip({ return (
{ if (ev.phase === 'move') { - if (!isOverStrategyBuilder(ev.x, ev.y)) { - if (lastPreviewAnchorRef.current != null) { - lastPreviewAnchorRef.current = null; - lastPreviewKeyRef.current = null; - clearPreviewRef.current(); - } - return; - } const { flowPos, anchorId } = resolveDropFromClientRef.current(ev.x, ev.y); if (anchorId) { - const sideKey = `${anchorId}:${Math.round(flowPos.x / 8)}:${Math.round(flowPos.y / 8)}`; - if (sideKey === lastPreviewKeyRef.current) return; - lastPreviewKeyRef.current = sideKey; lastPreviewAnchorRef.current = anchorId; + lastPreviewKeyRef.current = `${anchorId}:${Math.round(flowPos.x / 8)}:${Math.round(flowPos.y / 8)}`; updatePreviewRef.current(anchorId, flowPos); return; } diff --git a/frontend/src/styles/strategyEditor.css b/frontend/src/styles/strategyEditor.css index b7976a2..b87f245 100644 --- a/frontend/src/styles/strategyEditor.css +++ b/frontend/src/styles/strategyEditor.css @@ -2033,6 +2033,11 @@ .desktop-client .se-palette-card:active { cursor: grabbing; } +.se-palette-card--pointer { + touch-action: none; + -webkit-user-drag: none; + user-select: none; +} .se-palette-card:hover { transform: translateY(-2px); } diff --git a/frontend/src/utils/flowScreenProjection.ts b/frontend/src/utils/flowScreenProjection.ts index 489cffb..0050160 100644 --- a/frontend/src/utils/flowScreenProjection.ts +++ b/frontend/src/utils/flowScreenProjection.ts @@ -1,5 +1,9 @@ import type { ReactFlowInstance } from '@xyflow/react'; -import { getLastHtmlPaletteDragClientXY, isPaletteHtmlDragActive } from './paletteDragSession'; +import { + getActivePaletteDrag, + getLastHtmlPaletteDragClientXY, + isPaletteHtmlDragActive, +} from './paletteDragSession'; type FlowProjector = Pick; @@ -93,8 +97,9 @@ export function isOverStrategyBuilder(clientX: number, clientY: number): boolean /** DOM hit-test — react-flow 노드 id (Safari HTML5 drag: elementsFromPoint 불가 → rect 우선) */ export function findFlowNodeIdAtClientPoint(clientX: number, clientY: number): string | null { const pad = 10; - const nodeEls = document.querySelectorAll('.react-flow__node'); - for (const nodeEl of nodeEls) { + const nodeEls = Array.from(document.querySelectorAll('.react-flow__node')); + for (let i = nodeEls.length - 1; i >= 0; i--) { + const nodeEl = nodeEls[i]; const rect = nodeEl.getBoundingClientRect(); if ( clientX >= rect.left - pad && clientX <= rect.right + pad @@ -166,6 +171,13 @@ export function resolvePaletteDropClientPoint( return { flowPos, anchorId: domId }; } + if (getActivePaletteDrag()) { + const flowHit = findAtFlow(flowPos, nodes); + if (flowHit && isConnectTarget(flowHit.id)) { + return { flowPos, anchorId: flowHit.id }; + } + } + for (const node of nodes) { if (!isConnectTarget(node.id)) continue; const escaped = typeof CSS !== 'undefined' && CSS.escape diff --git a/frontend/src/utils/paletteDragSession.ts b/frontend/src/utils/paletteDragSession.ts index 112a351..eb8b630 100644 --- a/frontend/src/utils/paletteDragSession.ts +++ b/frontend/src/utils/paletteDragSession.ts @@ -7,7 +7,7 @@ import { bindWindowDrag, isPrimaryPointerButton, } from './pointerDrag'; -import { isDesktop, isMacDesktop } from './platform'; +import { isDesktop } from './platform'; export type PaletteDragPayload = Record & { type: string; @@ -96,10 +96,8 @@ function hasCoarsePointer(): boolean { } } -/** HTML5 DnD 대신 pointer/touch overlay 드래그 사용 (Windows Desktop 등) */ +/** HTML5 DnD 대신 pointer/touch overlay 드래그 (Tauri Desktop 전체 — macOS WKWebView HTML5 DnD 불안정) */ export function needsPointerPaletteDrag(): boolean { - // macOS Tauri WKWebView — Safari 웹과 동일하게 HTML5 DnD 사용 - if (isMacDesktop()) return false; return isDesktop() || hasCoarsePointer(); } @@ -279,7 +277,7 @@ function startPaletteDragGesture( finishDrag('end', xy); } }, - { preventTouchScroll: true }, + { preventTouchScroll: true, useCapture: isDesktop() }, ); } diff --git a/frontend/src/utils/strategyFlowLayout.ts b/frontend/src/utils/strategyFlowLayout.ts index ca630fe..5f8f0f1 100644 --- a/frontend/src/utils/strategyFlowLayout.ts +++ b/frontend/src/utils/strategyFlowLayout.ts @@ -368,7 +368,7 @@ export type EdgeHandleBinding = { export const FLOW_NODE_W = 200; export const FLOW_NODE_H = 72; export const FLOW_START_W = 200; -export const FLOW_START_H = 78; +export const FLOW_START_H = 168; function nodeCenter( id: string,