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 @@