/digest 라우트 신규 — Phase 4 (7일 rolling country×topic batch digest) backend
운영 데이터 사용자 진입점. 최신 1건 (GET /api/digest/latest) 표시 + country
pill 탭 + topic 카드 (rank/label/summary/article_count/importance, fallback
Badge 조건부).
- frontend/src/routes/digest/+page.svelte 신규 (123 LOC) — Svelte 5 runes,
Tabs snippet 패턴, 404 EmptyState 흡수, country reload 보호.
- frontend/src/routes/+layout.svelte nav 1줄 추가 (아침 브리핑 뒤).
후속 별 PR: date picker, article click 라우팅, 국기+한국어 dictionary,
Phase 4.6 feedback loop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
사용자 요청 (2026-05-13):
- 오늘 briefing 만 보여주고 과거 못 보는 게 아쉬움 → 날짜 선택 UI
- 시간대 별 나열은 오히려 불편 → date dropdown 1단계 선택
- 각 카드에 읽음/하이라이트 토글
Schema (migrations 263~266, 단일 statement):
- briefing_topics.is_read BOOL NOT NULL DEFAULT false
- briefing_topics.read_at TIMESTAMPTZ
- briefing_topics.highlighted BOOL NOT NULL DEFAULT false
- briefing_topics.highlighted_at TIMESTAMPTZ
API (app/api/briefing.py):
- TopicResponse 에 id / is_read / read_at / highlighted / highlighted_at 추가
- GET /api/briefing/dates → 사용 가능 날짜 목록 (60일 cap)
· briefing_date / total_topics / total_articles / status / read_count / highlighted_count
- PATCH /api/briefing/topics/{id}/read body {value: bool} → 읽음 토글
- PATCH /api/briefing/topics/{id}/highlight body {value: bool} → 하이라이트 토글
- 토글 시 *_at 컬럼 자동 설정/NULL
UI (frontend/src/routes/news/+page.svelte):
- 헤더 우측 <select> date dropdown — 최신 + N일치 (highlighted_count 별 표시)
- 선택 시 /api/briefing?date=… 로 해당 날짜 briefing 로드
- 카드 우측 상단 ★ (하이라이트) + 읽음 버튼
- 하이라이트 = Card class ring-2 ring-yellow-400
- 읽음 = 외부 div class opacity-60 (시각 차분화, 펴기 가능)
- 토글 즉시 PATCH 호출 + 로컬 state 갱신
each key topic.topic_rank → topic.id 변경 (이미 unique).
PR-2B/2C frontend (commit 4/4). plan v9 Memo Intake Upgrade.
PR-2B 분류 표시 + 1-click promote:
- 메모 카드 상단에 AI 분류 배지 (task/calendar/activity/reference + confidence%)
- ai_event_kind != 'note' 메모 하단에 4 버튼:
· [할 일로] [일정으로] [활동으로] (AI 추천 kind 는 색깔 highlight)
· [그냥 메모] (dismiss → ai_event_kind='note' 강제)
- promote 후 메모 카드에 "→ events #N" link 배지 (사용자 시각 확인)
PR-2C 음성 메모 표시:
- source_channel='voice' 메모는 🎙️ "음성" 배지
- audio player (<audio src=/api/documents/{id}/file?token=>) — 기존 file endpoint 재활용
- STT 대기 중인 voice 메모는 "음성 → 텍스트 변환 대기 중…" placeholder
API helpers:
- promoteMemo(memoId, kind) → POST /memos/{id}/promote-to-event
- dismissEventSuggestion(memoId) → POST /memos/{id}/dismiss-event-suggestion
- voiceAudioUrl(memoId) → /api/documents/{id}/file?token= (access token URL pattern)
Sidebar 영향 0 (events 진입점은 이미 PR-2 에서 추가됨).
원칙 (재명시): AI worker 는 events row 직접 생성 X — 본 UI 의 promote 버튼만이 events 진입.
plan v6 PR-2 scope. 5초 행동 기록 UX 가 핵심 가설.
Backend:
- GET /api/events/{id}/history — events_history timeline 조회 (lifecycle op 자동 기록)
Frontend (SvelteKit 5 runes mode):
- /events 메인 — 4-tab (오늘/Inbox/예정/활동) + 빠른 행동 기록 widget
· 단일 입력 + Enter → POST /api/events kind=activity_log
· status=done + 시간 default 채워짐 (서버 측) → Activity 탭 즉시 반영
· 새 항목을 list 최상단 prepend (refetch 불필요)
· 연속 입력 위해 입력 ref focus 유지
· lifecycle 버튼 (complete/defer/cancel/reactivate) — activity_log 는 lifecycle 대상 X
- /events/[id] 상세 — PATCH 허용 필드 edit (title/desc/시간/priority/project_tag) + history timeline
· PATCH 금지 필드는 UI 노출 X (status/completed_at/cancelled_at/defer_until 은 별 버튼)
- /events/new — kind 선택 (task/calendar_event/activity_log) 후 필드 분기 form
· task: due_at + start_at (선택, "14:00 전화" 같은 시각 task 허용 — 라운드 10)
· calendar_event: start_at 필수 + end_at + all_day
· activity_log: started_at/ended_at 비우면 서버 default now()
- Sidebar 메모 옆에 events 진입점 (CalendarCheck icon)
API helpers: frontend/src/lib/utils/events.ts (createEvent / logActivity / list*
/ lifecycle ops / kind&status enum label/color).
quickref doc: docs/events_api_quickref.md (이전 commit, PR-2 frontend reference).
PR-2 핵심 가설 검증 = 빠른 입력 → 저장 → Activity 즉시 반영 → 새로고침 유지.
PR-1 deferred HTTP behavior 5건도 본 UI 의 자연 사용으로 닫힘.
`<img src=>` 가 Authorization header 를 못 보내서 /api/documents/{id}/images/{key}/raw
가 401 반환 → 이미지 안 보임. 기존 /file?token= iframe 패턴과 동일하게 access token
쿼리 파라미터로 전달.
backend: get_current_user 의존성 제거하고 token 쿼리 파라미터 직접 검증 (기존 /file
엔드포인트와 동일 흐름).
frontend: MarkdownDoc 의 swap selector 가 img.src 에 ?token={getAccessToken()} 부여.
로그아웃 상태면 placeholder 유지.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Markdown Canonical Phase 1B.5 — marker 가 추출하던 이미지를 NAS 에 영구 저장하고
DB 메타 + 인증 라우트 + 프론트 swap 까지 wiring.
핵심 변경:
- marker-service /convert 응답에 base64 image 리스트 포함 (stateless 유지, NAS write 권한 X)
- marker_worker 가 NAS `/documents/extracted_images/{doc_id}/` 에 persist + UPSERT +
고아 row DELETE + md_content ref 를 `docimg:img_NNN` stable scheme 으로 정규화
- /api/documents/{id}/images/{key}/raw 인증 라우트 (Cache-Control private + ETag = content_hash)
- frontend MarkdownDoc 가 placeholder card 안의 docimg ref 를 실제 <img> 로 swap
원칙:
- 이미지 binary = NAS, metadata = Postgres (학습 섹션 패턴 동일)
- image_key sequence 기반 결정적 → 재변환 idempotent
- MARKDOWN_IMAGE_PERSIST=false env 로 rollback 가능 (placeholder card 폴백 자연 유지)
기존 28건 marker success 문서는 본 PR 에서 건드리지 않음 — deploy + 신규 업로드 1건 +
sample 5건 검증 후 scripts/marker_reprocess_existing_success.py 로 targeted reprocess.
plan: ~/.claude/plans/piped-humming-crystal.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1B marker_worker 결과(현재 success 29건, 전부 PDF)를 사용자 흐름에
연결하고 1D pilot 품질 평가 데이터를 확보하기 위한 viewer 마무리 작업.
빠진 부분 3가지를 닫는다:
1) PDF viewerType 기본 view = Markdown
- md_status='success' AND md_content 비어있지 않음일 때 MarkdownDoc 기본 표시.
- 사용자가 "PDF 원본" 토글 시 iframe.
- pdfViewMode 초기화는 doc.id 변경 시에만 (lastDocId tracker) — reactive cycle
이 사용자 토글을 덮어쓰지 않도록 보호.
- markdown 사라지는 케이스(success → failed 재처리)는 자동으로 pdf 로 보호.
2) Image renderer → placeholder card (docMarkdown.ts)
- md_content 의 69%(20/29)에 image syntax 포함. asset serving(1B.5) 미구현
상태에서 raw <img> 를 emit 하면 깨진 아이콘 → 1D pilot 평가가 markdown
품질이 아닌 viewer 미완성 문제로 오염됨.
- href / alt / basename 모두 escape 후 figure.md-image-placeholder 로 렌더.
- 원본 src 는 data-md-image-src 에 escape 보존 → 1B.5 ImgAuth selector 로
실제 <img> 로 교체할 entry point 마련.
- DOMPurify ADD_ATTR 에 data-md-image-src 추가.
3) MarkdownStatusBadge (신규) — 4-state badge
- pending 숨김(legacy 9792건 시각 노이즈 회피).
- processing/success/skipped/failed 표시.
- success tooltip: md_extraction_quality 의 metrics raw 일부
(markdown_heading_count / markdown_table_row_count / markdown_image_count /
text_length_ratio / warnings) 만 노출. text_length_ratio / null /
metrics nested / flat fallback 모두 방어.
- skipped/failed tooltip: md_extraction_error 또는 정책 문구.
- MarkdownDoc 내부 + PDF iframe fallback 양쪽에서 재사용 → failed 같이
MarkdownDoc 가 안 렌더되는 경로에서도 사용자가 상태를 알 수 있음.
기존 markdown/hwp-markdown/article 분기에도 mdExtractionQuality prop 전달.
Out of scope (1B.5 또는 후속):
- ImgAuth blob URL 실제 wiring (data-md-image-src selector + Bearer raw)
- /data/assets/<doc_id>/ 저장 + 서빙
- Caddy /data/assets/* 라우팅
- localStorage 사용자 view preference 저장
- side-by-side viewer (1D pilot 결과 본 후)
- quality chip 별도 UI (1D 후)
Verify:
- npm run build 통과
- npm run lint:tokens 신규 파일 위반 0
- 관련 plan: ~/.claude/plans/iterative-nibbling-catmull.md
- pre-flight: md_extraction_quality 실제 shape 확인 ({score, metrics:{...}, warnings:[]})
Risks:
- feature/design-system worktree 가 [id]/+page.svelte 의 stale 버전 보유
(main 보다 212 commits behind, MarkdownDoc 부재). 1C 머지 후 worktree
머지 시 conflict 확정 — 그쪽 rebase 필요 (별건).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
결과 화면에서 사용자가 [AI 해설 보기] 누를 때 캐시 hit/miss 가 불투명함.
헤더에 한 줄 indicator 추가 — 오답·모르겠음 대상 N건 중 ready 박힌 카운트
+ 진행 중/실패/자료 부족 분포.
Backend (study_topics.py get_quiz_session):
- questions[i].ai_explanation_status 응답에 추가 (q.ai_explanation_status 그대로)
· frontend 가 attempts.outcome (wrong/unsure) 와 결합해 카운트
Frontend (quiz-sessions/[sid]/+page.svelte):
- $derived aiExplProgress — wrong/unsure attempts 와 question.ai_explanation_status
결합 카운트 (target / ready / pending / failed / skipped)
- 헤더에 Sparkles 아이콘 + "AI 풀이 자동 생성: N/M (P%)" 한 줄
· pending > 0: "생성 중 N" (warning 색)
· failed > 0: "실패 N" (error 색)
· skipped > 0: "자료 부족 N" (dim)
· 셋 다 0인데 ready < target: "대기열 처리 대기" (worker 1분 주기 안내)
이 indicator 는 GET fallback enqueue 와 함께 작동 — 결과 화면 진입 시점에
backfill 이 누락된 wrong/unsure 가 이미 enqueue 되고, 1분 주기로 ready 박힘.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4-A 가 wrong/unsure 풀이를 background batch 로 캐시하는데, 사용자/운영자
입장에서 (1) 지금까지 얼마나 캐시 채워졌는지, (2) 환각 차단/파싱 실패/자료 없음
같은 worker 결과 분포를 볼 수 없었음. 통계 대시보드에 카드 추가.
Backend (study_question_progress.py /stats):
- StatsAiExplanation 신규 응답 섹션
· status_distribution — 토픽 전체 study_questions.ai_explanation_status 분포
(none/ready/failed/skipped/stale/pending 6 키 default 0)
· target_total / target_ready — wrong/unsure progress 의 ready 비율
(캐시 hit 가능성 추정 핵심 지표)
· recent_jobs — 최근 7일 study_question_jobs 의 (status, error_code) 분포
('completed', 'failed:guard_fail', 'failed:parse_fail', 'skipped:evidence_missing'
같은 합성 키)
Frontend (/study/topics/[id]/stats):
- 신규 Card "AI 풀이 캐시" — Sparkles 아이콘
· 큰 숫자 + 진행률 바: ready / wrong+unsure
· 토픽 전체 status 분포 inline (한국어 라벨)
· 최근 7일 worker 결과 grid (환각 차단 / 파싱 실패 / 자료 없음 skip 등 분리)
- statusLabel / jobLabel 헬퍼 — 운영자 친화 한국어
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
사용자가 며칠 안 들어오면 due_today 가 누적되어 학습 페이스 압박. Phase 1
plan 위험 항목 처리. 자동 batch 대신 사용자 명시 액션으로 통제권 보장.
Backend:
- POST /study-topics/{tid}/review-queue/redistribute — overdue 를 round-robin
분산. days_offset = i % spread_days + 1 (오늘 + 1~7일). 같은 날 안에서도
i*7분 spread 로 시간 분산. review_stage 는 보존 (재배치만, stage 리셋 X).
body { spread_days: 1~14, default 7 }. 응답 { redistributed_count, spread_days }.
- GET /review-queue?tab=due_today 응답에 overdue_count: int 옵션 필드 — UI 가
경고 + [정리] 노출 판단. due_at < today 0시 (UTC) + stage<4 카운트.
Frontend (review-queue):
- due_today 탭에서 overdue_count>0 시 노란 banner — "정체 N건" + [정리] 버튼.
- 정리 클릭 → confirm → POST → toast (N건을 7일에 분산) → 카운트/목록 reload.
- 다른 탭에서는 banner 미노출 (backend 가 overdue_count=0 응답).
Plan: ~/.claude/plans/crispy-petting-dijkstra.md (Phase 2-F)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
복습함 카드 단위 체크박스 + sticky bottom bar 로 N개 골라 한 quiz_session.
backend QuizSessionStartRequest 에 question_ids 파라미터 추가 — 우선순위
stage > question_ids > 기존 subject 경로. 명시되면 selection 우회 + 검증
(user × topic 소속 + 미삭제 + 최대 200 + 중복 제거 순서 보존).
Backend:
- question_ids: list[int] | None — Field 한도 200
- valid_set 검증: 다른 user/topic 또는 deleted_at 인 qid 는 silent drop
- subject_distribution 자동 계산 (결과 카드용)
- 빈 wanted / 무효 qid → 400
Frontend (review-queue 페이지):
- 카드 좌측 체크박스 (분리 영역, 본문 클릭은 기존대로 문제 페이지)
- "이 페이지 전체 선택 / 해제" 토글
- 선택 N>0 시 sticky bottom bar — `{N}개 풀이 시작` 버튼
- 탭 변경 시 선택 초기화 (다른 의도 묶음 가능성)
- 페이지 이동 시 선택 유지 (Set<question_id>)
- 진행 중 in_progress 세션 있으면 confirm 후 abandon
- 200 한도 도달 시 toast 경고
Plan: ~/.claude/plans/crispy-petting-dijkstra.md (Phase 2-E)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
review-queue API (Phase 1) 를 사용한 복습함 페이지 신규.
탭: 오늘 할 일 (due_today) / 미확인 (pending_review) / 반복 오답 (chronic) /
퇴행 (regressed) / 학습완료 (mastered).
- 신규 라우트: /study/topics/[id]/review-queue
- 5탭 sticky + 카운트 배지 (page_size=1 5회로 카운트만 빠르게 — backend 변경 0)
- 페이지네이션 (page_size=50, ?page= URL 동기)
- ?tab= URL 동기 (새로고침/뒤로가기 보존, replaceState 사용)
- 카드 클릭 → 개별 문제 페이지 이동 (멀티 셀렉트 풀이는 후속)
- 진입 동선: 결과 화면 "바로 할 일" 콜아웃 → 해당 탭으로 directlink,
결과 화면 footer + 토픽 페이지 헤더에 [복습함] 버튼 추가
Plan: ~/.claude/plans/crispy-petting-dijkstra.md (Phase 2-C)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 finalize 가 계산하던 SessionSummary 가 응답에 포함되지 않고 discard
되던 것을 quiz_session row 4 컬럼으로 영속화. 결과 화면 헤더에 회복/퇴행/
새로 맞힘/반복 오답 누적 변화 카운트 + "바로 할 일" 콜아웃 (지금 시점
progress 기반 동적 카운트 — pending_review/chronic/regressed). 동적 카운트는
결과 GET 호출 시점에만 계산 (목록 endpoint 비용 회피).
확인완료 통합 — 결과 카드의 [학습완료] 버튼이 attempts.reviewed_at 만 박던
것을 progress.last_reviewed_at + (wrong/unsure 면 due_at 최초 부여) 도 같이
박도록. reviewed=false 토글은 attempts 만 되돌림 (다른 attempt 가 검토 표시
했을 수 있어 progress 의 last_reviewed_at 은 보존).
- migrations/230 — quiz_sessions 4 컬럼 ADD (단일 ALTER TABLE)
- StudyQuizSession 모델 + finalize_session 가 row 영속화
- QuizSessionSummary 응답에 4 스냅샷 + 3 동적 필드 (default 0)
- _build_session_summary include_progress_counts=True 시 SQL 3회
- review-mark 가 reveiwed=true 시 progress 동기화
- 결과 화면: 헤더 변화 카운트 줄 + 바로 할 일 콜아웃 (값 있을 때만)
Plan: ~/.claude/plans/crispy-petting-dijkstra.md (Phase 2-B)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vision 의 단일 풀이 진입점 — 옵션 토글로 학습 단계 (입문/학습 중/시험 직전) +
분량 (30/50/100) 선택. Phase 1-E bucket+stage 알고리즘과 매칭.
- 학습 단계 3 카드 + 분량 3 토글이 메인 옵션
- 단계 선택 시 분량 토글 노출
- 단계 미선택 시 "고급 옵션" collapsible — 기존 PR-12-B subject 단위 출제 호환
- 시작 버튼 disabled 상태 가이드 (단계 선택 또는 고급 옵션 펼침 필요)
서버 호출:
- optStage 있으면 { stage, size, abandon_existing } body
- 없으면 기존 { target_per_subject, subject, wrong_only, abandon_existing }
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
문서 상세 페이지에서 canonical markdown(md_content) 을 우선 렌더하고
없으면 extracted_text fallback. md_frontmatter 가 있으면 본문 위에 메타
박스. h1~h6 에 GFM heading id + hover 시 # 링크 표시. 이미지 alt 가
있으면 figure + figcaption. KaTeX 수식 ($...$ / $$...$$) 지원.
Backend:
- DocumentDetailResponse 신규 (DocumentResponse + extracted_text + md_*)
- GET /documents/{doc_id} 응답 모델 전환
- 리스트 응답은 DocumentResponse 그대로 (페이로드 비대화 회피)
Frontend:
- lib/utils/docMarkdown.ts — 별도 Marked 인스턴스 (study mathMarkdown.ts
영향 0). marked-katex-extension + marked-gfm-heading-id + custom image
renderer (figure/figcaption + data-md-img marker).
- lib/components/MarkdownDoc.svelte — md_content/extracted_text 우선순위,
frontmatter 박스, mdStatus=failed 안내 배지, heading anchor DOM 후처리.
- /documents/[id] markdown / hwp-markdown / article viewer 3 곳 wiring.
- app.css — .markdown-doc heading-anchor / md-figure / katex 가로 스크롤.
이미지 ImgAuth 후처리(blob URL 교체) wiring 은 Phase 1B.5 에서. 현재는
data-md-img="1" 마킹만 두고 marker 출력 src 그대로.
Plan: ~/.claude/plans/plan-idempotent-sundae.md (Phase 1C)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회차 카드 expand 시 100개 문제 카드가 viewport 에 들어오는 즉시 SvelteKit 이
question 라우트의 코드 chunk (KaTeX/marked/DOMPurify) prefetch 시작. 카드 클릭
시점엔 이미 파싱 완료 상태.
데이터(`/study-questions/{qid}`)는 hover 시점에만 prefetch — 카드 100개 전체
스캔이 100번의 데이터 fetch 가 되지 않게 분리.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
처음 문제 진입 시 KaTeX + marked + DOMPurify 등 무거운 chunk 가 lazy load 되어 느림.
다음/이전 버튼은 같은 번들 재사용이라 빠름. 카드 hover 시점에 prefetch 시작 →
클릭 시점엔 이미 파싱 완료된 상태.
app.html body 에 data-sveltekit-preload-code/data="hover" 추가 (전역).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- load() 가 q 도착 후 related-types/siblings Promise.all 까지 기다려서 loading=false → 빈 카드 노출 시간이 셋 중 가장 느린 것 기준으로 늘어남
- q 직후 loading=false, 나머지 두 fetch 는 fire-and-forget
- related 섹션 자체 relatedLoading, prev/next 는 siblings 비면 안 보여 UX 영향 0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- onMount: await loadTopic(); await load(); → Promise.allSettled 병렬화
- 같은 회차 안 prev/next 이동 시 page_size=200 batch fetch 반복 제거
- module-level Map 캐시, key=topicId:encodeURIComponent(exam_round)
- TTL 5분, get/set 시 얕은 복사로 참조 공유 차단
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- round_count 별 등급 매핑 (단골/잘 나오는 반복 출제/반복 출제/신출/빈출)
- ≥7 단골, 5–6 잘 나오는 반복 출제, 3–4 반복 출제,
2 + max(연도)≥2024 신출, 2 + 모두 옛 빈출
- SIMILAR_THRESHOLD 0.88 → 0.85 (5-source 분포 측정 결과 자연 갭 위치 반영)
- API 응답 + 프론트 3곳 (보기/통합뷰/결과 카드) 라벨 일괄 통일
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AI 응답이 마크다운 자체를 \`\`\` 으로 감싸서 오는 패턴 (시작만 있고 닫음 누락 포함)
때문에 explanation/AI 해설 영역이 raw 코드블록으로 보이는 회귀.
- frontend/lib/utils/mathMarkdown.ts: stripOuterFence helper.
- terminated wrap 처리 (inner 에 \`\`\` 추가 있으면 보존)
- unterminated 처리 (백틱 그룹 == 1 인 경우만 안전하게 unwrap)
- 본문 중간 정상 코드블록은 보존
- scripts/strip_outer_fences.py: dry-run + --apply 양 모드.
- 5개 필드 (question_text, choice_1~4, explanation, ai_explanation, content) 검사.
- 운영 결과 explanation 34건 unwrap 적용 완료, recount 0 검증.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이전 fix(4c26b91)는 query 회차명이 examRounds 에 없을 때 mode='new' 자동 전환했지만,
사용자 화면은 여전히 select 모드 노출 (캐시 또는 동선 이슈). 더 직관적인 방식으로 수정:
- onMount 의 mode='new' 자동 전환 제거.
- select dropdown markup 에 query 회차가 examRounds 에 없으면 "(신규)" 라벨 옵션 추가.
- 사용자는 select 모드 그대로 유지하면서 신규 회차도 보임. 폼 제출 시 그 값 그대로 박힘.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회차 카드 페이지의 [새 회차 시작] → /questions/new?exam_round=...&start_qnum=1 진입 시
query 의 회차명이 기존 examRounds 에 없으면 (신규 회차라 등록된 문제 0개) select dropdown
옵션에 매칭이 없어서 회차 정보가 표시 안 되는 회귀.
onMount 에서 query 회차명이 examRounds 에 없으면 mode='new' + f_exam_round_new prefill.
사용자가 신규 회차로 입력한 이름이 그대로 폼에 박힘.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
학습 의미가 회차 간 반복성 — 차단/제거가 아니라 패턴 표시 frame.
- 신규 service `related_types.py` — threshold/회차 필터/round_count 계산 공유
- REPEAT >= 0.95 / SIMILAR 0.88~0.95
- 회차 조건 백엔드 강제 (자기 자신/같은 회차/null exam_round candidate 제외)
- round_count: related_count == 0 → 0 (현재 회차만 1로 채우지 않음)
- GET /study-questions/{qid}/related-types — 단건 분류 (repeat_questions / similar_questions)
- POST /study-topics/{tid}/related-types-bulk — 카드 배지용 카운트 batch
- 비교 대상 = 토픽 전체 ready pool (입력 qid 끼리 비교 X)
- 응답 키 보존 — 권한 없음/임베딩 미준비 등도 (0,0,0,0)
- 보기 페이지: PR-11 비슷한 문제 토글 제거 + 🔥 반복 출제 / 🧩 유사 유형 두 섹션 자동 노출
- 헤더 = round_count "N개 회차", 본문 위 = related_count "관련 N문제"
- source_status / source_exam_round 안내 분기
- 결과 페이지 (틀린/모르겠음 카드): bulk 호출 후 round_count >= 2 일 때만 배지
- 통합뷰 회차 expand 시 lazy bulk 호출 — 같은 회차 캐시
- 기존 /similar 엔드포인트 유지 (raw 디버깅용)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tailwindcss/typography 플러그인 미설치 — prose prose-sm prose-invert
max-w-none 클래스가 무효라 결과 페이지(특히 모르겠음·틀림 카드)와 풀이 페이지의
질문 본문/사용자 해설/AI 해설/분야 설명에서 마크다운 스타일링이 안 먹었음.
이 codebase 의 정식 마크다운 클래스는 src/app.css 에 정의된 .markdown-body
(h1~h4, ul/ol, blockquote, code, pre, table, hr 등 완비). 모든 renderMathMarkdown
컨테이너에 markdown-body + math-area 두 클래스 적용.
영향 파일:
- review/+page.svelte (풀이 중 본문)
- quiz-sessions/[sid]/+page.svelte (결과 카드 expand 시 본문/해설/AI/분야설명)
- questions/[qid]/+page.svelte (보기 페이지)
- questions/[qid]/edit/+page.svelte (편집 페이지의 AI 풀이 미리보기)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 통합뷰 문제 섹션: 평면 리스트 → 회차별 아코디언 (디폴트 모두 접힘)
- 회차 정렬: "YYYY년 N회" 파싱 → year desc / round desc (localeCompare 단독 회귀 차단)
- 회차 행 라벨: "총 시도 N건 · 마지막 결과: 정답 K / 오답 M" (누적/마지막 혼동 회피)
- 회차 미지정 그룹은 노란 톤 + 안내, 표시 문자열은 UI 전용 (원본 NULL 분리)
- 본문 / [편집] 링크 구조 분리로 이벤트 버블링 충돌 차단
- /study/topics/{tid}/questions/{qid} 신규 — KaTeX 마크다운 렌더 + 정답 표시 +
AI 해설 5상태 (idle/loading/success/stale/error) + 비슷한 문제 + prev/next
- prev/next URL 직접 접근 — 단건 fetch + 같은 회차 목록 fetch 자체 처리
- page_size=200 만땅 + total>200 시 토스트 안내 (조용히 자르지 않음)
- 사용자 입력 해설/이미지 없으면 섹션 숨김, exam_round NULL 이면 prev/next 비활성
- StudyTopicQuestionSummary 에 exam_question_number 추가 (회차 안 정렬 키)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- study_quiz_sessions 테이블 (한 토픽 in_progress 1개 partial unique)
- study_question_attempts 에 quiz_session_id + reviewed_at 컬럼
- 풀이 진행률 서버 단일 진실 (cursor) — 나갔다 와도 이어풀기 가능
- 통합뷰: 진행 중 카드(이어풀기) + 최근 완료 결과 카드(미확인 N건 배지)
- 신규 /quiz-sessions/[sid] 결과 페이지 (3 카테고리 + AI 해설 + 분야 설명 + 학습완료 토글)
- /review 페이지는 풀이만, 마지막 문제 풀이 후 결과 페이지로 redirect
- 마이그레이션 206~209 (single-statement, asyncpg 호환)
- API: POST/GET/PATCH /study-topics/{tid}/quiz-sessions(/{sid}),
PATCH /study-question-attempts/{aid}/review-mark
- AttemptCreate.quiz_session_id 추가 — submit_attempt 가 같은 트랜잭션에서
세션 cursor + count 증가, 마지막이면 status='done' + finished_at
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 라벨 "복습 시작" → "문제풀이"
- attempts.outcome 컬럼 + selected_choice nullable (correct/wrong/unsure)
- 풀이 중 정답·해설·AI·비슷한 문제 모두 비노출, 답 클릭 시 자동 진행
- "모르겠음" 5번째 옵션 추가
- 결과 화면 = 정답/틀린/모르겠음 3 카테고리 탭, 카드 클릭 expand
- 틀린 → PR-3 AI 해설 (RAG)
- 모르겠음 → 분야(subject+scope) 설명 AI 즉석 생성 + 캐시 (PR-9 신규)
- 분야 설명 RAG: 매핑 documents 청크 + 같은 분야 다른 문제·해설 → bge-reranker
- 마이그레이션 200~205 (single-statement, asyncpg 호환)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
문제별 N개 이미지 첨부. 회로도/그래프 등이 필요한 시험 문제 지원.
입력·편집·복습 모두에서 표시.
데이터 모델 (migration 198):
- study_question_images: id, user_id FK CASCADE, study_question_id FK CASCADE,
file_path, file_size, mime_type, sort_order, created_at
- partial idx (study_question_id, sort_order, id)
저장: NAS /documents/study_question_images/{topic_id}/{qid}/{img_id}.{ext}
file_watcher 가 보는 PKM 경로와 분리 — 자동 인덱싱 안 됨.
API:
- POST /api/study-questions/{qid}/images (multipart, MIME PNG/JPEG/WEBP/GIF,
10MB/파일 제한, sort_order 자동 max+1)
- GET /api/study-questions/{qid}/images/{img_id}/raw (FileResponse, Bearer 인증)
- DELETE /api/study-questions/{qid}/images/{img_id} (DB row + 파일 시스템 정리)
- StudyQuestionResponse / ReviewQuestionItem 응답에 images 배열 포함
- StudyQuestionSummary 응답에 has_images bool 추가
프론트:
- 신규 lib/components/ImgAuth.svelte — Bearer 인증 endpoint 의 이미지를 fetch +
blob URL 로 변환해 <img> 표시. unmount 시 URL.revokeObjectURL.
- /questions/new: 입력 폼에 이미지 dropzone (client-side 보유) → POST
/questions 받은 qid 로 자동 multipart 업로드. "저장 후 계속 입력" 시 reset.
- /questions/[qid]/edit: 별도 카드 — 기존 이미지 grid + 추가/삭제. 즉시 업로드.
- /review: 문제 본문 아래 이미지 grid (max-h-72 object-contain).
- 모든 표시는 ImgAuth 컴포넌트 — accessToken 만료 케이스 대비.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이전 fix(effect→onchange)에도 race 재발 (id 306,307 qnum=1,2 로 또 들어감).
근본 해결 — 클라이언트의 f_qnum 표시값과 실제 저장값을 분리.
변경:
- f_qnum_user_edited dirty flag 추가
- input 에 oninput → user_edited=true (사용자가 직접 박스 수정한 경우)
- onMount fallback / onRoundChange / applyNewRound / 저장 후 → user_edited=false
- POST body 의 exam_question_number: user_edited=true 면 명시 전송, false 면
null → 서버가 같은 회차 max+1 자동 채움 (PR-6 의 기존 서버 로직)
- POST 응답의 실제 저장 qnum 으로 화면 동기화 (saved.exam_question_number)
→ 표시값이 어긋났어도 저장 후 정확하게 갱신
- applyNewRound 에서 이미 존재하는 회차명 입력 시 next_question_number 적용
(사용자가 dropdown 대신 새 회차 모드로 같은 이름 다시 입력해도 1번부터 다시 시작 X)
이제 클라이언트가 어떤 표시값을 보여주든 실제 저장은 항상 정확. 사용자가
직접 박스를 수정한 경우만 명시 전송.
$effect 가 examRounds fetch 전 첫 실행되며 f_qnum=1 로 reset 하는 race 가
이전 fix(lastExamRound sync) 만으로 완전히 막히지 않음. effect 자체를
제거하고 select onchange + applyNewRound 에서 명시 호출하는 onRoundChange()
로 변경. examRounds 미적재 시 (length=0) 는 skip — onMount fallback 이 처리.
이제 흐름:
- 진입 (sessionStorage prefill 만 있음) → onMount await fetch 후 fallback
으로 next_question_number 적용
- dropdown 으로 회차 변경 → onchange={onRoundChange}
- 새 회차 입력 → applyNewRound() 안에서 직접 f_qnum=1
- examRounds 변경 (저장 후 refreshExamRounds) → 어떤 자동 reset 도 발생 안 함
$effect 가 examRounds fetch 전에 첫 실행되면 found=undefined → f_qnum=1
로 강제 reset. 그 후 examRounds fetch 완료해도 effect 재실행 안 돼서
사용자가 그대로 입력 시작 → 회차 안 문항 번호 중복 (1,2,3,1,1,...) 발생.
수정:
- applyQueryParams() 가 start_qnum 명시 여부 boolean 반환
- onMount 에서 await loadTopicAndRounds() 후 explicit start_qnum 없고
f_exam_round 가 있으면 examRounds.find().next_question_number 명시 적용
- lastExamRound 를 현재 값으로 sync — $effect 첫 실행이 또 reset 안 함
이미 발생한 데이터(중복 qnum) 는 사용자 직접 정정 또는 별도 SQL 보정 필요.
이 fix 후 새로고침/재진입 시에는 정상 next 적용.
기사시험 문제·해설에 √, ρ, R̄, α/β/γ, ㎥, $\\sqrt{...}$ 같은 수식이 자주
들어가는데 기존 plain text 표시는 LaTeX 문법이 그대로 노출되거나 깨짐.
표시·미리보기 영역에서만 KaTeX 렌더링 (입력 textarea 는 plain text 유지).
의존성: marked-katex-extension + katex (frontend/).
공통 유틸 frontend/src/lib/utils/mathMarkdown.ts:
- renderMathMarkdown(text): block 렌더 (문제 본문·해설·AI 해설용)
- renderMathMarkdownInline(text): inline parseInline (보기 1~4 button 안)
- 별도 marked 인스턴스 사용 → 글로벌 marked 영향 없음
- $...$ inline / $$...$$ block 모두 지원
- KaTeX throwOnError=false → 잘못된 수식은 빨간색 fallback (페이지 안 깨짐)
- DOMPurify USE_PROFILES.html + ADD_ATTR style/aria-hidden + FORBID
script/iframe/onclick 등 — XSS 차단 유지
- 실패 시 text-only fallback (HTML escape)
CSS (app.css):
- .math-area .katex-display { overflow-x: auto } — 모바일 가로 overflow
생기면 수식만 가로 스크롤, 페이지 레이아웃 보존
- .katex { white-space: nowrap } — KaTeX 자체 줄바꿈 방지
적용 위치 (표시·미리보기만, textarea 무변경):
- review: 문제 본문, 보기 1~4(inline), 답 제출 후 explanation, AI 해설
- edit: AI 해설 본문 (기존 marked → 통일)
- new 화면 preview / 통합뷰 카드 snippet: 무변경 (1차 보류, 사용자 요청 시 추가)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
기존엔 whitespace-pre-line 으로 plain 표시 → '**굵게**' 같은 markdown 문법이
그대로 노출. DocumentViewer 와 동일한 marked + DOMPurify 패턴 적용. prose
타이포그래피 클래스로 list/heading/inline 코드 스타일 자동.
기사시험 회차별 100문제 채워가기 시나리오. 문제 입력 페이지를 단순 폼에서
"회차 진행률 추적·재개" 도구로 보강.
데이터 모델 (migrations 195~197):
- study_topics: exam_round_size INT CHECK 1~300 (회차당 문항 수, NULL=미설정)
+ exam_subjects JSONB DEFAULT '[]' (과목 리스트, 입력 페이지 드롭다운 옵션)
- study_questions: exam_question_number SMALLINT CHECK >0 (회차 안 문항 번호)
- partial idx (study_topic_id, exam_round, exam_question_number) WHERE
deleted_at IS NULL AND exam_round IS NOT NULL — 회차별 max+count 고속화
백엔드:
- POST /questions: exam_round 명시 + exam_question_number 미명시 시 서버가
같은 토픽·회차의 max+1 자동 채움
- 신규 GET /api/study-topics/{id}/exam-rounds: 회차별 진행률 집계
{exam_round_size, items: [{exam_round, question_count, max_question_number,
next_question_number, is_complete}]}
- StudyTopic Create/Update/Response/Meta 에 exam_round_size·exam_subjects
- StudyQuestion Create/Update/Response 에 exam_question_number
- exam_question_number 변경은 embedding stale 트리거에서 제외 (의미 영향 없음)
프론트:
- 토픽 생성/편집 모달: "시험 정보" 섹션 (회차당 문항 수 + 과목 리스트
+추가/제거 칩)
- /study/topics/[id]/exam-rounds 신규 페이지: 회차 카드 + 진행 바 +
[N번부터 이어서] 버튼 + [새 회차 시작] 모달
- 통합뷰 문제 섹션 헤더에 [회차 보기] 진입점
- /questions/new 페이지 전면 개편:
- 시험명 = topic.name 자동 prefill
- 과목 드롭다운 (topic.exam_subjects + 기존 distinct, "직접 입력" 토글)
- 회차 드롭다운 (기존 distinct + "새 회차")
- 문항 번호 자동 (회차 선택 시 next_question_number, 새 회차 = 1)
- 진행률 바 (현재/exam_round_size)
- 출처/메모 자동 합성 "회차 N번" (수정 가능)
- "저장 후 계속 입력" → 본문/보기/정답 reset, 회차 유지, 문항 +1
- 회차 변경 감지 시 문항 번호 1로 reset
- exam_round_size 도달 시 회차 강조 + "저장 후 계속 입력" 비활성
- query string ?exam_round=&start_qnum= 지원 (회차 목록에서 재개 진입)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
study_questions 자동 임베딩(PR-4 bge-m3 1024차원) 기반 cosine 유사도
top-K. 26B 호출 없음, vector search 만. additive UI — 기존 입력·복습
흐름 영향 없음.
백엔드: GET /api/study-questions/{id}/similar?limit=5&topic_only=true
- 자기 자신/soft-deleted/embedding_status!=ready 제외
- topic_only=true (default) 면 같은 study_topic 안에서만
- 응답: items[{id, question_text(80자 truncate), subject, scope, exam_round,
similarity(1-cosine), attempt_count, last_correct}], source_status, source_id
- 현재 문제 embedding 미생성/실패/stale 시 빈 결과 + source_status 안내
- attempt_count + last_correct batch 조회 (N+1 회피)
프론트:
- 편집 화면(/questions/[qid]/edit): 페이지 로드 시 자동 GET /similar →
카드 5개. 본문 truncate + subject/scope/exam_round + 유사도 % + attempt
배지 (정/오답 아이콘). 카드 클릭 시 해당 문제 편집 페이지로 이동.
- 복습 화면(/review): 답 제출 후 "비슷한 문제 보기" 토글 → expand 5개 카드.
같은 형태. 다음 문제로 cursor 이동 시 자동 닫힘.
- 통합뷰: 변경 없음 (이미 편집 진입점이 시각적 cue 역할).
source_status별 안내 (pending/failed/stale/none): 임베딩이 아직 준비 안 됐을
때 "약 1분 안에 cron 자동 처리" 메시지 노출.
후속 PR 예정: subject/scope 자동 추천(PR-6), 오답노트/통계(PR-7),
AI 풀이 idle batch(PR-8). 현재 PR-5 는 vector search 결과 노출까지만.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
복습 답 제출 후 또는 편집 화면에서 사용자가 명시적으로 누를 때만 AI 가
4지선다 풀이 생성. 자동 일괄 생성 금지 (하루 100문제 입력 시 MLX 부하·
잘못 입력 문제 해설 위험).
데이터 모델 (migrations 191~192):
- study_questions 4 컬럼 추가: ai_explanation TEXT, ai_explanation_status
VARCHAR(20) DEFAULT 'none' (none/pending/ready/failed/stale),
ai_explanation_generated_at, ai_explanation_model
- partial idx (study_topic_id, ai_explanation_status) WHERE status != 'none'
PATCH stale 자동 전이: question_text/choice_*/correct_choice 변경 시
status='ready' 만 'stale' 로. 본문은 보존, UI 배지 + "다시 생성" 동선.
신규 엔드포인트: POST /api/study-questions/{id}/ai-explanation
- regenerate=false + ready/stale → 캐시 즉시 (MLX 호출 없음, is_stale 플래그)
- pending → 409 (race-safe 조건부 UPDATE 로 동시 호출 차단)
- 그 외 → 새 생성
RAG 입력 풀:
- 1순위: study_topic 매핑 documents 청크 + ai_summary, bge-reranker top-5
- 2순위: 같은 토픽 다른 questions (자기 자신 제외, ai_explanation 은 ready
상태만 포함 — 재귀적 hallucination 방지), reranker top-3
- 제외: 필기 OCR / 외부 웹 / Premium 모델
모델: Mac mini MLX gemma-4-26b primary 단독. get_mlx_gate() Semaphore(1) 경유,
30s timeout. 실패 시 status='failed' + 직전 본문 보존.
프롬프트 (app/prompts/study_question_explanation.txt): 자료 우선순위·인용
형식·할루시네이션 방지 절대 규칙 (법령명·조항·수치·표준 번호 단정 금지,
"자료에서 확인되지 않음" 명시).
프론트:
- 복습 화면 답 제출 후 인라인 expand. status별 버튼 분기 (ready 캐시 /
stale "이전 풀이"+"다시 생성" / failed "다시 시도")
- 편집 화면 별도 카드. 상태 배지 + "이전 풀이 보기" / "다시 생성" 분리
- 참고 근거 토글 (source_type 별 아이콘 📄/❓ + 제목 + snippet)
후속 PR 보류: 오답노트/통계, AI 일괄 백그라운드 생성, 필기 OCR RAG,
Premium/Claude 재생성, /api/search/ask retrieval scope 통합.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
가스기사처럼 한 워크스페이스에 273건 자료가 묶이면 평면 리스트로 쭉 나열
되어 통합뷰가 무너졌음. /study/topics/[id] 자료 섹션을 자료실 카테고리
경로 기반 트리로 그룹핑하고 노드별 접기/펼치기 도입. 기본값 모두 접힘.
백엔드: StudyTopicDocumentSummary 에 library_paths(`@library/<path>` 태그
에서 prefix 제거) 필드 추가. 그룹핑은 첫 path 만 사용 (단순화).
프론트: documents 를 path segment 별로 트리 빌드 → snippet 재귀 렌더링.
헤더에 "자료 N개 · 카테고리 K개 · [모두 펼치기/접기]" 컨트롤. 분류 없는
자료는 "분류 없음" 그룹으로 별도. 자료 0건 path 는 자동 누락.
필기/문제 섹션은 분류축이 달라(certification/subject vs subject) 동일
트리 못 쓰므로 본 PR 범위 밖. 후속에서 패턴 일관성 검토.
study_topic 워크스페이스에 4지선다 문제은행 자산 트랙 추가. 기사시험 필기
대비 시나리오 — 빠른 반복 입력 + 과목별 균등 추출 복습 + 정오답 누적.
데이터 모델 (migrations 186~190):
- study_questions: study_topic 1:N, soft delete, is_active 토글, correct_choice
SMALLINT CHECK 1~4
- study_question_attempts: 답 제출 1행 누적. study_question_id FK는 ON DELETE
RESTRICT (이력 보존 원칙 — hard delete 실수로 풀이 기록 소실 차단)
설계 원칙:
- 문제 삭제는 API 에서 soft delete only. attempts FK RESTRICT 로 DB 레벨도 보호
- correct_choice 변경 시 기존 attempts.is_correct 재계산 안 함 (시점 사실 보존)
- 복습 default = 과목별 target_per_subject(20) 무작위 균등 추출. 한 과목이
부족하면 가용한 만큼만
- wrong_only=true 정의 = 가장 최근 attempt 가 오답인 문제 (latest-wrong, ever-wrong 아님)
- 출제 응답에서 정답·해설 비공개. 답 제출 시점에만 노출
- subject/scope 강한 enum 미사용 (자유 텍스트, 자동완성은 후속)
API: /api/study-topics/{id}/questions, /review/questions, /api/study-questions/{id},
/attempt. 통합뷰(/study-topics/{id}) 응답에 sections.questions / stats.question_count
추가. 기존 question_set_count 는 후속 PR(회차/모의고사 묶음)용으로 보존.
프론트: /study/topics/[id]에 문제 섹션 + "새 문제"/"복습 시작" 진입.
/questions/new (저장 후 계속 입력 + sessionStorage persistent),
/questions/[qid]/edit (정답 변경 시 attempts 재계산 안 됨 안내 배너),
/review (시작 옵션 → 풀이 → 마지막 요약).
후속 PR 예정: 오답노트/취약 과목 리포트, AI 해설/클러스터링, spaced
repetition, 이미지 OCR 입력, CSV import, study_question_sets 묶음.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
페이지네이션으로 여러 페이지에서 전체선택을 누적하면 100건 초과로 백엔드
StudyTopicDocumentLinkRequest 의 max_length=100 위반 → 422. 백엔드 제약은
abuse 방어용으로 유지하고, 프론트에서 100개씩 chunk 로 분할 POST + 결과
카운트 누적해 단일 토스트로 보고.
기존 page_size=50 으로 박혀 있어서 한 카테고리에 50건 초과 자료가 있을
때 51번째부터 안 보였음. page_size 를 백엔드 max(100)로 올리고 이전/다음
페이지 컨트롤 + 총 건수/페이지 표시 추가. 100건 초과 시 모달 상단에
"좌측 트리 폴더+ 아이콘으로 한 번에 추가" 안내 배너.
자료 추가 모달이 1건씩 검색·체크박스만 지원해서 같은 카테고리에 자료가
많을 때 비효율적. /api/library/tree 의 카테고리 구조를 모달 좌측에 띄우고,
노드 옆 아이콘 한 번으로 그 path 하위 자료 전체를 한 번에 매핑.
백엔드: POST /api/study-topics/{id}/documents/by-path 추가. user_tags
@library/<path> prefix 매칭(documents.py 의 list_library_documents 와
동일한 EXISTS 쿼리)으로 100건 limit 우회. 응답은 linked_count /
skipped_existing_count / total_in_path 카운트만 노출.
프론트: 모달을 max-w-4xl + grid(트리/자료) 레이아웃으로 개편. 트리 노드
클릭 = 우측 자료 목록 path 필터링, 노드 옆 FolderPlus 버튼 = 즉시 일괄
추가. 검색·체크박스·전체선택은 그대로. 모바일은 트리가 상단 max-h-40vh
영역으로 stack.