Commit Graph

467 Commits

Author SHA1 Message Date
Hyungi Ahn 3db5d331de feat(study): Phase 4-A 운영 가시화 — 통계 대시보드 AI 풀이 카드
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>
2026-05-01 11:59:20 +09:00
Hyungi Ahn 43097e6fd9 fix(study): Phase 4-A envelope 프롬프트 — answer_choice 사용자 정답 강제
검증 결과 모델이 envelope 안에서 자료 근거로 정답 번호를 재판단해서 거의 매번
guard_fail (answer_choice != correct_choice). 환각 가드는 정확히 작동했지만
caching 효율 0%.

PR-3 의 free-form 풀이는 "사용자 정답 우선, 충돌 명시" 라 정상 ready 박혔지만
envelope.txt 가 "자료 근거 우선" 으로 충돌. 환각 가드의 본질 — 모델이 envelope
형식을 어겨 임의로 다른 번호를 박는 케이스 차단 — 을 유지하되, answer_choice
값은 사용자 정답 (correct_choice) 을 그대로 박도록 명시.

자료 근거와 사용자 정답이 다를 경우 explanation_md 안에 짧게 명시만 하고
answer_choice 는 보존. 정답 자체를 바꾸는 게 환각 가드의 차단 대상이라고 강조.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:47:44 +09:00
Hyungi Ahn e8da53490c feat(study): Phase 4-A wrong/unsure AI 풀이 prefetch batch
PR-3 의 결과 화면 [AI 해설 보기] 실시간 호출이 클릭 시 8~30초 대기. 풀이 직후
백그라운드 batch 로 미리 생성해 캐시 hit. 환각 가드는 PR-3 보다 강화 — envelope
JSON {answer_choice, explanation_md, confidence} + answer_choice == correct_choice
검증 + evidence 의무.

processing_queue 가 documents.id FK 라 study_questions 에 직접 재사용 불가 →
별도 study_question_jobs 테이블 + 별도 consumer.

Backend:
- migrations/231 — study_question_jobs CREATE TABLE (13컬럼, kind 권장값
  'explanation' / 'session_summary' 예약, status pending/processing/completed/
  failed/skipped, max_attempts=2)
- migrations/232 — partial unique idx (qid, kind) WHERE status IN
  (pending, processing) — active 행 중복 차단, terminal 이력 누적 허용
- models/study_question_job — ORM + enqueue_study_question_job() 헬퍼
  (on_conflict_do_nothing 멱등)
- prompts/study_explanation_envelope.txt — envelope 형식 프롬프트
  (answer_choice 1~4 강제, confidence high/medium/low)
- workers/study_explanation_worker — terminal status 분기:
  · evidence 둘 다 빈 리스트 → job/question 모두 skipped (LLM 호출 X)
  · answer_choice != correct_choice → guard_fail / failed (재시도 X)
  · timeout/parse → 재시도 후보 (max_attempts=2)
  · catch-all except → unknown 명시 + retryable 분기
  · question.ai_explanation_status='ready' 이미 박혀있으면 즉시 completed
  · confidence 는 job.payload 에 보존 (운영 분석)
- workers/study_queue_consumer — APScheduler 1분 주기, BATCH_SIZE=1, MLX gate
  Semaphore(1) 공유. STALE_MINUTES=10 자체 복구
- main.py — scheduler.add_job(consume_study_queue, ..., id='study_queue_consumer')
- services/study/explanation_enqueue — finalize + GET fallback 공유 헬퍼:
  filter_needs_explanation (study_questions status + 최신 job error_code 필터,
  guard_fail/evidence_missing 인 마지막 job 은 자동 재enqueue 제외) +
  enqueue_explanation_for_qids (max_count cap)
- session_finalize — 끝에서 wrong/unsure qid prefetch enqueue (best-effort,
  실패해도 finalize 자체 안 깨짐)
- api/study_topics get_quiz_session — done 세션에서 backfill enqueue (max=30,
  non-blocking, debug 로그)

