cron dry-run 검증 중 발견:
- /app/scripts/ 는 bind-mount 활성 (Phase 2 main FF 후 컨테이너 가시화 ✓)
- /app/evals/ 는 fastapi 이미지에도 없고 compose 마운트도 없음
- 이전 README/plan 의 --log-tsv /app/evals/markdown/... 은 컨테이너
writable layer 에 쓰여 재기동 시 유실되는 문제
해결: nightly --log-tsv 와 post-report --output-* 는 /app/logs/ 사용
(rw bind-mount → host ~/Documents/code/hyungi_Document_Server/logs/ 영구).
주 1회 git commit 시 logs/ → evals/markdown/ 로 cp 후 add.
post-report 도 동일 패턴.
Plan/README 가 /app/scripts 를 통일 경로로 가정했으나 실측 결과 read-only
bind-mount 라 docker cp 불가. soft lock 으로 --build 도 금지. 단계별로
다른 경로 사용해야 함:
- 2-B canary (pre-merge): /app/logs/phase2_backfill.py + /app/logs/*.csv
(docker cp worktree → /app/logs rw bind-mount). canary 검증 동안
미검증 코드 main 진입 회피.
- 2-C nightly (post-merge canonical): /app/scripts/phase2_backfill.py +
/app/evals/markdown/phase2_* (feat/phase2-backfill main 머지 +
parent git pull 후 bind-mount 자동 활성). cron 도 canonical path.
evals/markdown/README.md 의 enqueue 예제 + 신규 #### 경로 정책 섹션 반영.
`<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>
`:payload::jsonb` 의 `::` postfix 캐스트가 SQLAlchemy text() 의 named-param prefix
`:` 와 충돌해 asyncpg syntax error. doc 3757 sample reprocess 시 발견.
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>
기존: (ConnectError, TimeoutException) 만 transient → raise → queue retry.
ReadError / WriteError / RemoteProtocolError 같은 다른 transport 류는
'except Exception' 이 잡아 _fail 처리 → max_attempts 무시하고 final fail.
Phase 1D pilot 에서 5111/5115 두 건이 'Server disconnected without
sending a response' (RemoteProtocolError) 로 retry 없이 final fail.
Fix: except (ConnectError, TimeoutException) → except TransportError.
TransportError 가 Connect/Read/Write/RemoteProtocol/Timeout 의 공통 부모
라서 모든 transport 계층 오류가 transient queue retry 대상이 됨.
5135 의 ReadTimeout (queue exhausted) 는 본 fix 와 별개 — 8.4MB PDF 가
MARKER_TIMEOUT=300s 안에 못 끝나 3번 retry 다 timeout. timeout 자체를
늘리거나 큰 PDF 분할 처리하는 별도 결정 필요.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
사용자 quality 평가:
"애플펜슬로 필기한건 내 글씨체 이슈에 더해서 좋은 자료를 뽑아내지
못하네 그 외에는 잘되는거 같은데"
분류:
overall_pass=true 24건 — 일반 PDF (born-digital + scan-like 中
5127 같이 정상 변환되는 케이스)
overall_pass=false 4건 — 애플펜슬 필기 4건 (4798/4813/4815
controlled_backfill + 4809 anchor)
overall_pass=empty 2건 — page_count > MAX_PAGES=200 의도 skip
(5178 ASME 272p, 5180 ASME Sec I 453p)
정식 rubric 5축 (text_accuracy/structure/noise_rate/multi_script/
completeness) 점수는 비워둠 — 사용자 약식 판정으로도 의사결정 매트릭스
분기 (필기만 fail → SKIP rule 확장) 가 명확해 정식 채점 over-investment.
후속 라운드 (Marker 튜닝/대안 OCR 도입 시) 같은 30건 재평가에는 정식
rubric 채울 가치 있음.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1D pilot (2026-05-02 야간 sweep, 25 controlled_backfill 결과) 에서
필기 PDF 3건 (4798 / 4813 / 4815) 이 status='success' 로 변환됐으나
사용자 quality 평가에서 좋은 자료 추출 불가 판정. 근본 원인은 Marker
설정 부족이 아니라 입력 자체 (애플펜슬 손글씨 + 사용자 글씨체 = OCR/
layout 모델 한계 영역). Marker 튜닝으로 해결될 영역이 아니므로 enqueue
단계에서 자동 skip.
가드 로직:
marker_worker.process() 의 doc_type SKIP 직후 (1.5 단계) title/path 의
보수적 키워드 4개 (필기, 손글씨, handwritten, handwriting) 매칭 시
_set_skipped() 호출. md_content/md_content_hash NULL clear,
md_extraction_error='skipped: handwritten note (title/path heuristic)',
content_origin='extracted'.
키워드 선정 (보수적):
포함: 필기 / 손글씨 / handwritten / handwriting
제외 (false positive 위험):
- 노트 (노트북 매뉴얼 / release notes / Note_240528_워크숍 같이
필기 아닌 정상 문서까지 잡음)
- scan / 스캔 (스캔 PDF 中 정상 변환되는 케이스 있음, 1D 결과
doc 5127 표준기계설계(KS)_08_핀 density 1.59 / scan_likely 인데
성공)
logger:
markdown_skip_handwritten_hint id=<id> keyword=<matched> title=<...>
regex 단위 테스트 15 케이스 (실 production fastapi venv) 전부 통과:
매칭: Note_240805_용접교육 필기 / Note_240827_필기 / 손글씨 모음 /
Handwritten Notes 2024 / handwriting practice / path/필기/* /
path/handwritten_collection/* (8건)
비매칭: 다이아프람워크숍 / 노트북 매뉴얼 / Release notes v2 / PIPE
FABRICATORS / 표준기계설계 / scan documentation / 스캔 문서 (7건)
이번 가드는 enqueue 시점 적용. 이미 success 인 4건의 md_content 는
보존 (사용자가 직접 보고 싶을 때 표시 가능). 정리 필요 시 별건.
후속 (별 PR):
- A2 (정식 doc_type='필기노트' 라벨): 1D 3건 sample 너무 적어 라벨
정의 보류. 필기 PDF 누적 후 별도 검토.
- C (Phase 2 풀 backfill plan): 본 PR 머지 후 별도 라운드.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 2 sample 에 existing_success 5건 (anchor doc 4809 + calibration 4)
이 포함되었지만, cmd_enqueue 가 sample_source 무시하고 30건 전부 enqueue
하던 버그. 결과:
- existing 5건 marker 재처리 (~25분 marker 시간 낭비)
- 동일 quality output 으로 md_content overwrite → baseline 유실
- anchor (doc 4809) 의 "before" 상태가 사라져 후속 라운드 비교 anchor 손상
Fix:
- default = sample_source == "controlled_backfill" 만 enqueue (25건)
- --include-existing flag 추가 (후속 Marker 튜닝 라운드에서 anchor 재처리
필요 시 사용)
- print 로 mode 명시 + 제외된 ids 표시
야간 단발 sweep (23:00 KST) 예약 실행 전 fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
기존 phase1d_pilot.py (단순 ai_domain × file_size 3-bucket) 를 plan
~/.claude/plans/stratified-mingling-otter.md 의 4축 + sample_source 분리
+ forced_include 로 augment.
Round 1 (ai_domain × file_size 3-bucket) 의 한계:
pending PDFs 의 자연 분포만 반영 → 알려진 약점 (필기/스캔/한중일
mixed OCR) 이 sample 에 안 들어옴. 1C 시각 확인에서 doc 4809
(Note_240805_용접교육 필기) 가 실제로 그 패턴을 보였는데, 자연
selection 에 맡기면 다음 라운드도 같은 case 가 빠질 위험.
Round 2 디자인:
- 4 축 stratification: doc_type × file_size_band × text_density_band
× handwritten_hint
- sample_source ∈ {existing_success(5), controlled_backfill(25)}
- forced_include doc 4809 — known bad anchor. 다음 튜닝/대안 도입 후
같은 문서 재변환 결과와 1:1 비교 가능.
- text_density = LENGTH(extracted_text) / (file_size / 1024) chars/KB
가장 깨끗한 단일 proxy. 0.17(필기 4809) ↔ 94(born-digital 3759)
양 끝 검증.
- script_mix proxy: Hangul/CJK/Hiragana/Katakana/Latin Unicode block
ratio → korean_dominant / mixed_korean_cjk / mixed_korean_latin /
cjk_dominant / latin_dominant / unknown.
- page_count_estimate: existing_success 는 md_extraction_quality.
metrics.source_page_count 사용. controlled_backfill 은 NULL
(marker 가 PyMuPDF 로 어차피 다시 읽음).
- 시드 SAMPLE_SEED=20260502 고정, 재현성 보장.
Sample 분포 (실측 2026-05-02):
bucket_label: born_digital=12, mixed=5, existing_calibration=4,
handwritten=3, scan_likely=3, large=2, existing_anchor=1
doc_type: Academic_Paper=7, study_note=6, Standard=5, Note=4,
Reference=3, Manual=3, Drawing=1, Report=1
file_size_band: M=14, S=12, L=4
text_density_band: born-digital=15, scan-likely=9, mixed=6
handwritten_hint: lo=26, hi=4 (모집단 1.1% 대비 13배 over-sample)
forced anchor doc 4809 = density 0.17 (사용자 시각 확인의 그 문서)
새 subcommand:
eval_template — pilot_1d_eval.csv 스켈레톤 (rubric 5축 1~5 +
overall_pass + notes). 사용자가 MarkdownDoc + PDF 토글 비교하며
점수 채움.
기존 cmd_enqueue (snapshot/backup/dedup) + cmd_report (quality 메트릭)
는 유지.
산출물:
scripts/phase1d_pilot.py — 4축 + sample_source + forced_include +
eval_template subcommand. CSV+JSON dual output.
evals/markdown/README.md — rubric + decision matrix + workflow guide.
evals/markdown/pilot_1d_sample.csv — 30 rows × 15 cols (시드 결과,
재현성 보존).
evals/markdown/pilot_1d_eval.csv — 빈 스켈레톤 (사용자 평가 후 채움).
실행 경계:
Step 1~3 (selection / template / dry-run) = 본 PR 으로 완료.
Step 4 (--yes enqueue, 실제 30건 markdown 큐 인입) = 사용자 timing
승인 + 야간 단발 sweep 윈도우 (23:00~03:00 KST) 안에서 별도 실행.
marker-service BATCH_SIZE=1, 30건 평균 5분/건 ≈ 2.5h.
Verify:
GPU 서버 fastapi 컨테이너에서 select 실행 → 30건 sample CSV 생성됨.
eval_template subcommand 동작 확인. enqueue dry-run 으로 30 doc_ids
+ snapshot 출력 후 사용자 취소 분기 확인.
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>
case 3/4 의 setup 이 EXPLANATION_MAX_CHARS (1200) 보다 작은 text 를 만들어
assert 실패. 한글 chunk 반복 횟수 늘려 1200 자 이상 보장.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
운영 데이터에서 ready 박힌 풀이가 793/838/866자 — 권장 200~400 대비 큰 편.
1차 운영 후 결과 화면 가독성 + 토큰 사용량 통제 위해 prompt 강화 + 저장 전 cap.
Prompt (study_explanation_envelope.txt):
- explanation_md 권장 300~600자, 최대 900자 명시
- 핵심 개념 + 정답 근거 + 헷갈리는 1~2개 오답만 — 모든 오답 풀이 X
- explanation_md 안 줄바꿈 최소화 (parse_json fix 와 결합 — invalid escape 줄임)
- LaTeX 수식 자제 — \\circ/\\text/\\, 매크로 가능하면 평문 ('0°C', 'C')
- 출력은 raw JSON 한 객체만 — 코드 펜스/thinking/메타 X 강조
Worker (study_explanation_worker.py):
- _cap_explanation_md(text, max_chars=1200) 헬퍼 신규
· 1200자 이하 passthrough
· 초과 시 마지막 200자 안에서 \\n\\n / \\n / '. ' / '다.' / '요.' 경계 탐색
· 경계에서 자르기 + '…' (단어 중간 자르기 회피)
· 경계 못 찾으면 단순 자르기 + '…'
- save 전 cap 적용. ai_explanation_status='ready' 유지 (cap 됐다고 failed X)
- payload 에 운영 분석 metadata: explanation_len_original / _saved / capped 플래그
검증:
- tests/test_explanation_cap.py (6 케이스)
· short passthrough / exact at limit / paragraph boundary / sentence boundary
· no boundary fallback / empty input
- scripts/phase4_health.sql 섹션 8/9 추가
· ai_explanation 길이 p50/p95/max (study_questions.ready)
· cap 작동 빈도 (job.payload 의 explanation_capped/_original/_saved)
cap 1200 = 800 (4-B summary_md) 보다 여유 — 기사시험 풀이는 공식+오답+개념 묶이면
800 빡빡함. 운영 후 800~1000 으로 조정 검토.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
직전 fallback 의 무차별 newline replace 가 string 외부 (object 구조) 의 raw newline
까지 escape 해서 JSON 거부. 또 LaTeX 수식 (\circ, \text, \, etc) 의 invalid backslash
는 newline 이슈와 별개라 별도 fix 필요.
state machine: in_string 토글 (`\"` 만남). string literal 안에서만:
- raw LF/CR/TAB → \\n/\\r/\\t 로 변환
- backslash 다음에 valid escape char (\"\\/bfnrtu) 면 그대로
- backslash 다음에 invalid (\\c, \\,) 면 backslash 자체를 \\\\ 로 escape
- string 외부 raw newline 은 JSON whitespace 라 보존
운영 데이터 id=243 의 raw 940자에 \\circ \\text \\, \\approx \\times 등 다수 LaTeX +
markdown 줄바꿈 → 새 walker 가 두 케이스 모두 fix. 다른 worker (classify/triage/
study_explanation/evidence/study_session_analysis) 자동 혜택.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4-A debug 결과 study_question_jobs.parse_fail 33건의 raw preview 분석:
- 모델이 explanation_md 안에 raw newline (LF) 그대로 박음 ('### [풀이]\n\n**자료...')
- JSON 표준상 string literal 안 raw control char 금지 → json.loads 거부
- 4단계 fallback (greedy slice) 도 이 때문에 실패
5단계 fallback 추가: candidate 의 \r\n/\n/\r 을 ``\\n``/``\\r`` escape 로 치환 후 재시도.
이미 escape 된 ``\\n`` (Python str = backslash+n 두 글자) 는 raw newline 아니라 영향 없음.
다른 worker (classify/triage/study_explanation/evidence/study_session_analysis) 모두
같은 파서를 공유하므로 자동으로 혜택.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
운영 데이터에서 4-A study_question_jobs 의 33/114 가 'envelope JSON parse failed'
로 종결. parse_json_response 의 balanced 정규식이 못 잡는 케이스 다수 추정.
원인 분류 위해:
1. 파서 보강 (app/ai/client.py)
- 기존 4단계 파싱 (fenced / balanced finditer / 전체 cleaned) 보존
- 5단계 fallback 추가: first '{' ~ last '}' greedy slice → json.loads
- envelope JSON 안에 내부 따옴표/뉴라인/escape 때문에 balanced 가 못 잡는
케이스 방어. 모델이 JSON 앞뒤 자유 텍스트 섞어도 본체만 추출.
- 회귀 위험 낮은 추가만 (앞 단계 성공 시 즉시 반환)
2. parse_fail 시 raw preview 저장 (study_explanation_worker)
- 3개 inline parse_fail 분기 (not_dict / invalid_answer_choice /
empty_explanation_md) 모두 _save_raw_preview() 헬퍼 호출
- job.payload.debug_raw_preview = raw_text[:1000]
- job.payload.parse_fail_reason = 분류 키
- 향후 parse_fail row 의 payload 분석으로 원인 정확히 분류 가능
다음 단계: 배포 후 재발생 추이 + raw preview 분석 → prompt 추가 강화 또는
parser 추가 보강.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>