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,