대상 조건: ai_explanation_status IN ('none', 'failed') OR ai_explanation IS NULL.
stale / skipped / pending / ready 는 자동 enqueue 대상 X. stale 재생성은 PR-3
명시 [다시 생성] 또는 후속 Phase 에서.

Plan: ~/.claude/plans/nifty-sparking-spindle.md (Phase 4-A)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:42:08 +09:00
Hyungi Ahn 711b81f8f0 feat(study): Phase 2-F due_at 정체 정리 — overdue redistribute
사용자가 며칠 안 들어오면 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>
2026-05-01 10:48:00 +09:00
Hyungi Ahn f42f6ff480 feat(study): Phase 2-E 복습함 멀티 셀렉트 → 복습 세션
복습함 카드 단위 체크박스 + 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>
2026-05-01 10:39:46 +09:00
Hyungi Ahn d39882c38e feat(study): Phase 2-D 학습 통계 대시보드 — 6 섹션
신규 라우트 /study/topics/[id]/stats — backend 단일 endpoint 호출로 6 섹션:
진척도 / 학습 상태 분포 / 복습 큐 / 세션 추이 / 일별 풀이량 / 과목별 약점.
차트는 SVG 직접 렌더 (의존성 0).

Backend (app/api/study_question_progress.py):
- GET /study-topics/{tid}/stats — 6~7 쿼리 묶음
  · 문제 진척도 (study_questions count + progress count)
  · pattern_state 분포 (NULL → unattempted + 토픽 미시도분 합산)
  · review_stage 분포 (0/1/2/3/mastered≥4)
  · due 분류 (today / this_week / later / mastered) — datetime 비교 + filter
  · 최근 done 세션 추이 (Phase 2-B 4 컬럼 활용, limit 20)
  · 일별 풀이량 30일 (cast Date + group)
  · 과목별 약점 (subject 별 attempted/correct/pending_review/chronic)

Frontend (/study/topics/[id]/stats):
- Card grid 6개. 진행률 바 + stacked horizontal bar + SVG sparkline + bar chart.
- 패턴 분포: 7색 stacked bar + 범례 grid.
- 복습 큐: 4 카운트 박스 + stage 분포 inline.
- 세션 추이: SVG sparkline (50% baseline) + 최근 5세션 표 (회복/퇴행/새로 맞힘 인라인).
- 일별 풀이량: SVG bar (max 동적) + title tooltip + start/end 날짜 라벨.
- 과목별: 정답률 진행률 바 + 미확인/반복 오답 인라인.

진입: 토픽 페이지 헤더 [통계] 버튼.

Plan: ~/.claude/plans/crispy-petting-dijkstra.md (Phase 2-D)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:04:03 +09:00
Hyungi Ahn 7cab78e490 ops(canonical): Phase 1D enqueue 전 backup + targets + md_status 스냅샷
enqueue 시작 직전 3가지 흔적 남김:
  (1) /tmp/phase1d_pilot.json 의 timestamped 사본 (재실행 대비)
  (2) 대상 30건 document_id 한 줄 출력
  (3) documents.md_status 분포 스냅샷 JSON 저장

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:00:33 +09:00
Hyungi Ahn 7e5716e594 feat(study): Phase 2-C 복습함 페이지 — 5탭 + 진입 동선
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>
2026-05-01 09:57:44 +09:00
Hyungi Ahn 3e831a2dc7 fix(canonical): Phase 1D script sys.path — /app/scripts/.. 가 PYTHONPATH 루트
fastapi 컨테이너는 WORKDIR=/app, 코드가 직접 풀려있고 app/ 디렉토리 없음.
backfill_category.py 의 ../app 패턴은 컨테이너 안에서 /app/app (없음)
가 되어 ModuleNotFoundError. 스크립트 자기 디렉토리의 .. 를 sys.path 에
넣어 /app 루트 노출.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:50:23 +09:00
Hyungi Ahn f98cf2e505 ops(canonical): Phase 1D marker pilot one-shot script (select/enqueue/report)
30건 한정 stratified pilot. baseline markdown 품질 측정 후 Phase 2 전체
백필 결정. 영구 worker 경로 아님.

