Files
goldenChart/전략편집기 전략지표 드래그 문제.md

12 KiB

전략편집기 — 전략지표 드래그 문제 (macOS Desktop)

상태: 2026-06 macOS Desktop(Tauri/WKWebView)에서 드래그·START 연결점 preview·드롭 연결 문제 해결됨
목적: 동일 증상 재발 시 원인 추적·수정 시 되돌리면 안 되는 설계 원칙을 기록

관련 문서: 전략편집기 로직 및 동작흐름.md


1. 증상 요약

1.1 기대 동작 (웹 frontend와 동일)

그래프 모드 (전략 빌더):

  1. 우측 패널 지표/논리 카드를 눌러 드래그
  2. 캔버스 START 노드 위에 올리면 노란 테두리(glow) + 연결점(handle) 활성화
  3. 드롭 시 START(또는 다른 부모 노드)에 트리 연결·추가

목록 모드:

  • START 아래 점선 영역 highlight + 드롭 시 목록에 추가

1.2 재발했던 실패 패턴 (반복 수정의 함정)

단계 사용자 체감 실제로 깨진 것
A 드래그는 되나 START 위 반응 없음 preview hit-test (좌표 0,0)
B A 수정 후 드래그 자체 안 됨
C B 수정 후 다시 연결점/preview 안 됨

핵심: 드래그 UI와 preview/drop을 서로 다른 메커니즘(HTML5 ↔ pointer) 으로 번갈아 쓰면 한쪽만 고쳐지고 다른 쪽이 깨지는 oscillation(진동) 이 반복된다.


2. 환경

항목
플랫폼 macOS Desktop — Tauri + WKWebView
테스트 기기 MacBook Pro M1 Max 등
빌드 ./scripts/build-desktop.sh --mac
산출물 desktop/src-tauri/target/release/bundle/macos/GoldenChart.app
웹 검증 Docker frontend만으로 Desktop DnD 검증 불충분 — 반드시 .app 실행

3. 아키텍처 — 플랫폼별 DnD 분기

분기 기준: needsPointerPaletteDrag() (frontend/src/utils/paletteDragSession.ts)

export function needsPointerPaletteDrag(): boolean {
  return isDesktop() || hasCoarsePointer();
}
플랫폼 needsPointerPaletteDrag() 드래그 방식 카드 draggable
Web 브라우저 false HTML5 DnD true
Desktop (mac/Windows) true pointer overlay false
Coarse touch true pointer overlay false

isDesktop()window.__TAURI__ 또는 html.desktop-client 클래스 (frontend/src/utils/platform.ts)


4. 근본 원인

4.1 macOS WKWebView + HTML5 DnD 는 preview/drop에 부적합

Desktop에서 HTML5(draggable=true)를 쓰면 드래그 고스트는 보일 수 있으나, WKWebView 네이티브 drag 세션 중:

  • pointermove / mousemove 중단
  • dragover / drag 이벤트의 clientX, clientY0, 0 으로 오는 경우 다수
  • pageX/pageY, 전역 mouse 추적 fallback도 drag 중에는 갱신되지 않음

preview/drop은 resolvePaletteDropClientPoint() → START DOM/flow hit-test에 실제 커서 좌표가 필요하다. 좌표가 팔레트 패널 위치에 고정되면 anchorId가 항상 null연결점·glow 없음, 드롭도 실패.

웹 Chrome/Safari는 HTML5 dragover에 유효 좌표를 주므로 동일 코드가 웹에서는 정상 동작한다. mac Desktop만 HTML5를 쓰면 안 된다.

4.2 pointer + window 리스너 + setPointerCapture + threshold (초기 Desktop 시도)

우측 지표 패널 구조:

.se-right-body { overflow-y: auto; }          /* 스크롤 컨테이너 */
.se-palette-section--scroll { overflow-y: auto; }

실패 요인:

  1. setPointerCapture를 팔레트 카드(스크롤 내부)에 설정 → WebKit이 제스처를 스크롤/캡처 대상으로 처리, windowpointermove 미수신
  2. 이동 threshold(3~6px) 이후에만 overlay 마운트 → move 이벤트가 오지 않으면 드래그가 시작조차 안 됨
  3. bindWindowDrag만 사용 → capture 단계에서 document까지 이벤트가 가기 전에 스크롤 패널이 흡수

