diff --git a/frontend/src/lib/components/HandwriteCanvas.svelte b/frontend/src/lib/components/HandwriteCanvas.svelte index 93d22d8..a471798 100644 --- a/frontend/src/lib/components/HandwriteCanvas.svelte +++ b/frontend/src/lib/components/HandwriteCanvas.svelte @@ -262,18 +262,15 @@ dbg = { ...dbg, rejectedByType: dbg.rejectedByType + 1 }; return; } + // 이미 다른 pointer 가 그리는 중이면 무시 — palm rejection / multi-touch race 차단. + // (Apple Pencil 진행 중에 손가락 닿거나 두 번째 pen 시도하면 둘 다 망가짐) + if (isDrawing) return; + e.preventDefault(); try { document.getSelection?.()?.removeAllRanges?.(); } catch {} - // 이전 inflight 가 어떤 이유로 살아있다면 즉시 finalize (다음 stroke 누락 방지). - if (inflight) { - if (inflight.points.length > 1) { - strokes = [...strokes, inflight]; - isDirty = true; - backup(); - } - inflight = null; - } + // 이전 inflight 가 살아있다면 폐기 (다음 stroke 와 섞이지 않게). + inflight = null; try { canvas.setPointerCapture(e.pointerId); } catch {} activePointerId = e.pointerId; @@ -339,8 +336,10 @@ scheduleRedraw(); } - // pointerup / pointercancel 만 finalize. pointerleave 는 무시 — stale leave 가 - // 진행 중 stroke 를 강제 종료시켜 다음 pointermove 가 다 누락되는 핵심 버그 차단. + // pointerup → 정상 finalize. pointercancel → inflight 폐기 (사용자 의도가 아닌 + // OS 강제 취소 — multi-touch / 시스템 gesture 인식 시 발생. cancel 된 stroke 가 + // strokes 에 들어가면 의도치 않은 짧은 노이즈 stroke 누적, 사용자 글자 망가짐). + // pointerleave 는 무시 — stale leave 가 진행 중 stroke 끊는 케이스 방어. function endStroke(e: PointerEvent) { if (e.type === 'pointerup') dbg = { ...dbg, up: dbg.up + 1 }; else if (e.type === 'pointercancel') dbg = { ...dbg, cancel: dbg.cancel + 1 }; @@ -354,13 +353,21 @@ activePointerId = null; isDrawing = false; + const wasCanceled = e.type === 'pointercancel'; if (tool === 'eraser') { inflight = null; return; } - // 1점 stroke (짧은 탭) 도 보존 — 사용자가 그린 모든 획이 들어와야 함. + // pointercancel 은 stroke 폐기. + if (wasCanceled) { + inflight = null; + scheduleRedraw(); + return; + } + + // pointerup: 정상 종료 — 1점 stroke (짧은 탭) 도 보존. if (inflight && inflight.points.length >= 1) { strokes = [...strokes, inflight]; undoStack = []; diff --git a/frontend/src/routes/study/write/[id]/+page.svelte b/frontend/src/routes/study/write/[id]/+page.svelte index 40538fd..f64552b 100644 --- a/frontend/src/routes/study/write/[id]/+page.svelte +++ b/frontend/src/routes/study/write/[id]/+page.svelte @@ -42,13 +42,14 @@ function blockGesture(e) { e.preventDefault(); } // 추가: touchstart 가 두 손가락이면 preventDefault — 일부 Safari 빌드에서 // gesture 이벤트 미발화 시에도 핀치 제스처 자체를 차단. - function blockMultiTouch(e) { - if (e.touches && e.touches.length > 1) e.preventDefault(); - } // 데스크톱 Chrome/Firefox: trackpad 핀치 = wheel + ctrlKey 로 디스패치. function blockPinchWheel(e) { if (e.ctrlKey || e.metaKey) e.preventDefault(); } + // 주의: blockMultiTouch (touchstart 의 length>1 preventDefault) 는 제거함. + // iOS Safari 의 자체 palm rejection (Apple Pencil 닿으면 손가락 touch 자동 무시) 이 + // touch 이벤트 default 처리에 의존하는데, preventDefault 가 그 메커니즘을 망가뜨려 + // pointercancel 발생률 증가시킴 (사용자 디버그 패널에서 cancel:4 관측). // 모든 button 공통 inline style — 더블탭 줌 / long-press 메뉴 / 텍스트 선택 차단. const BTN_STYLE = @@ -69,8 +70,6 @@ document.addEventListener('gesturestart', blockGesture, { passive: false }); document.addEventListener('gesturechange', blockGesture, { passive: false }); document.addEventListener('gestureend', blockGesture, { passive: false }); - document.addEventListener('touchstart', blockMultiTouch, { passive: false }); - document.addEventListener('touchmove', blockMultiTouch, { passive: false }); document.addEventListener('wheel', blockPinchWheel, { passive: false }); }); onDestroy(() => { @@ -78,8 +77,6 @@ document.removeEventListener('gesturestart', blockGesture); document.removeEventListener('gesturechange', blockGesture); document.removeEventListener('gestureend', blockGesture); - document.removeEventListener('touchstart', blockMultiTouch); - document.removeEventListener('touchmove', blockMultiTouch); document.removeEventListener('wheel', blockPinchWheel); } });