대상 WHERE:
  deleted_at IS NULL
  AND file_format='pdf'
  AND md_status='pending'
  AND category='document'
  AND document_type NOT IN SKIP_DOC_TYPES (marker_worker 와 일관)

Stratification:
  ai_domain × file_size_bucket (small<500KB / medium<5MB / large)
  documents 에 page_count 컬럼 부재 (marker_worker 가 PyMuPDF 로 동적
  측정) → file_size 를 길이 proxy 로 사용.
  cell 안에서 file_size 작은/큰 mix 로 짧은/긴 문서 차이 관찰.

Subcommands:
  select  — 30건 dry-run + JSON 저장 (/tmp/phase1d_pilot.json)
  enqueue — markdown 큐 enqueue (uq_queue_active 충돌 시 skip)
  report  — md_status / 평균 elapsed / 실패 top5 / heading anchor 후보 /
           KaTeX 후보 / file_size bucket 별 success 비율 / UI 검수 URL

리포트 메모:
  markdown_image_count 는 현재 server.py 가 _images 버림 → 0 정상.
  Phase 1B.5 에서 _images 출력 시 자동 활성.

실행:
  docker compose exec fastapi python /app/scripts/phase1d_pilot.py select
  docker compose exec fastapi python /app/scripts/phase1d_pilot.py enqueue --yes
  docker compose exec fastapi python /app/scripts/phase1d_pilot.py report

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:49:17 +09:00
Hyungi Ahn d3bf963a66 feat(study): Phase 2-B 결과 화면 변화 카운트 + 확인완료 progress 통합
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>
2026-05-01 09:49:01 +09:00
Hyungi Ahn c46fd564af feat(study): Phase 2-A 풀이 시작 UI — 학습 단계 + 분량 토글
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>
2026-05-01 09:39:20 +09:00
Hyungi Ahn d038f11444 feat(canonical): Phase 1C MarkdownDoc renderer + heading anchor + KaTeX
문서 상세 페이지에서 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>
2026-05-01 09:37:33 +09:00
Hyungi Ahn 242288aaf3 fix(study): Phase 1 migrations 222-225 → 226-229 — markdown canonical layer 222 충돌 회피
타 PR (markdown canonical layer Phase 1B) 의 222_processing_queue_stage_markdown.sql 와
번호 충돌. init_db 가 'migration 버전 중복' 에러 띄움. 4파일 + SQL 헤더 주석 일괄 rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:32:16 +09:00
Hyungi Ahn 9094b2dbc5 feat(study): Phase 1-E 풀이 선별 알고리즘 — bucket + stage 비율
vision 의 단일 풀이 진입점 — stage (intro/learning/pre_exam) + size 옵션으로
같은 endpoint 가 다른 분포의 문제 출제.

services/study/quiz_selection.py:
- bucket: unattempted / wrong_or_unsure / due_review / regressed / frequent / random
- stage 별 비율:
  - intro:    unattempted 55, wrong_or_unsure 30, frequent 15
  - learning: due_review 20, wrong_or_unsure 40, unattempted 30, frequent 10
  - pre_exam: due_review 20, wrong_or_unsure 30, regressed 10, frequent 20, random 20
- bucket 우선순위 (dict 순서) — 다음 bucket 은 이미 뽑힌 qid 제외
- 후보 부족 시 random backfill, 그래도 부족 시 ValueError

api/study_topics.py:
- QuizSessionStartRequest 에 stage / size 옵션 추가
- stage 명시 시 select_questions_for_quiz 사용
- stage 미명시 시 기존 PR-12-B 경로 (subject bucket + spacing) 호환 유지

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:30:11 +09:00
Hyungi Ahn e5982ebde4 feat(study): Phase 1 학습 루프 데이터 계층 — progress 캐시 + finalize + review API
vision (풀이 → 확인 → 학습 → 복습 → 다음 풀이 가중치) 의 데이터 계층.

