diff --git a/frontend/src/lib/components/HandwriteCanvas.svelte b/frontend/src/lib/components/HandwriteCanvas.svelte index 9ecc1df..636c3d5 100644 --- a/frontend/src/lib/components/HandwriteCanvas.svelte +++ b/frontend/src/lib/components/HandwriteCanvas.svelte @@ -63,9 +63,12 @@ let isDrawing = $state(false); let activePointerId = $state(null); - // ── 디버그 카운터 — DEV 빌드 한정. prod 에선 mutation 자체가 DCE 되도록 모든 - // dbg = ... 호출을 if (DBG) 로 감쌌음. - const DBG = import.meta.env.DEV; + // ── 디버그 카운터 — DEV 빌드 또는 prod 에서 ?debug=1 query 시 활성화. + // prod 에서 mutation 자체를 DCE 하려면 import.meta.env.DEV 단독이지만, + // 사용자 iPad 진단을 위해 prod 에서도 query 로 토글 가능하게 함. const 라 + // 페이지 로드 시 한 번만 평가 (성능 영향 미미). + const DBG = import.meta.env.DEV || + (typeof window !== 'undefined' && /[?&]debug=1\b/.test(window.location.search)); let dbg = $state({ down: 0, move: 0, up: 0, cancel: 0, leave: 0, rejectedByType: 0, rejectedByPointerId: 0, @@ -297,6 +300,13 @@ if (DBG) dbg = { ...dbg, rejectedByType: dbg.rejectedByType + 1 }; return; } + // pen 의 hover-down (buttons===0) 은 무시 — 실제 접촉이 아닌 hover 진입. + // Pencil 닿는 순간 buttons===1. 이걸 stroke 시작으로 오인하면 hover 이동이 + // 점으로 추가됨. + if (e.pointerType === 'pen' && e.buttons === 0) { + if (DBG) dbg = { ...dbg, rejectedByType: dbg.rejectedByType + 1, lastType: 'hover-down' }; + return; + } // 이미 다른 pointer 가 그리는 중이면 무시 — palm rejection / multi-touch race 차단. // (Apple Pencil 진행 중에 손가락 닿거나 두 번째 pen 시도하면 둘 다 망가짐) if (isDrawing) return; @@ -343,6 +353,35 @@ return; } + // ── Apple Pencil hover 감지 (iPadOS 17+) ───────────────────────── + // 펜이 화면에서 떨어진 채로도 pointermove 가 발화 — pointerType==='pen' 이지만 + // buttons===0 (펜 안 닿음). pointerup 안 와도 hover 모드는 사실상 stroke 종료. + // 이 케이스를 잡지 않으면 hover 이동이 stroke 의 점으로 추가됨 → ㄱ 다음에 + // ㅏ 위치로 hover 이동 시 ㄱ 끝점에서 ㅏ 위치까지 직선이 그어지거나 한 stroke + // 가 의도치 않게 연장됨 (사용자 보고: ㄱ→ㅏ 가 안 써짐, 글씨 흩어짐). + if (e.pointerType === 'pen' && e.buttons === 0) { + try { canvas?.releasePointerCapture?.(e.pointerId); } catch {} + activePointerId = null; + isDrawing = false; + if (DBG) dbg = { ...dbg, up: dbg.up + 1, lastType: 'hover-end' }; + if (tool === 'eraser') { + inflight = null; + eraserLast = null; + return; + } + // pointerup 흐름: 1점 stroke (짧은 탭) 도 보존. + if (inflight && inflight.points.length >= 1) { + strokes = [...strokes, inflight]; + undoStack = []; + isDirty = true; + backup(); + scheduleSave(); + } + inflight = null; + scheduleRedraw(); + return; + } + const [x, y] = getLocalXY(e); if (tool === 'eraser') { @@ -653,8 +692,8 @@ style="touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; cursor: {tool === 'eraser' ? 'cell' : 'crosshair'};" > - {#if import.meta.env.DEV} - + {#if DBG} +
type:{dbg.lastType} p:{dbg.lastPressure.toFixed(2)}
down:{dbg.down} move:{dbg.move} up:{dbg.up} cancel:{dbg.cancel}