팝업 패널 타이틀바 드래그(useDraggablePanel)는 스크롤 밖 fixed 영역이라 동일 패턴이 거기서는 동작했다. 팔레트 카드는 다른 DOM 맥락이다.

4.3 드래그와 preview를 다른 파이프로 처리 (oscillation)

잘못된 수정 패턴:

  • mac만 HTML5 → preview용 pointer/html rAF 추가
  • preview 안 되면 mac만 pointer → drag 시작 로직 변경
  • drag 안 되면 다시 HTML5 …

올바른 원칙: Desktop에서 drag 이동 좌표 = preview 좌표 = drop 좌표 — 단일 subscribePaletteDrag 파이프.


5. 최종 해결책 (현재 코드)

5.1 설계 원칙

  1. Desktop: HTML5 사용 금지 (draggable={false}, onDragStart에서 preventDefault)
  2. pointerdown 즉시 overlay — threshold로 “드래그 시작”을 미루지 않음
  3. document capture pointermove/pointerupbindDocumentPointerDrag() (frontend/src/utils/pointerDrag.ts)
  4. setPointerCapture 사용 안 함 (카드·overlay 모두)
  5. 이동·preview·drop 모두 subscribePaletteDrag → 동일 (x, y)
  6. 5px 미만 이동 = cancel — 클릭(카드 선택)과 드래그 구분 (CLICK_VS_DRAG_PX)

5.2 Desktop 데이터 흐름

PaletteChip.onPointerDownCapture
  → handlePalettePointerDown
  → startPaletteDragGesture
       ├─ mountDragOverlay (즉시)
       ├─ body.se-palette-drag-armed (스크롤 잠금)
       └─ bindDocumentPointerDrag(pointerId)
            ├─ move → notifyPaletteDragMove → emit('move')
            └─ up   → finishDrag → setPaletteDragDropHandler → applyPaletteDropAt

subscribePaletteDrag('move')  [StrategyEditorCanvas, StrategyListEditor]
  → resolvePaletteDropClientPoint
  → updateDropPreview (dragOver, activeSourceSide)
  → FlowNodes START: se-flow-node--drop, se-flow-handle--active

5.3 Web 데이터 흐름 (참고 — Desktop과 분리 유지)

PaletteChip.onDragStart → writePaletteHtmlDragData
  → HTML5 drag + window dragover
  → subscribePaletteDrag('move') 또는 rAF + getLastHtmlPaletteDragClientXY
  → setHtmlPaletteDropRouter → applyPaletteDropAt

6. 핵심 파일

파일 역할
frontend/src/utils/paletteDragSession.ts Desktop/Web 분기, drag 세션, subscribePaletteDrag, drop handler
frontend/src/utils/pointerDrag.ts bindDocumentPointerDrag (Desktop 전용), bindWindowDrag (Web 보조)
frontend/src/utils/flowScreenProjection.ts client → flow 좌표, START hit-test (DOM rect 우선)
frontend/src/utils/platform.ts isDesktop(), isMacDesktop()
frontend/src/components/strategyEditor/PaletteChip.tsx onPointerDownCapture, draggable={!pointerMode}
frontend/src/components/strategyEditor/PaletteDragOverlay.tsx 드래그 중 overlay UI
frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx setPaletteDragDropHandler, subscribePaletteDrag → preview
frontend/src/components/strategyEditor/FlowNodes.tsx START dragOver / activeSourceSide UI
frontend/src/components/StrategyEditorPage.tsx {needsPointerPaletteDrag() && <PaletteDragOverlay />}
frontend/src/styles/strategyEditor.css .se-palette-drag-armed 스크롤 잠금, .se-flow-node--drop

7. preview (START glow / 연결점) 구현 요약

StrategyEditorCanvas.updateDropPreview(anchorId, flowPos):

  • setNodes로 해당 노드 data.dragOver = true, data.activeSourceSide 설정
  • FlowNodes.StartNode: se-flow-node--drop, handle se-flow-handle--active

드래그 중 layout rebuild가 preview를 지우지 않도록:

// StrategyEditorCanvas rebuild effect
if (isPaletteDragSessionActive()) return;

isPaletteDragSessionActive() = activePayload != null || htmlPaletteDragActive


8. hit-test (START 감지)