데이터 모델 (migrations 222~225):
- study_question_progress 테이블 — user × topic × question 단위 현재 상태 캐시
  - 마지막 시도: last_outcome, last_attempted_at, last_attempt_id
  - 검토 상태: last_reviewed_at
  - 복습 큐: due_at, review_stage
  - 패턴 분류 (derived): pattern_state, pattern_updated_at, pattern_window_attempts
- 3 partial idx (due / topic_pattern / pending_review) — 탭별 빠른 조회

패턴 분류 (services/study/learning_pattern.py):
- 7 분류: unattempted/unsure/chronic_wrong/regressed/recovered/stable/unstable
- 윈도우 = 최근 3회 + 과거 correct/wrong 존재 여부
- chronic_wrong > regressed > recovered 우선순위 (보수적 학습)
- 가드: wrong 1회만으로 regressed 안 됨 (이전 correct 이력 필요)
- stable 은 3 연속 correct 부터

세션 종료 집계 (services/study/session_finalize.py):
- attempts append-only 원본 보존, progress upsert 만
- 마지막 attempt 직후 finalize hook 자동 발동
- finalize 는 last_* + pattern_state 만 갱신, due_at 미진입 문제는 NULL 유지
- 이미 due_at 박힌 문제는 finalize 가 stage 갱신 (correct → +1 / wrong → 리셋)

API (api/study_question_progress.py):
- POST /study-topics/{tid}/questions/{qid}/review-complete
  → last_reviewed_at + (wrong/unsure 인 경우만) due_at 최초 부여
- GET /study-topics/{tid}/review-queue?tab=due_today|pending_review|chronic|regressed|mastered
  → 5 탭 paginated 조회
  → pending_review 는 last_reviewed_at < last_attempted_at 까지 포함 (이전 확인완료 후 다시 wrong 잡힘)

Phase 1-E (풀이 선별 알고리즘) 은 후속 commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:28:46 +09:00
Hyungi Ahn e66addf975 fix(canonical): marker engine_version via importlib.metadata
marker module 이 __version__ attribute 를 노출하지 않아 ship gate 10 에서
engine_version="unknown" 으로 표시되던 cosmetic 문제. importlib.metadata.
version("marker-pdf") 로 패키지 버전 정확히 읽음.

테스트: ship gate 10 PASS 확인 후 재배포.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:19:46 +00:00
Hyungi Ahn daaf18bdae fix(canonical): add markdown to process_stage ORM enum (Phase 1B follow-up)
migration 222 가 DB enum 에 markdown 을 추가했지만 SQLAlchemy ORM 측 enum
정의 (app/models/queue.py) 에 누락되어 LookupError 발생.

테스트 enqueue → consumer 실행 시:
LookupError: 'markdown' is not among the defined enum values.

DB enum 마이그레이션은 migration 222 가 처리. ORM 측은 SQLAlchemy 가
직렬화/역직렬화에 사용하는 Python 측 enum mirror 역할.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:16:03 +00:00
Hyungi Ahn e50869cbda feat(canonical): Phase 1B marker-service + marker_worker for PDF→markdown (222)
신규 컨테이너 marker-service (port 3300, Marker 1.10.2 + surya 0.17.1 + HF
cache volume). marker_worker 가 markdown stage 큐 소비:
  classify_worker → enqueue 'markdown' (leaf, embed/chunk 와 독립)
  → SKIP_DOC_TYPES (발주서/세금계산서/명세표) 스킵
  → 확장자 != .pdf 스킵 (Phase 1B = PDF only)
  → page_count > 200 스킵
  → marker-service POST /convert
  → 422/404 = doc-level failed, 5xx = queue retry

안정성 장치:
- migration 222: ALTER TYPE process_stage ADD VALUE markdown (단일 statement)
- md_extraction_quality JSONB dict 직접 저장
- skip 시 md_content/hash NULL 클리어
- /ready Response.status_code + warmup_error 가시화
- HF cache volume (build-time download 0)
- file_path 는 NAS 상대경로 → /documents prefix prepend

성공 기준: 파이프라인 안정성. markdown 품질은 Phase 1D pilot.

