Phase 4-B v1 첫 검증 결과 자료 부족 토픽인데도 모델이 confidence='high'
박는 케이스 발견. 정의 (high = 자료 + 다른 ai_explanation 으로 패턴 명확)
보다 과신 — UX 신뢰도 위험. 자동 cap 보정 + 운영 관찰 SQL 추가.
confidence calibration (services/study/session_summary_guard):
- calibrate_confidence(c, ctx_docs_count, ready_explanation_count) 신규
· ctx_docs_count == 0 AND ready_explanation_count == 0 → 'low' cap
· ctx_docs_count == 0 (ready 만 있음) → 'medium' cap
· ctx_docs_count >= 1 → 모델 값 그대로
- 모델이 정의보다 더 보수적인 값 박은 경우 (모델 'low' + cap 'medium') 는
보존 — 더 보수적인 값을 절대 올리지 않음
worker 적용 (study_session_analysis_worker):
- ctx_docs_count = len(ctx_docs)
- ready_explanation_count = sum(1 for a in prompt_attempts if a.get('ai_explanation'))
- calibrate_confidence 호출 → study_quiz_session_analysis.confidence 박힘
- job.payload 에 운영 분석 metadata 보존:
· ctx_docs_count / ready_explanation_count
· model_confidence_raw (모델 응답) vs calibrated_confidence (cap 후)
· prompt_attempts / valid_attempts_total / summary_len
→ SQL 4 번 쿼리가 cap 작동 빈도 측정
scripts/phase4_health.sql (신규 운영 점검 SQL 7 섹션):
1. 4-A study_question_jobs status × error_code 분포
2. 4-B study_quiz_session_jobs status × error_code 분포
3. 4-B confidence 분포 (calibrated)
4. 4-B model_confidence_raw vs calibrated 차이 (cap 작동 빈도)
5. 4-A/4-B 최근 7일 처리 지연 p50/p95/max/avg
6. 4-A/4-B skipped 사유 분포
7. 4-B guard_fail / parse_fail / llm_timeout 비율
ship gate (단위 테스트):
- test_calibrate_confidence_no_evidence_caps_to_low (3 케이스)
- test_calibrate_confidence_only_explanations_caps_to_medium (3 케이스)
- test_calibrate_confidence_with_documents_passthrough (3 케이스)
- test_calibrate_confidence_normalizes_invalid_first (2 케이스)
Plan: ~/.claude/plans/nifty-sparking-spindle.md (Phase 4-B v1 후속)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이전 attempt 가 llm_timeout/parse_fail 박은 후 다음 attempt 가 정상 완료해도
error_code 가 잔존해서 운영 분석 시 혼선. status='completed' 박는 시점에
error_code = None / error_message = None 으로 명시 reset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
세션 1 (wrong+unsure 84건) 에서 prompt 가 23K자 넘어 30초 timeout. plan 가정
(5~30건) 대로 MAX_ATTEMPTS_IN_PROMPT=30 cap 추가. 가장 최근 attempts 우선
(answered_at asc 정렬의 뒤쪽). 기존 valid_attempts 카운트 검증 (5건 미만 skip)
은 그대로 유지 — cap 은 prompt 입력만, 검증은 전체 기준.
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>
검증 결과 모델이 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>
사용자가 며칠 안 들어오면 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>
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>
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>
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>
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>
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>
타 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>
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>
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>
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>
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>
회차 카드 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>
- 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>
- 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>
문제: 보기/해설 본문의 \$\$ ... \$\$ 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>
운영 중 발견한 패턴 추가:
- 보기 형식: "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>
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>
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>
- 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>
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 순차 적용.
문제별 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>