fix(study): ㄱ→ㅏ hang + 1사분면 확대 회귀 — pointerleave 안전망 + viewport meta
증상:
- ㄱ 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -152,9 +152,12 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<!-- 접근성을 위해 user-scalable / maximum-scale 강제는 적용하지 않는다.
|
||||
핀치줌 차단은 페이지 root 의 ongesturestart 등으로 영역 제한. -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<!-- 학습 캔버스: 핀치줌이 의도 사용 패턴 아님. element-level gesturestart 차단이
|
||||
일부 iOS Safari 빌드에서 흡수만 되고 줌이 진행되는 케이스 (페이지가 1사분면으로
|
||||
확대되는 회귀) 를 viewport 레벨에서 원천 차단. iOS 13+ 에서 maximum-scale=1 +
|
||||
user-scalable=no 조합이 OS 핀치줌 차단으로 동작. 접근성 zoom 은 별도 zoom
|
||||
버튼으로 Phase 2 에서 제공. 페이지별 meta 라 다른 페이지 영향 없음. -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user