Pre-flight (2026-05-01):
- marker-pdf 1.10.2 stable
- file_path 9503건 NAS 상대경로
- DOCUMENT_TYPES 한국어 7종 → SKIP alias 보강
- queue retry max_attempts=3 + reset_stale_items 확인
- main 220/221 study_q_related 선점 → 222 rebump

Plan: ~/.claude/plans/plan-idempotent-sundae.md (Round 5 approved)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:06:23 +00:00
Hyungi Ahn 2dd0f655bc refactor(study): 통합뷰 문제 카드 viewport prefetch — 첫 진입 가속
회차 카드 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>
2026-05-01 08:05:37 +09:00
Hyungi Ahn 56f9aecf77 refactor(study): SvelteKit hover 시 라우트 코드/데이터 prefetch 활성화
처음 문제 진입 시 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>
2026-05-01 08:02:10 +09:00
Hyungi Ahn 219e233a48 feat(study): related-types DB 캐시 — HNSW 매번 재계산 제거
- migrations 220/221: study_questions 에 related_repeat/similar JSONB + 카운트/grade/computed_at/threshold_version + partial idx
- 임베딩 워커: ready 처리 직후 같은 트랜잭션에서 related 계산·저장 + 같은 토픽 ready 행들의 related_computed_at=NULL invalidation
- 신규 cron study_q_related_refresh (1분, batch=20) — stale 캐시 일괄 재계산
- API list_related_types: cache hit (computed_at + threshold version 일치) 시 SELECT 1번으로 응답. miss 면 즉시 계산+저장 후 응답
- update_question PATCH: 본문/exam_round 변경 시 related_computed_at=NULL
- soft delete: 같은 토픽 ready 행 invalidation

threshold 변경 시: related_types.THRESHOLD_VERSION 갱신 + UPDATE WHERE version != '<신>' SET computed_at=NULL 한 번이면 cron 자동 일괄 재계산.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:22:31 +09:00
Hyungi Ahn fe26aadb27 fix(canonical): split Phase 1A migrations into single-statement files (211-219)
asyncpg exec_driver_sql 의 prepared statement 제약상 multi-statement 파일은
"cannot insert multiple commands into a prepared statement" 에러로 적용 실패.
규칙: 한 migration = 한 statement (다중 ADD COLUMN 절은 단일 statement 라 허용,
인덱스/CHECK/CREATE TABLE 은 별도 파일).

이전 cee01af 의 211_md_canonical_layer.sql (6 statements) + 212_document_lineage.sql
(3 statements) 을 9 파일로 분할:
  211 ALTER TABLE documents ADD COLUMN x13
  212 ADD CONSTRAINT documents_md_draft_status_only_ai
  213 idx_documents_md_status_pending
  214 idx_documents_content_origin
  215 idx_documents_md_frontmatter_gin (선제 인덱스)
  216 idx_documents_md_draft_status
  217 CREATE TABLE document_lineage
  218 idx_document_lineage_source
  219 idx_document_lineage_derived

dry-run 재검증: 13 cols / 28 doc idx / 4 lineage idx PASS.
계획 변경 없음 — schema 결과 동일, 적용 단위만 분할.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:57:11 +00:00
Hyungi Ahn cee01af96a feat(canonical): Phase 1A markdown canonical layer schema (211/212)
documents 13 신규 컬럼 (md_content/md_frontmatter/md_status/content_origin
포함) + 4 인덱스 + 1 CHECK 제약 + document_lineage 테이블 (FK RESTRICT).

상태값은 모두 TEXT+CHECK (확장 시 enum drop/rebuild 비용 회피).
어떤 워커도 컬럼을 채우지 않음 — 스키마 기반만 깔고 Phase 1B 에서
marker_worker 로 채우기 시작.

