From 7f3955c0206eac860f37c39c589f70bc07aeb6b8 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 27 Apr 2026 12:16:45 +0900 Subject: [PATCH] =?UTF-8?q?fix(study):=20=E3=84=B1=E2=86=92=E3=85=8F=20han?= =?UTF-8?q?g=20+=201=EC=82=AC=EB=B6=84=EB=A9=B4=20=ED=99=95=EB=8C=80=20?= =?UTF-8?q?=ED=9A=8C=EA=B7=80=20=E2=80=94=20pointerleave=20=EC=95=88?= =?UTF-8?q?=EC=A0=84=EB=A7=9D=20+=20viewport=20meta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상: - ㄱ stroke 후 ㅏ stroke 가 안 그려짐. iOS Safari 가 setPointerCapture 를 silently 풀어 pointerup 이 캔버스로 routing 안 되는 케이스에서 isDrawing 락 잔존 → 다음 pointerdown 이 onPointerDown:298 가드 에서 거부. - 캔버스가 1사분면으로 확대되는 OS 핀치줌. element-level gesturestart 차단이 일부 iOS 빌드에서 흡수만 되고 줌이 진행. A. pointerleave 안전망 (HandwriteCanvas.svelte) - onpointerleave={endStroke} 복구. - endStroke 내 pointerleave 분기: canvas.hasPointerCapture true 면 ignore (정상 흐름, pointerup 곧 도착). false 면 안전망 finalize → isDrawing 락 해제. - capture 가 정상 잡힌 케이스엔 영향 없음 (leave 자체가 안 옴). B. viewport meta 강화 ([id]/+page.svelte) - maximum-scale=1, user-scalable=no 추가. iOS 13+ 에서 OS 핀치줌 원천 차단. - 페이지별 meta 라 다른 페이지 접근성 영향 0. zoom UI 는 Phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/lib/components/HandwriteCanvas.svelte | 13 +++++++++++-- frontend/src/routes/study/write/[id]/+page.svelte | 9 ++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/components/HandwriteCanvas.svelte b/frontend/src/lib/components/HandwriteCanvas.svelte index 0a64008..658ba16 100644 --- a/frontend/src/lib/components/HandwriteCanvas.svelte +++ b/frontend/src/lib/components/HandwriteCanvas.svelte @@ -373,11 +373,19 @@ // pointerup → 정상 finalize. pointercancel → inflight 폐기 (사용자 의도가 아닌 // OS 강제 취소 — multi-touch / 시스템 gesture 인식 시 발생. cancel 된 stroke 가 // strokes 에 들어가면 의도치 않은 짧은 노이즈 stroke 누적, 사용자 글자 망가짐). - // pointerleave 는 핸들러 미바인딩 — setPointerCapture 가 잡히면 leave 자체가 안 - // 오고, 캡처 실패 케이스는 OS 가 pointercancel 로 흘려보냄. + // pointerleave 는 안전망 — capture 가 정상 잡혀 있으면 leave 자체가 사양상 안 + // 오므로 무해. 만약 leave 가 도착했다면 iOS Safari 가 capture 를 silently 풀어 + // pointerup 이 캔버스에 routing 안 된 케이스 → 이 분기에서 강제 finalize 해야 + // isDrawing 락이 풀려서 다음 stroke 가 막히지 않는다 (ㄱ → ㅏ hang 회귀 방어). 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 }; + else if (e.type === 'pointerleave') { + // capture 가 활성이면 leave 는 정상 흐름이 아님 — ignore (정상적으로 pointerup + // 이 곧 도착할 것). capture 가 풀렸을 때만 안전망으로 finalize 진행. + if (canvas?.hasPointerCapture?.(e.pointerId)) return; + dbg = { ...dbg, leave: dbg.leave + 1 }; + } if (!isDrawing) return; if (e.pointerId !== activePointerId) { dbg = { ...dbg, rejectedByPointerId: dbg.rejectedByPointerId + 1 }; @@ -620,6 +628,7 @@ onpointermove={onPointerMove} onpointerup={endStroke} onpointercancel={endStroke} + onpointerleave={endStroke} oncontextmenu={(e) => e.preventDefault()} onselectstart={(e) => e.preventDefault()} class="block" diff --git a/frontend/src/routes/study/write/[id]/+page.svelte b/frontend/src/routes/study/write/[id]/+page.svelte index 90206b7..9159b34 100644 --- a/frontend/src/routes/study/write/[id]/+page.svelte +++ b/frontend/src/routes/study/write/[id]/+page.svelte @@ -152,9 +152,12 @@ - - + +