resolvePaletteDropClientPoint() (flowScreenProjection.ts) 우선순위:

  1. .react-flow__node[data-id] DOM getBoundingClientRect (pointer overlay에서 가장 정확)
  2. flow 좌표 → screen rect 변환 (projectFlowToScreen, host: .react-flow__viewport)
  3. findFlowNodeIdAtClientPoint
  4. flow 좌표 hit (findNodeAtFlowPosition)

Tauri에서 screenToFlowPosition이 screen px를 그대로 반환하는 경우가 있어 manualProject fallback 존재.


9. 재발 방지 — 하지 말아야 할 수정

금지 이유
mac Desktop만 HTML5로 되돌리기 WKWebView dragover 좌표 0,0 → preview/drop 불가
mac / Windows DnD 방식 분리 (mac HTML5, Win pointer) oscilation 재발
Desktop에서 draggable=true HTML5와 pointer 이중 경로 충돌
팔레트 카드에 setPointerCapture 스크롤 패널 내부에서 move 유실
threshold 큰 값(6px+)만 overlay 표시 move 미수신 시 드래그 “안 됨”
preview만 HTML5 dragover, drop만 pointer 등 좌표 소스 이원화 한쪽만 동작
document dragend에서 무조건 clearDropPreview() pointer 세션과 무관하게 preview 제거 가능 — HTML5일 때만 처리

10. 재발 시 진단 체크리스트

10.1 드래그 자체가 안 될 때

  • Desktop에서 카드 draggablefalse인지 (needsPointerPaletteDrag() true?)
  • onPointerDownCapturehandlePalettePointerDown 호출되는지
  • PaletteDragOverlay 마운트 여부 (StrategyEditorPage)
  • pointerdown 직후 overlay 라벨 표시되는지 (HTML5 고스트와 구분)
  • .se-right-bodybody.se-palette-drag-armed 스크롤 잠금 적용되는지
  • 기간 입력란 클릭 시 early return — 카드 제목 영역으로 테스트
  • ./scripts/build-desktop.sh --mac.app 실행 (웹 dev server만 X)

10.2 드래그는 되나 START glow/연결점 없을 때

  • subscribePaletteDrag move 이벤트 발생하는지
  • resolvePaletteDropClientPointanchorId: '__strategy_start__' 반환하는지
  • isPaletteDragSessionActive() 중 rebuild effect가 preview를 지우지 않는지
  • Desktop에서 HTML5 경로(htmlPaletteDragActive)로 잘못 들어가지 않는지

10.3 드롭이 연결되지 않을 때

  • Desktop: setPaletteDragDropHandler 등록 (needsPointerPaletteDrag() true)
  • Web: setHtmlPaletteDropRouter 등록 (needsPointerPaletteDrag() false)
  • applyPaletteDropAtcanAcceptPaletteAsParent(START) true

11. 검증 시나리오

  1. 그래프 모드: CCI(등) → START 위 glow + 연결점 → 드롭 연결
  2. 연속 2~3회 드래그 반복
  3. 카드 살짝 클릭 → 선택만 되고 드래그/overlay 없음 (< 5px)
  4. 목록 모드: 점선 영역 highlight + 드롭
  5. Windows Desktop 동일 pointer 경로 회귀 없음
  6. Web 브라우저 HTML5 드래그·preview 정상

12. 참고 — oscillation 타임라인 (교훈)

HTML5(mac)     → drag OK, preview FAIL   (WKWebView coords)
pointer+threshold → drag FAIL             (scroll + capture)
HTML5 + coord fix → drag OK, preview FAIL (근본 한계)
mac HTML5 only  → drag OK, preview FAIL
pointer unified → drag OK, preview OK     ← 최종

교훈: Desktop WKWebView에서는 “드래그만 HTML5”와 “preview만 pointer” 조합이 아니라, 처음부터 끝까지 pointer overlay 단일 세션이어야 한다.


13. 빌드·배포

# macOS Desktop (로컬)
./scripts/build-desktop.sh --mac

# Windows 크로스는 cargo-xwin 필요 — mac 빌드와 별도
./scripts/build-desktop.sh

Desktop 버전 bump 시 desktop/src-tauri/tauri.conf.json 등 반영 후 배포.


14. 변경 이력

날짜 내용
2026-06 macOS Desktop 지표 드래그·START preview·드롭 통합 해결. bindDocumentPointerDrag, pointerdown 즉시 overlay, HTML5 Desktop 제거

문서 작성: 전략편집기 DnD 디버깅 세션 정리. 코드 변경 시 이 문서의 §5·§9도 함께 갱신할 것.