Plan: ~/.claude/plans/plan-idempotent-sundae.md (round 3 approved)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:51:31 +00:00
Hyungi Ahn 36c8a0df3c refactor(study): 본문 표시를 q 단독 도착 시점으로 unblock
- 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>
2026-04-30 10:44:00 +09:00
Hyungi Ahn 4687546e37 refactor(study): 문제 상세 페이지 loadTopic 병렬화 + roundSiblings 캐시
- 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>
2026-04-30 10:27:37 +09:00
Hyungi Ahn fc8aea1649 feat(study): 반복 출제 라벨 등급 + cosine 임계값 0.85 조정
- 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>
2026-04-30 08:50:39 +09:00
Hyungi Ahn 5404343a1a fix(study): HC-5 block math spacing — KaTeX \$\$...\$\$ 앞뒤 빈 줄 보장 자동 fix
문제: 보기/해설 본문의 \$\$ ... \$\$ block math 가 앞뒤 빈 줄 없으면
마크다운 파서가 라벨/텍스트와 같은 단락에 묶어 KaTeX 렌더 실패 → raw 표시.

운영 결과 (21회분 = 2,100문항):
- HC-5 detect 317건 모두 자동 fix 완료. 모든 회차 재검사 0건.
- 추가 fix: q1579 (2023년 1회 q81) 바이메탈 ASCII 다이어그램 fence wrap.

알고리즘:
- 자체 줄 \$\$...\$\$ (한 줄 안 시작·종료, 길이 4+) detect.
- 앞·뒤 라인이 비어있지 않으면 빈 줄 삽입 — idempotent.
- inline \$ ... \$ 영향 없음.
- 의미 변경 0 (빈 줄 삽입만, 본문 텍스트/수식 보존).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 08:29:39 +09:00
Hyungi Ahn 87b6c38d99 feat(study): 보기 동그라미 숫자 (①②③④) 형식 지원 + 10회분 추가
운영 중 발견 — 2023년 이후 회차 md 가 보기를 ①②③④ 으로 표기.
파서가 "1번:" / "1." / "1)" 만 매칭해서 100문항 보기 1~4번 비어있음 → import abort.
CIRCLED 매핑 활용해서 동그라미 숫자도 처리 추가.

운영 결과 (10회분 추가, 누락 png 제외):
- 2022년 3회 / 2023년 1회: 100건 (이미지 0)
- 2023년 2회: 98건 / 2023년 3회: 96건 (png 일부 누락)
- 2024년 1·2·3회: 각 98건 (png 누락)
- 2025년 1·2·3회: 97/99/97건 (png 누락)
- audit: HC 0 / LC-5 1건 자동 fix (q2183 표 구분자)
- 누락 png 19건은 사용자 추후 보충 예정

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 06:47:06 +09:00
Hyungi Ahn 1d73986fd6 feat(study): 가스기사 import 스크립트 — 보기 형식 다양화 + subject 슬래시 정규화
운영 중 발견한 패턴 추가:
- 보기 형식: "1번:" + "1." + "1)" 모두 매칭 (2022년 회차에서 "1." 사용 발견).
- subject 정규화: 괄호 형태(연소공학 (열역학))뿐 아니라 슬래시 형태
  (가스안전관리 / 가스설비) 도 head + scope 분리.

운영 결과 (6회분 = 600문항 추가):
- 2020년 3회 / 2021년 1·2·3회 / 2022년 1·2회 모두 등록 완료.
- 이미지 27건 자동 첨부 (1+4+7+6+5+4).
- audit: HC 0건, LC-5 2건 (2022년 2회 q41/q90 표 구분자 누락) 자동 fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:56:17 +09:00
Hyungi Ahn cb07ffa4ce feat(study): study_questions DB 마크다운 정합성 audit 스크립트
scripts/audit_study_question_markdown.py:
- HC 자동 fix (HC-1 outer fence / HC-2 escape 잔재 / HC-3 HTML 엔티티 / HC-4 공백)
  · HC-2 KaTeX 명령어 (\rho, \nabla 등) false positive 회피 — lookahead (?![A-Za-z])
  · 비정상 카운트 abort_threshold 안전장치
- LC 리포트 (LC-1 백틱 / LC-2 \$\$ / LC-3 \$ / LC-4 ** / LC-5 표 / LC-6 들여쓰기)
  · 각 항목에 edit 페이지 URL 포함 — 사용자 직접 처리 가능
  · LC-5 다컬럼 표만 검사 (|...|y|... pipe 3+) — 절대값 |x| 한컬럼 false positive 회피

