From 475a542ea3e2e7dd22474af5d7b0ebe36af7cce3 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 27 Apr 2026 08:30:28 +0900 Subject: [PATCH] =?UTF-8?q?feat(study):=20iPad=20=EC=86=90=EA=B8=80?= =?UTF-8?q?=EC=94=A8=20=ED=95=99=EC=8A=B5=20=EC=84=B8=EC=85=98=20frontend?= =?UTF-8?q?=20(Phase=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-2: 자격증/어학 학습 세션 UI. iPad Safari + Apple Pencil 지원. 신규 컴포넌트: - HandwriteCanvas — perfect-freehand + PointerEvents (압력/tilt) + palm rejection (pointerType==='pen') + DPR + touch-action:none + stroke 단위 undo/redo + 5초 idle / 5 stroke 자동 저장 + localStorage 백업 + PNG snapshot export - StudyMetaEditor — study_type(certification/language) 토글, 자격증/어학 분기 메타 입력, 어학 metadata.reading/meaning/unit_type - SourceTextPanel — 원문 + 어학 메타 read-only 표시 - AssetList — 연결된 audio/video/scan/handwriting 표시 + 재생 + 연결/해제 라우트: - /study → /study/write 리다이렉트 - /study/write — 도메인 토글 + 빠른 시작 폼 + 세션 목록 - /study/write/[id] — 좌측 메타/원문/asset, 우측 캔버스 (md+ 분할, 모바일 위/아래) Layout/Sidebar: - 상단 nav 에 "공부" 추가 (메모와 뉴스 사이) - Sidebar 메모/Inbox 섹션에 GraduationCap 아이콘 항목 추가 기타: - frontend/package.json: perfect-freehand ^1.2.3 (MIT) - THIRD_PARTY_LICENSES.md 신규 — perfect-freehand MIT 고지 플랜: ~/.claude/plans/scalable-chasing-stonebraker.md (PR-2) 신규 파일 lint:tokens 회귀 0 (기존 잔존 130 그대로). --- THIRD_PARTY_LICENSES.md | 34 ++ frontend/package-lock.json | 9 +- frontend/package.json | 3 +- frontend/src/lib/components/AssetList.svelte | 112 ++++++ .../src/lib/components/HandwriteCanvas.svelte | 363 ++++++++++++++++++ frontend/src/lib/components/Sidebar.svelte | 12 +- .../src/lib/components/SourceTextPanel.svelte | 62 +++ .../src/lib/components/StudyMetaEditor.svelte | 182 +++++++++ frontend/src/routes/+layout.svelte | 1 + frontend/src/routes/study/+page.svelte | 13 + frontend/src/routes/study/write/+page.svelte | 223 +++++++++++ .../src/routes/study/write/[id]/+page.svelte | 180 +++++++++ 12 files changed, 1191 insertions(+), 3 deletions(-) create mode 100644 THIRD_PARTY_LICENSES.md create mode 100644 frontend/src/lib/components/AssetList.svelte create mode 100644 frontend/src/lib/components/HandwriteCanvas.svelte create mode 100644 frontend/src/lib/components/SourceTextPanel.svelte create mode 100644 frontend/src/lib/components/StudyMetaEditor.svelte create mode 100644 frontend/src/routes/study/+page.svelte create mode 100644 frontend/src/routes/study/write/+page.svelte create mode 100644 frontend/src/routes/study/write/[id]/+page.svelte diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md new file mode 100644 index 0000000..813e0bf --- /dev/null +++ b/THIRD_PARTY_LICENSES.md @@ -0,0 +1,34 @@ +# Third Party Licenses + +본 프로젝트는 다음 오픈소스를 사용합니다. + +## perfect-freehand + +- License: **MIT** +- Repository: https://github.com/steveruizok/perfect-freehand +- Used by: `frontend/src/lib/components/HandwriteCanvas.svelte` — Apple Pencil 압력/tilt + 를 반영한 손글씨 stroke 렌더링. + +``` +MIT License + +Copyright (c) 2021 Stephen Ruiz Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4ac90fe..6cffc8f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "dompurify": "^3.3.3", "lucide-svelte": "^0.400.0", - "marked": "^15.0.0" + "marked": "^15.0.0", + "perfect-freehand": "^1.2.3" }, "devDependencies": { "@sveltejs/adapter-node": "^5.0.0", @@ -1850,6 +1851,12 @@ "dev": true, "license": "MIT" }, + "node_modules/perfect-freehand": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.3.tgz", + "integrity": "sha512-bHZSfqDHGNlPpgH2yxXgPHlQSPpEbo+qg7li0M78J9vNAi2yjwLeA4x79BEQhX44lEWpCLSFCeRZwpw0niiXPA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9738654..32c6403 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "dependencies": { "dompurify": "^3.3.3", "lucide-svelte": "^0.400.0", - "marked": "^15.0.0" + "marked": "^15.0.0", + "perfect-freehand": "^1.2.3" } } diff --git a/frontend/src/lib/components/AssetList.svelte b/frontend/src/lib/components/AssetList.svelte new file mode 100644 index 0000000..819ddc6 --- /dev/null +++ b/frontend/src/lib/components/AssetList.svelte @@ -0,0 +1,112 @@ + + +
+
+ 연결된 자료 ({assets.length}) + +
+ {#if assets.length === 0} +
연결된 자료 없음. 오디오/영상/스캔 교재를 연결하세요.
+ {:else} + {#each assets as a (a.id)} + {@const Icon = iconFor(a.asset_type)} +
+ + {labelFor(a.asset_type)} + {#if a.role} + · {a.role} + {/if} + {#if a.asset_type === 'audio'} + + {:else if a.asset_type === 'video'} + video #{a.document_id} 보기 + {:else} + document #{a.document_id} + {/if} + onUnlink?.(a.id)} + /> +
+ {/each} + {/if} +
diff --git a/frontend/src/lib/components/HandwriteCanvas.svelte b/frontend/src/lib/components/HandwriteCanvas.svelte new file mode 100644 index 0000000..8314ca6 --- /dev/null +++ b/frontend/src/lib/components/HandwriteCanvas.svelte @@ -0,0 +1,363 @@ + + +
+ +
+ + + + stroke {strokes.length} +
+ {#if snapshotErr} + {snapshotErr} + {/if} + +
+ + +
+ { if (e.pointerId === activePointerId) endStroke(e); }} + class="block" + style="touch-action: none; cursor: crosshair;" + > +
+
diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte index 9be1f3d..d36750f 100644 --- a/frontend/src/lib/components/Sidebar.svelte +++ b/frontend/src/lib/components/Sidebar.svelte @@ -2,7 +2,7 @@ import { page } from '$app/stores'; import { goto } from '$app/navigation'; import { api } from '$lib/api'; - import { ChevronRight, ChevronDown, FolderOpen, FolderTree, Inbox, Clock, Mail, Scale, StickyNote } from 'lucide-svelte'; + import { ChevronRight, ChevronDown, FolderOpen, FolderTree, Inbox, Clock, Mail, Scale, StickyNote, GraduationCap } from 'lucide-svelte'; let tree = $state([]); let loading = $state(true); @@ -209,6 +209,16 @@ 메모 + + + + 공부 + + + /** + * SourceTextPanel — 학습 세션의 원문/어학 메타 표시 (read-only). + * + * 캔버스 옆 또는 위쪽에 깔린다. 트레이싱 모드(mode='trace')에선 캔버스가 직접 + * traceText 를 배경으로 그리므로 여기선 별도 표시 안 해도 되지만, 표시 보존. + */ + + type Mode = 'copy' | 'trace' | 'blank-repeat' | 'dictation' | 'shadowing' | 'quiz' | 'flashcard'; + + interface Props { + sourceText?: string | null; + sourcePage?: number | null; + metadata?: Record | null; + mode?: Mode; + studyType?: 'certification' | 'language'; + } + let { sourceText, sourcePage, metadata, mode, studyType }: Props = $props(); + + let m = $derived((metadata ?? {}) as Record); + + +
+ {#if sourceText} +
+
+ 원문 {sourcePage ? `(p.${sourcePage})` : ''} +
+
{sourceText}
+
+ {:else} +
원문 미설정 — 메타 편집기에서 입력
+ {/if} + + {#if studyType === 'language' && metadata} +
+ {#if m.reading} +
reading: {m.reading}
+ {/if} + {#if m.meaning} +
meaning: {m.meaning}
+ {/if} + {#if m.unit_type} +
unit: {m.unit_type}
+ {/if} + {#if m.romaji} +
romaji: {m.romaji}
+ {/if} + {#if m.example_sentence} +
+ example: {m.example_sentence} +
+ {/if} +
+ {/if} + + {#if mode === 'trace' && sourceText} +
+ 트레이싱 모드 — 캔버스 배경에 회색으로 깔린 원문 위에 펜으로 덮어쓰세요. +
+ {/if} +
diff --git a/frontend/src/lib/components/StudyMetaEditor.svelte b/frontend/src/lib/components/StudyMetaEditor.svelte new file mode 100644 index 0000000..78b4ad9 --- /dev/null +++ b/frontend/src/lib/components/StudyMetaEditor.svelte @@ -0,0 +1,182 @@ + + +
+ +
+ 도메인 +
+ + +
+
+ + + onPatch?.({ language_code: (e.target as HTMLSelectElement).value || null })} + /> + onPatch?.({ learning_level: (e.target as HTMLInputElement).value })} + /> + {/if} + + onPatch?.({ subject: (e.target as HTMLInputElement).value })} + /> + onPatch?.({ topic: (e.target as HTMLInputElement).value })} + /> + + +