운영 결과 (5회분 = 500문항):
- 2019년 1회: HC-4 43건 + LC-1 8건 + LC-3 2건 + LC-6 3건 자동/사용자 fix
- 2019년 2회: LC-1 4건 자동 fix
- 2019년 3회 / 2020년 1·2회: 0건
- 모두 audit PASS (HC 0 / LC 0)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:06:39 +09:00
Hyungi Ahn b20c4f933b feat(study): exam_round 필터 + 일괄 import 스크립트 — 1천+ 문제 대비 (P0)
문제: 1천+ 문항 토픽에서 보기 페이지 prev/next 가 page_size=200 cap 으로
회차 외 문항만 받아 같은 회차 prev/next 누락 회귀.

해결:
- /study-topics/{tid}/questions 에 exam_round Query 파라미터 추가 (exact match).
- StudyQuestionSummary 응답에 exam_question_number 필드 추가.
- exam_round 필터 시 정렬 = exam_question_number asc NULLS LAST, created_at asc.
- 보기 페이지 loadRoundSiblings 가 ?exam_round= 로 한 회차만 fetch.
- 토스트 문구 "토픽 200문제 초과" → "이 회차에 200문항 초과" (의미 일치).

추가 — 가스기사 기출 일괄 import 스크립트:
- scripts/import_gas_questions.py: md 파서 + dry-run + apply.
  · exam_question_number 3소스 (파일명/제목/메타) 일치 검증.
  · subject 정규화 (괄호 세부분류는 scope 로 이동, 5과목 통일).
  · 이미지 4케이스 판정 + import_reports/{회차}_image_required.md 생성.
  · 첫 실패 abort 기본, --skip-existing/--continue-on-error 옵션.
  · 토큰 사전 검사 (GET /study-topics/{tid}).
- import_reports/: 2019년 1~3회 + 2020년 1~2회 리포트.
- 운영: 4회분 360문항 자동 import 완료 (이미지 4건 자동 첨부).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:39:02 +09:00
Hyungi Ahn 373dd059b7 fix(study): outer fenced code block auto-unwrap (renderMathMarkdown + DB 일괄 정리)
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>
2026-04-29 13:55:18 +09:00
Hyungi Ahn 73b895c613 fix(study): 새 회차 진입 시 dropdown 에 (신규) 옵션 표시 — mode 전환 대신 옵션 추가
이전 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>
2026-04-29 13:17:44 +09:00
Hyungi Ahn 4c26b9153f fix(study): 새 회차 시작 동선 — query 회차명이 examRounds 에 없을 때 mode='new' 자동 전환
회차 카드 페이지의 [새 회차 시작] → /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>
2026-04-29 13:13:13 +09:00
Hyungi Ahn 13404cd366 feat(study): 같은 유형 과밀 방지 — 출제 단계 spacing (PR-12-B)
학습 의미: 한 quiz 세션 안에서 같은 유형 문제가 과도하게 몰리지 않게 분산.
같은 유형을 없애는 게 아니라 펼치는 것 — dedup/제거 프레임 금지.

- 마이그레이션 210: study_quiz_sessions.quiz_mode VARCHAR(30) DEFAULT 'random'
- ORM: StudyQuizSession.quiz_mode 필드
- service.related_types: apply_type_spacing helper 추가
  - SPACING_THRESHOLD=0.88 (회차 무관 — PR-12-A 회차 필터 재사용 X)
  - PER_TYPE_CAP=2 (local neighbor cap, transitive cluster 보장 X)
  - SPACING_BUFFER_RATIO=2.0
  - 3단계 fallback: ready spacing → pending 보충 → hold cap 위반 fallback
  - debug 로그 type_spacing_applied subject=... ready=N selected=M ...
- _select_questions_for_topic: subject bucket 단위 spacing (과목 균등 보호)
- QuizMode Enum (random) — 향후 frequent_focus/wrong_variants 예약
- start_quiz_session 에 quiz_mode 받기 + apply_spacing 전달
- 프론트 startNewQuiz body 에 quiz_mode='random' 명시

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:45:15 +09:00
Hyungi Ahn cbe852bb37 feat(study): 반복 출제 / 유사 유형 분리 표시 (PR-12-A)
학습 의미가 회차 간 반복성 — 차단/제거가 아니라 패턴 표시 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>
2026-04-29 08:09:14 +09:00
Hyungi Ahn 8525c9aefb fix(study): 마크다운 컨테이너 클래스 prose → markdown-body
@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>
2026-04-29 07:13:39 +09:00
Hyungi Ahn 1cf64fd11e feat(study): 문제 회차별 그룹 + 읽기전용 보기 페이지 (PR-11)
- 통합뷰 문제 섹션: 평면 리스트 → 회차별 아코디언 (디폴트 모두 접힘)
- 회차 정렬: "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>
2026-04-29 07:01:27 +09:00
Hyungi Ahn 6e25523600 fix(study): quiz_session 결과 — StudyQuestionImage.position → sort_order 재사용
PR-10 결과 페이지에서 GET /quiz-sessions/{sid} 가 500. 이미지 batch 호출에서
존재하지 않는 컬럼 position 사용 → AttributeError. 기존
_images_for_questions_batch 헬퍼 (sort_order 기준 + served_url 포함) 재사용.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 16:52:52 +09:00
Hyungi Ahn 7f4d64c6df feat(study): 문제풀이 세션 + 결과 카드 + 학습완료 체크 (PR-10)
- 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>
2026-04-28 16:49:21 +09:00
Hyungi Ahn d968b2d901 feat(study): 문제풀이 모드 개편 + 결과 분류 + 분야 설명 (PR-9)
- 라벨 "복습 시작" → "문제풀이"
- 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>
2026-04-28 15:58:35 +09:00
Hyungi Ahn 3abccc512d fix(study): 마이그레이션 198 single-statement 분리 — 199_idx 추가
CREATE TABLE + CREATE INDEX 한 파일에 들어가 asyncpg prepared statement
원칙 위반 (cannot insert multiple commands). 198 = TABLE 만, 199 = idx 분리.
첫 시작에서 198 적용 fail 로 init_db 트랜잭션 전체 롤백 → 컨테이너 시작 후
schema_migrations 미반영 + study_question_images 테이블 미생성. 본 fix 후
다음 시작 시 198+199 순차 적용.
2026-04-28 13:44:59 +09:00
Hyungi Ahn b58268ba96 fix(study): Svelte fragment 문법 제거 — <></> 대신 명시적 태그 2026-04-28 13:43:31 +09:00
Hyungi Ahn 8b15e6e019 feat(study): 문제 첨부 이미지 (PR-8)
문제별 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>
2026-04-28 13:41:50 +09:00
Hyungi Ahn df52cb191b fix(study): TS annotation 제거 — plain JS svelte 파서 호환
applyQueryParams(): boolean 형태가 vite-plugin-svelte 의 JS 모드에서
parse error → 빌드 실패 → 직전 fix 모두 컨테이너에 적용 안 됨.
2026-04-28 13:30:55 +09:00
Hyungi Ahn a7b3164f78 fix(study): 클라이언트 카운터 신뢰 X — 서버 max+1 자동 채움 (user-edited dirty flag)
이전 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)

이제 클라이언트가 어떤 표시값을 보여주든 실제 저장은 항상 정확. 사용자가
직접 박스를 수정한 경우만 명시 전송.
2026-04-28 13:25:08 +09:00
Hyungi Ahn 0d66107743 fix(study): 회차 변경 race 제거 — $effect → 명시적 onchange 핸들러
$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 도 발생 안 함
2026-04-28 13:14:52 +09:00
Hyungi Ahn 5b7e06abc1 fix(study): 입력 페이지 진입 시 회차 next_question_number race 수정
$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 적용.
2026-04-28 13:08:05 +09:00
Hyungi Ahn f6393fbe66 feat(study): 수식 입력/표시 KaTeX 렌더링 (PR-7)
기사시험 문제·해설에 √, ρ, 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>
2026-04-28 12:52:34 +09:00