Commit Graph

156 Commits

Author SHA1 Message Date
hyungi a850745f85 feat(docpage): asme 절뷰 Part 접이 그룹 렌더 — SectionOutline rail + [id] treeNav (asme D8)
flat 1030 절뷰를 read-time 표현계층에서 front-matter 단일 접이그룹 + PART/APPENDIX 접이그룹
(기본 전부 접힘)으로. 빌더/재분해 무접촉, 검색 무관(in_corpus=false 불변).

- partitionOutlineItems: 순서기반 carry-forward 그룹핑(비-PART top-segment 항목은 직전 PART 흡수).
  buildPartOutline = partitionOutlineItems∘collapseWindows 로 통일. PART_MARKER_RE = case-sensitive
  PART/SUBSECTION/APPENDIX(+대문자제목 가드) — 본문 cross-ref/문장 false match 차단
  (5210 'Part D…'·'PART UW 규정은…' 거부). 한글제목 PART 미인식은 D3 재정련(주석 박제).
- partGroupViews/groupKeyByChunkId: front-matter 첫 그룹 평탄화 + auto-expand 역인덱스.
- SectionOutline.svelte: Part 접이 모드 + groupOrFlat 폴백 + activeKey auto-expand.
- [id]/+page.svelte: treeNav 그룹 접이(treeNode 스니펫·d3 시안 보존) + 기본선택=첫 본문 Part +
  selectedSectionId auto-expand. 데스크탑/모바일 treeNav 공유.
- 리뷰 반영: rail max-height calc() 공백 fix / treeNode a11y role 조건부 / 문서 전환 접이상태 리셋 /
  모바일 본문 스코프 주석.

real-data 검증(prod read-only): 5180 → front-matter231 + 15 PART + 6 APPENDIX = 22 접이그룹·
커버리지 1030/1030·PG-27 정상. 5210(D3 재분해 전 stale) → 깨끗 PART 0 → hasParts=false →
flat 폴백(무회귀). 단위 26/26, vite build PASS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:32:25 +09:00
hyungi 513c6507bc feat(docpage): 절뷰 read-time front-matter 억제 + Part 그룹 유틸 (asme D7/D9)
긴 ASME 코드 절뷰가 flat 1030 으로 길어지는 문제(front-matter 240 + 다중 PART 가 GROUP_MAX 초과
→ flat 폴백)를 표현 계층에서 해결. 빌더/재분해 무접촉.
- D9 cleanHeading: ASME 개정바 ðNÞ(<sup>ð</sup>**25**<sup>Þ</sup>) 통째 strip (가운데 25 안 남김).
- D7 buildPartOutline: 첫 content part(PART/SUBSECTION/항목코드) 경계로 front-matter 분리 +
  본문을 heading_path 첫 세그먼트(PART)로 그룹. window/_split 도 PART 로 모여 흡수. content part
  없으면 hasParts=false 폴백. SectionOutline(D8) 이 소비.
단위 17/17(신규 6: 개정바 strip·front-matter 분리·window 흡수·폴백·항목코드). 미배포·prod 무접촉.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:21:14 +09:00
hyungi fabbca64e9 feat(markdown): 외부 링크 새 탭 + rel=noopener noreferrer (P0)
docMarked link 렌더러: http/https 링크에 target=_blank rel=noopener noreferrer
(탭내빙 차단, 코퍼스 521건). 내부/'#'프래그먼트/상대/mailto 는 무손 — outline
gfmHeadingId 경로 유지(클릭 인터셉터 없음=충돌 0). marked15 토큰객체 시그니처.
SANITIZE_OPTS ADD_ATTR 에 target/rel.

load-bearing 게이트: 상대 .md=코퍼스 0건·doc_key 부재 → path→id prop/document_links
미구현(dead). [[..]]=13건 대부분 인용 노이즈([[3\]]) → resolution/스트립 미구현.
외부 링크 하드닝만 정당화됨.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:06:58 +09:00
hyungi 2d6d1b8e8a fix(markdown): 수식 pre-render(katex 직접) + TL;DR 마크다운 렌더
본문 $$수식$$가 raw로 노출되던 문제: marked-katex 토크나이저가 개요 anchor
splice/런타임 환경 영향으로 미발화 → marked 이전에 katex.renderToString 으로
직접 렌더 후 placeholder 복원(위치·인접 무관). TL;DR(ai_tldr)도 plain-text
보간이라 마크다운 미렌더 → renderDocMarkdown 경유로 교체(+summary-md 스타일).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 14:05:35 +09:00
hyungi f325bd0509 feat(observability): digest/briefing 을 처리 보드에 맥미니 작업으로 노출 (background_jobs)
큐 밖 cron 생성 작업(global_digest/morning_briefing)이 processing_queue stage 가
아니라 보드에 안 잡혀, 맥미니가 11분짜리 digest 를 돌려도 idle 처럼 보였다.
ebbcaf8 의 background_jobs 메커니즘 재사용:
- digest_worker/briefing_worker = start_job→finish_job (best-effort, 본작업 무해)
- pipeline = cluster 완료마다 heartbeat(processed/total) → 진행바
- queue_overview = kind→machine 맵으로 payload 에 machine 필드 (맥미니 귀속)
- 보드 = 머신 레인에 dot 점등 + "생성 중: <label> N/T" 표시

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 03:36:57 +00:00
hyungi ebbcaf86d8 feat(observability): 큐 밖 백그라운드 작업(backfill)을 처리 머신 보드에 노출
processing_queue 는 파이프라인 stage 전용이라 hier_overnight_backfill 같은 off-queue
관리 스크립트 작업이 대시보드 보드에 안 잡혀, 다른 세션이 모르고 fastapi 를 재생성해
in-flight 재분해를 끊는 사고가 발생(2026-06-14). 사각지대 해소.

- migrations/357_background_jobs.sql: background_jobs 테이블(kind/label/state/processed/
  total/heartbeat). worker_jobs(user_id 필수, worker-pool 전용)와 별개.
- services/background_jobs.py: start/heartbeat/finish 헬퍼 — 자율 트랜잭션(즉시 commit →
  실시간 가시화) + best-effort(관측 실패가 본작업 안 깸).
- hier_overnight_backfill: 작업 시작/절 ~10개마다 heartbeat/종료 계측.
- queue_overview: /api/queue/overview 응답에 background_jobs 추가(running + 최근 6h 완료,
  stale=heartbeat 끊김 추정). SAVEPOINT 로 테이블 부재/오류 시 보드 본체 무영향.
- ProcessingFlowBoard: "백그라운드 작업" 패널(진행/경과/state, stale 끊김 경고).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 12:27:18 +09:00
hyungi c5bc1f773d fix(docpage): 비인접 window 를 parent_id 로 split-parent 에 흡수 (빈 본문 절 수정)
split-parent(절 헤딩)와 그 window 조각이 chunk_index 상 비인접인 경우(예: 5180 FOREWORD
헤딩 idx 1143, window idx 1233~)가 있어, 인접 흡수만 하던 collapseWindows 가 split-parent 를
빈 본문 행으로 남기고 window 들은 따로 대표 행을 만들어 "같은 제목 2행(빈 것 + 본문 있는 것)" 이
됐다. 사용자가 "본문 없는 절" 로 본 것.

- /sections API 에 parent_id 반환 (window.parent_id = 그 split-parent chunk_id, 100% _split 링크)
- collapseWindows 가 window 를 parent_id 로 split-parent 대표에 흡수(비인접 허용), 인접 heading
  fallback 유지(legacy window). 흡수 멤버에서 본문/분석 집계.
- 회귀 테스트: 비인접 parent_id 흡수 (12/12 pass)

실데이터 검증(빈 본문→0): 5180 outline 85→58·5210 318→277·5178 73→49·5151 45→40, 전부 EMPTY_BODY=0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 07:46:18 +09:00
hyungi d007ad5492 fix(docpage): windowed 절에 조각별 분석(유형/신뢰도/요약) 집계 노출
절-레벨 분석(chunk_section_analysis)은 코퍼스 전역에 이미 있으나(절 보유 344문서 중 336)
window 조각의 chunk_id 에 붙어 있고, D3 는 window 를 split-parent 대표로 collapse 하며 버려서
windowed 절은 요약/유형/신뢰도가 안 떴다(분석은 대표가 아닌 조각에 있음).

- collapseWindows 가 멤버(대표+흡수 window)에서 절-레벨 분석 집계:
  sectionType=다수결(동률 첫등장) · confidence=평균 · summaries=조각 요약 배열(빈 것 제외)
- D3 트리/focus/모바일카드/이전다음이 it.sectionType/it.confidence/it.summaries 사용
- 요약은 단일 절=문단, windowed 절="절 요약 · N개 부분" 번호목록
- headingPath.test.ts: 집계 회귀 테스트 추가 (11/11 pass)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 07:27:41 +09:00
hyungi b6a4821cac fix(docpage): 절 본문을 청크 text로 렌더 + window 조각 collapse
대형 split 문서는 marker 가 md_content 를 앞 5만 자만 보존하고 char_start 도 NULL 이라
char_start 슬라이스로는 절 본문이 비었다. 전체 본문은 document_chunks.text 에 절별로 보존됨.

- /sections API 가 청크 text 반환 (SectionItem.text; 소비자=D3 단독, additive)
- collapseWindows 가 window 조각 본문을 대표 절 bodyText 로 합본 (split-parent heading 제외)
- D3 페이지가 outline(collapseWindows) 단위로 렌더 → window 파편화 제거
  (5180 = 27 논리 절이 562 동일제목 조각으로 쪼개지던 문제)
- useSectionView=hasSections 로 단순화(partial/대형 문서도 절뷰), 모바일 본문 lazy 파싱
- headingPath.test.ts: bodyText 누적 회귀 테스트 추가 (10/10 pass)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 07:10:59 +09:00
hyungi 540bc00dba feat(docpage): D3 절 구조 탐색기 — 슬림 인사이트 레일 + 절 트리 (frontend only)
문서 상세 /documents/[id] 재구성 (BE 무변경):
- 우측 탭(정보/AI/관리) → 슬림 전역 인사이트 레일: 요약·심층·불일치를 탭
  게이트 없이 상시 노출(details open, 모바일은 접기 가능), 정보/관리는 접이.
  → 가공 자료가 탭/온디맨드에 묻히던 IA 문제(G1) 해소.
- SectionOutline 절 목차 레벨 기반 들여쓰기(평탄→트리 모양).
- 모바일: 본문 메인 + 절목차/인사이트/정보/관리 접이 + 절 탭 본문 이동(기존 구조 활용).
관련 문서(See Also)는 v1 제외(자리만 유지). 심화 목업 = comparisons/2026-06-13-ds-docpage-d3-deepened.html.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 15:18:15 +09:00
hyungi 595f4b7d5e feat(board): 통합 보드 v3 — 머신 레인 + 정직 번다운/ETA (B-1·2·3·5)
ProcessingFlowBoard 를 통합안으로 재작성:
- 머신 3레인(GPU/맥미니/맥북) = "누가 일하나" + 요약 오프로드 가시화
  (요약 칩 분담 막대 맥미니 vs 맥북 + 맥북 레인 '요약 합류' 칩, summarize_by_machine 소비)
- 지배 백로그 스트립 + 정직 ETA(summarize_eta, 유입 차감 / null=소진 불가)
- 24h 번다운 SVG(유입 vs 소화) + 맥북 합류 변곡점 + 단계별 정직 ETA 미니리스트
- 신선도 '갱신 N초 전' + stale 경고(queueUpdatedAt, B-4)
- 실패 드로어 + 노드 상세 패널은 v2 자산 그대로 재사용
- 레인 stack + 칩 wrap 으로 모바일 반응형

svelte-check: 변경 파일 에러 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 14:01:50 +09:00
hyungi b630c31077 feat(board): expose summarize_by_machine for offload visibility (A-1)
요약 풀의 머신별 완료 실적(맥미니 vs 맥북)을 /api/queue/overview 응답에
summarize_by_machine 로 노출. rows_to_summarize_split 이 이미 계산하던 값의
additive 투영 — 신규 수집 SQL/마이그 0. 통합 보드 레인의 오프로드 가시화
(맥북이 요약 86% 처리) 재료. + FE 타입 동기 + store 신선도 timestamp(B-4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 13:54:39 +09:00
hyungi d05e41128a fix(board): 실패 뱃지 잘림(스크롤 컨테이너 헤드룸) + 구 단계별 현황 섹션 제거 + ETA 48h+ 일 표기
- 흐름 컨테이너 pt/px 헤드룸 — -top/-right 돌출 뱃지가 overflow-x-auto 에 잘리던 문제
- 단계별 현황 details = 흐름 보드가 대체(R2 통합안 의도) — 전용 파생값/헬퍼/chevron 동반 제거
- etaShort: 48시간 이상은 일 단위 (약 131시간 → 약 5.5일)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 03:02:04 +00:00
hyungi 5581d3f1ce feat(board): 처리 보드 v2 — 파이프라인 흐름 뷰·엔진 구분·실패 재시도/건너뛰기 (ds-board-engines-1)
- 흐름 뷰 메인: 좌→우 노드(머신·엔진 태그, 유입 우세 amber, 실패 뱃지) + 머신 스트립(모델 표기) + trend_24h 스파크라인 첫 렌더
- 노드 클릭 상세 패널: KV 4칸 + 다중 stage 행 + 지금 처리 중
- 실패 처리 드로어: 에러 패턴 그룹 + 재시도/건너뛰기 (영구 실패의 첫 사용자 조치 경로)
- API: stages[].done_1h/created_1h 노출 + GET /api/queue/failed + POST /api/queue/retry|/skip (uq_queue_active 충돌 skip, 건너뛰기는 enqueue_next_stage 미호출)
- 엔진/모델 표기 = queueDisplay.ts 정적 맵 단일 지점 (모델 교체 시 1곳)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:05:04 +00:00
hyungi fdac449a48 Merge pull request 'Feat/eid chat' (#35) from feat/eid-chat into main
Reviewed-on: #35
2026-06-11 15:14:43 +09:00
hyungi 250896cdfa feat(eid): deep 모드 = ReAct 자동검색 + 근거 카드 (ds-eid-ask-absorb P1)
- deep 분기 _eid_chat_deep: 비생성 probe → phase:searching → agentic_ask_loop
  (tool_choice=auto 가 검색 여부 자율 판단, 검색 불요는 early-exit 대화) → final_answer
  + eid_sources envelope → DONE. heartbeat {phase:ping}(~10s, 프록시 idle timeout 차단)
  · mid-stream BackendUnavailable → in-stream error envelope · disconnect 시 task.cancel()
  + await(고아화·27B 점유 방지).
- daily = call_stream 무변경(맥미니 대화). deep = 맥북 27B ReAct (tool calling 27B 전용,
  맥미니 26B token-leak 미검증). 멀티턴 = 메시지 단독 처리(agentic_ask_loop query: str,
  history 2단계 백로그).
- EidEvidenceCard.svelte 접이식 근거 카드(sources 순서번호·제목·점수) + 프론트 SSE 파서
  확장(ping/searching/error/eid_sources) + 검색 중 표시 + 이력 보존.
- 테스트: deep 4건(검색성/대화성/probe-503/mid-stream-error) + 기존 call_stream 회귀 daily
  로 이전 = 29 passed.
- 동반(이전 eid-chat 세션 미커밋): /api/eid/status endpoint + llm_gate.gate_status +
  test_eid_status (채팅 대기 UI 의 '대기 vs 고장' 구분용, 5 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:51:00 +09:00
hyungi 7031439364 feat(ui): 단계별 현황 재설계 — 완료 가시화 + 빈 단계 숨김 (사용자 피드백)
'대기만 보이고 성공은 안 보인다' 피드백 반영:
- overview 에 stages[] 노출 (stage 별 done_today + oldest_pending_age, SQL 1필드 추가)
- 게이지 의미 전환: 단계 간 대기량 비교(amber) → 단계 내 오늘 진척(완료=green 비율,
  가득 찬 초록 = 다 끝남) + 처리 중 pulse dot
- 움직임 없는 단계는 행 제거, 하단 '비어 있음: ...' 한 줄로
- 라벨 누수 fix: details 가 구 STAGE_LABEL 을 쓰던 것 → queueStageLabel 통일
  (deep_summary/markdown/summarize/chunk/fulltext 한글화)
- 헤더: 오늘 N 완료(성공 가시화) · 실패(error) · 대기. 데이터 소스 = overview 단일화

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:26:27 +09:00
hyungi 468804494d feat(ui): 처리 머신 보드 — 누가 일하나 (안2) + ETA·전 페이지 스트립/드로어 (안5/6 라이트)
plan ds-processing-ui-6an (시안 choice 채택: 안2 1차 + 안5/6 지원):
- GET /api/queue/overview — 머신(GPU/맥미니/맥북) 귀속 라이브 집계 5쿼리, 마이그레이션 0.
  summarize 풀 완료 실적은 documents.ai_model_version 조인으로 맥북/맥미니 분리,
  보류(deferred_until)=맥북 카드 귀속, state=active/deferred/idle. raw 모델명 비노출
- 홈: 처리 머신 보드(3열 카드 + 지금 처리 중 제목) + ETA 라인(유입 우세 시 null 명시),
  기존 stage 테이블은 details 접힘으로 강등 (구조 개편)
- 전 페이지: 상태 스트립(처리중·대기·실패·맥북 칩) + 우측 드로어(QueueDrawer,
  dialog a11y) — 공유 60s 폴링 store, 경량 fetch(401 강제 logout 부수효과 회피)
- tests: 판정부 30건 (귀속/풀 분리/state 9케이스/ETA 경계/trend 버킷/계약 shape)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:13:35 +09:00
hyungi cd06ef0403 feat(eid): 이드 채팅 표면 — /api/eid/chat SSE 스트리밍 + /chat 페이지 (P1)
- compose: eid_chat surface 등록(persona+rules, 자유-prose) + rules_present() 라이브 판정(D-6 fail-closed)
- EidAIClient.call_stream: 닫힌 mode 매핑(daily→mac-mini-default/deep→qwen-macbook), router 경유,
  MLX gate(FOREGROUND)+wall-clock 300s deadline, SSE 라인 relay(model→mode 치환·usage 제거),
  router 400 fail-loud, error_reason allowlist sanitize
- POST /api/eid/chat: JWT, role=system 422 거부, 8000자/40턴/총량 32000 cap,
  503 error_reason(ask 컨벤션), 본문 무로깅
- frontend /chat: 이드 표면 문법(일상/심층, 모델·머신명 비노출), SSE 파서(경계 buf·flush·[DONE]),
  error_reason UX, 8000자 선차단+422 오염 차단, localStorage 이력(logout 시 제거), nav 등록
- Caddyfile: encode 명시 match로 text/event-stream gzip 버퍼링 제외
- tests: 신규 32+ (fixture: router 경유 26B/27B SSE 박제), tests/eid 61 + ask 회귀 9 = 70 passed
- 적대 리뷰 3렌즈 18 finding 반영 13/13. 배포는 D26 게이트(fix/hwp 머지+Soft Lock) 대기

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 11:16:44 +09:00
hyungi aeb9290cbd feat(documents): hier 절 char_start offset (Path B) — md_content 점프 builder offset
플랜 ds-outline-anchor-b5 (g1~g6 코드). 핵심 ASME/법령 windowed 절의 0% 점프를
서버계산 char_start(builder offset)로 100% deterministic 점프로 전환.

- g1 migration 318: document_chunks.char_start INTEGER NULL (단일 statement, 멱등)
- g2 builder: char_start emit = FE 라인/offset 모델 미러(split('\n')+UTF-16 code unit+코드펜스 skip).
  window-child=NULL, split-parent=heading offset, preamble=NULL, CR 미strip, NFC=telemetry.
  node.text 보존(라인모델 hash-neutral) → hash_stable doc 보존. 단위테스트 7건.
- g3 persist+backfill 하이브리드:
  * persist INSERT char_start
  * update-char-start (g3-tU): hash_stable doc 비파괴 — 100% jump-target VERIFY(NEW-1) +
    position-aligned PK UPDATE(NEW-2), 미달 doc DEMOTE → re-decompose 합류(NEW-4)
  * --reprocess (g3-t2): md_content 출처(g0-t1) + jump-target-set 완료마커(B1) + B_jumptarget>=1(B3),
    --doc 필수 else REFUSE. self-heal sweep(g3-t3).
- g4 /sections: char_start inner+outer SELECT + split-parent 노출(is_leaf OR %_split)
- g5 FE: resolveAnchorMap(BE-first, NEW-5 jump-target-candidate-scoped 폴백, C1 OR-exclude),
  per-render-site basis guard(C3), endsWith('_split') 정정 + collapseWindows split-parent 흡수(C2).
  단위테스트 25건(NEW-5/B4/C1/C2 포함).
- g6 hier_outline_quality_gate.py: read-only g-measure(verdict/B_jumptarget/hash_stable/dup/fence)

배포(g7: --no-deps, 스냅샷, UPDATE-only 32 + re-decompose 230∪demote, 정확도 게이트)는 별 ops 단계.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 10:12:26 +09:00
hyungi 988631fdb6 feat(documents): 3-pane 중앙 리더에 절 목차 rail + 점프 + scroll-spy
[id] 전체보기에만 있던 개요 rail/점프를 메인 /documents 3-pane 중앙 리더로 확장
(사용자 주 사용 표면). 경로 A anchor 인프라 그대로 재사용.

- /documents/{id}/sections fetch(loadSections, doc.id 가드) → 좌측 SectionOutline rail
  (showRail = 표시가능 절 有 + markdown-ish 본문). window 빈제목 31% 노이즈는 outlineSections
  필터로 표시 제외(클린업, 코퍼스 무터치).
- anchorMap = buildAnchorMap(mdRenderText, sections) — 각 분기가 실제 렌더하는 텍스트 기준.
  MarkdownDoc(markdown/pdf/hwp/article)에 anchorMap 전달 → <span id=sec-N> splice.
- jumpTo = scrollEl 내 #sec-{id} scrollIntoView. scroll-spy = scrollEl scroll 리스너로
  상단 통과 마지막 .md-anchor → activeKey(SectionOutline 강조). $effect cleanup.
- 본문을 [rail | scrollEl] flex 로 래핑(비-섹션 문서는 rail 미표시=기존 그대로). pdf 분기는
  자체 overflow 제거하고 scrollEl 단일 스크롤로 정리(iframe h-[80vh]).

id↔id 점프라 중복제목·비-ATX 정확, anchor 없는 절=비활성(폴백). FE only, BE 무변.
vite build + node test 10/10 + lint:tokens(신규0) PASS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:26:08 +09:00
hyungi 5c065e6bec feat(documents): 개요 점프 결선 — anchor splice + id↔id 점프 + scroll-spy ([id])
불만② 개요→본문 점프를 deterministic 하게 결선(경로 A). 상세페이지([id], 개요 rail 보유).

- MarkdownDoc: anchorMap prop 추가 → 렌더 전 md_content 의 각 offset(내림차순)에
  <span id="sec-{chunkId}" class="md-anchor"> splice(점프 타깃). DOMPurify span+id+class 통과.
- SectionOutline: onJump(chunkId)/activeKey prop. 클릭=아코디언 toggle + onJump(점프).
  activeKey 일치 항목 좌측 accent border 강조(scroll-spy).
- [id]: anchorMap=buildAnchorMap(md_content, sections)(canShowMarkdown 시) → MarkdownDoc 전달.
  jumpToSection=#sec-id scrollIntoView. scroll-spy(window scroll, 120px 상단 통과 마지막 anchor).
  SectionOutline 양쪽(xl rail·details)에 onJump/activeKey 배선.

id↔id 직매칭이라 중복제목(표-1·Part UW 814건)·비-ATX(제N조) 정확. anchor 없는 절=점프
비활성(아코디언 폴백). node test 10/10, vite build + lint:tokens(신규0) PASS.
다음 = 3-pane(DocumentViewer) 개요 rail(commit 3, 레이아웃).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:17:07 +09:00
hyungi e1a047c2c2 feat(documents): 개요 점프 anchorMap 유틸 (forward-cursor 3중 방어)
불만② 개요→본문 점프의 deterministic anchor 좌표 산출(경로 A, FE-only).
게이트 측정상 textContent 매칭은 중복 63%·비-ATX 로 5% + silent 오점프 → md_content
에서 각 절 heading 라인 offset 을 찾아 <a id="sec-{chunk_id}"> 주입 좌표를 만든다.

★ false-early-match 방어 3중 (적대 리뷰 반영):
- 라인-시작(전체-라인) 매칭 → 본문 중간 상호참조("see Part UW")는 라인 전체가 제목과
  같지 않아 제외(forward-cursor 가 못 막던 핵심 구멍).
- 전체 매칭 + truncation(builder [:200]) 처리 → '제1조'가 '제1조의2' 오매칭 차단.
- 단조 커서 + 코드펜스 회피 → 역행/펜스 매칭 거부 = anchor 없음(점프 비활성, 오점프 금지).

window/section_split 조각·빈 제목은 skip. node test 10/10 PASS(상호참조 선행·중복 단조·
prefix·평문 제N조·펜스·window·miss·heading_path fallback). 순수 함수, vite build PASS.
다음 commit = MarkdownDoc splice + SectionOutline 점프 + DocumentViewer rail/scroll-spy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:11:00 +09:00
hyungi 360871e9cf feat(documents): 3-pane 중앙 리더 markdown-first 일원화 (DocumentViewer)
메인 /documents 3-pane 의 중앙 리더(DocumentViewer)가 md_content 를 안 쓰고
PDF=raw iframe·md/txt=plain marked(extracted_text)만 렌더하던 이원화 제거.
"전부 MD화" 한 canonical markdown 이 전체보기 없이 메인에서 바로 보이게 함(불만①).

- viewerType.ts 신설: 분류 단일 source(상세페이지와 공유 예정, drift 차단).
  csv/json/xml/html→text(<pre>, 콤마 뭉침 회피), office→preview-pdf, hwp→hwp-markdown.
- DocumentViewer: 자체 getViewerType/renderMd(본문) 제거 → viewerType.ts + MarkdownDoc.
  - pdf: canShowMarkdown(isMdSuccess+md_content) 시 MarkdownDoc 기본 + [Markdown|PDF원본]
    토글 + MarkdownStatusBadge, 아니면 PDF iframe. lastDocId 가드는 fullDoc.id(prop) 키잉.
  - markdown(md/txt): MarkdownDoc(extracted_text=표시·편집 단일 필드), 편집 유지.
  - hwp-markdown/article: MarkdownDoc(앵커/KaTeX/이미지). 편집 미리보기만 plain marked 유지.
  - article/preview-pdf/image/text/cad/synology/unsupported 분기 보존(회귀 금지) + synology 신설.

API md_status='completed'(S1 validator live) 대응 = isMdSuccess. FE only, BE/스키마 무변.
vite build + lint:tokens(신규 위반 0) PASS. 후속: 개요 rail·안전점프(commit 2), [id] 정합(commit 3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:44:46 +09:00
hyungi 4042d9ec61 fix(ui): md_status 'success'/'completed' 어휘 양립 (S1 API remap 대비)
S1 backend(이미 main 머지, app/api/documents.py field_validator
_db_success_to_completed)가 직렬화 시 DB 'success'를 API 'completed'로 remap한다.
그런데 프론트 3곳이 raw 'success' 만 검사 → S1 backend 배포 시 침묵 회귀:
  - documents/[id]/+page.svelte canShowMarkdown: completed PDF가 markdown-first
    대신 raw PDF로 표시
  - documents/+page.svelte 인스펙터 칩 게이트: success 문서 칩 사라짐
  - MarkdownStatusBadge: 'completed'→default→null (성공 칩 사라짐)

DB↔API enum divergence guard: 두 어휘를 모두 성공으로 취급해야 S1 배포
전(API='success')·후(API='completed') 모두 안전. 단일 source 헬퍼로 수렴.

- lib/utils/mdStatus.ts 신설: isMdSuccess / isMdStatusVisible (raw 비교 산재 금지)
- [id] canShowMarkdown → isMdSuccess()
- documents 인스펙터 게이트 → isMdStatusVisible()
- MarkdownStatusBadge: case 'completed' 를 'success' 동의어로 추가

FE only, 백엔드/스키마/마이그레이션 무변. vite build + lint:tokens(신규 위반 0) PASS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:48:38 +09:00
hyungi 7b8524192d fix(ui): 인스펙터 md상태 칩 enum 버그 (success 항상 노랑) + article suppress
documents/+page.svelte 인스펙터의 md상태 칩이 doc.md_status==='completed'
비교였는데 실제 enum은 success/partial/skipped/failed/pending 이라 'completed'가
존재하지 않음 → success 여도 항상 text-warning(노랑)으로 표시되던 라이브 버그.

- documents/+page.svelte: 깨진 삼항을 MarkdownStatusBadge 재사용으로 교체.
  success→success(초록) 자동, pending/null→null 이라 article(news) 칩 자동 suppress.
  표시 조건을 badge 가 렌더하는 5상태로 명시(빈 라벨 행 방지).
- MarkdownStatusBadge: partial case 추가(tone warning 'Markdown 일부') →
  대형 split 일부 실패 문서도 칩 노출 + md_status 표시 어휘를 단일 컴포넌트에 완결.

FE only, 백엔드/스키마 무변. vite build + lint:tokens(신규 위반 0) PASS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:35:05 +09:00
hyungi 279124d953 feat(ui): 학습 진단(이드 코치) 허브 진입점 + /study/diagnosis 전용 라우트
diagnosis는 cross-topic(사용자 단위) 코칭 표면인데 기존엔 /study/topics 상단에만
노출돼 발견성이 낮았다. 허브(/study)에 '학습 진단' 카드 추가 + 전용 라우트
/study/diagnosis 신설(향후 weekly_recap·review_set_draft 코치 표면의 정식 홈).

패널은 StudyDiagnosisPanel 공유 컴포넌트로 추출 — topics·diagnosis 양쪽이 단일
청크 참조(복붙 drift 0). 백엔드 무변경(기존 POST /diagnosis/generate 재사용).

검증: vite build OK, lint:tokens 내 파일 위반 0, 새 라우트·허브 링크·공유 청크
번들 반영 확인.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:35:35 +00:00
hyungi 2c8b6808b9 feat(study): 복습함(B4 v1) — 오늘 할 일/미확인 2탭 + 멀티셀렉트 선택 복습
/study/review-box: GET /study-cards/due(review_stage) 를 2탭 분리(오늘 할 일=review_stage 보유 / 미확인=review_stage null 신규). 카드 멀티셀렉트 → pendingReviewCards store 로 cards-study 복습 세션에 선택분 전달(백엔드 세션 X = eid contention 중 fastapi 무재빌드). '이 탭 전체 복습'도. 완료 탭은 졸업카드 엔드포인트 필요라 비활성('추후'). 허브에 복습함 진입 카드.
- 신규 store /stores/studySession.ts(pendingReviewCards). cards-study startReview 가 consume. 전부 frontend-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:17:31 +09:00
hyungi aa2d7814e3 feat(digest): date picker URL sync + article→문서 라우팅 + country 국기·한국어
- GET /api/digest/dates 신설 (브리핑 /briefing/dates 패턴 미러, read-only)
- topic article 제목 enrich (documents 배치 1쿼리 + dedupe(set) + map-miss=null → 프론트 '(제목 없음)')
- /digest 재작성: ?date=&country= URL sync(공유·뒤로가기), 국가 탭=인라인 SVG 국기+한국어, 기사=/documents/{id} 링크(상위5+펼치기)
- Phase 4.5(PR #22) 후속. 검증: py_compile·dates/enrich 쿼리(275 resolve·miss 0)·frontend docker build PASS. 시각 렌더 검증=preview 게이트 대기

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 23:39:07 +00:00
hyungi f7198d9d68 feat(search): expose hier section outline & summaries in document detail
PR-DocSrv-Hier-Section-UI-1 Phase 1 (코드+커밋만, 배포는 Phase 2 backfill 완주 후).

- backend: GET /documents/{id}/sections — hier leaf 목차 + chunk_section_analysis
  요약. document_chunks 직접 조회(retrieval 아닌 목차 표시라 corpus_chunks 뷰
  의도적 우회 — docstring 명시). DISTINCT ON 으로 최신 분석 1행.
- frontend: SectionOutline.svelte(좌측 목차, per-doc 동적 그룹/flat, window
  dedupe, 클릭 시 요약/breadcrumb 인라인), headingPath.ts 순수 유틸(+node:test
  단위테스트 8케이스). [id]/+page.svelte 3-zone 레이아웃 + 우측 메타 Tabs
  [정보|AI|관리] 로 카드 스프롤 해소.
- 절 없는 문서/404 는 목차 숨김(graceful). 본문 점프는 follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:22:34 +00:00
hyungi c086c9f85d feat(ask): /ask backend selector + 503 macbook_unavailable UI
선행 PR-MacBook-RAG-Backend-1 (main a7b8f15) backend dispatcher 의 frontend
소비. /ask 페이지에 backend selector (default | qwen-macbook) + URL
?backend=qwen-macbook 지원 + 503 friendly empty state + "Default 로 재요청"
버튼 (backend param 명시 제거 → 무한 루프 0).

정책 (선행 PR 그대로 유지):
- default / backend 미지정 = Gemma Mac mini (현 path 변동 0, 기존 호출자 호환)
- backend=qwen-macbook = MacBook 명시 opt-in. unavailable 시 HTTP 503 +
  error_reason=macbook_unavailable. Gemma 자동 fallback 0.

변경 4 파일:
- types/ask.ts: AskResponse 에 backend_requested / backend_used 필드 +
  SynthesisStatus 에 backend_unavailable literal 추가
- api.ts: ApiError 에 errorReason 추가, parseDetail 이 503 body 의
  error_reason 흡수 (다른 endpoint 영향 0)
- AskAnswer.svelte: backend_requested 명시 시 muted chip 표시
  (default 호출은 미표시, 시각 noise 회피)
- routes/ask/+page.svelte: selector dropdown + URL state + 503 분기

Non-Goals (별 PR):
- localStorage / Settings preference (PR-DocSrv-Ask-Default-Pref-1)
- SSE streaming, Tool-calling ReAct
- shared secret / MacBook auth (Tailscale ACL only)

검증: docker compose build frontend 통과 (svelte-check + vite build).
lint:tokens 본 PR 변경 위반 0 (기존 62 건은 baseline stale debt, settings/login).

Spec: ~/.claude/plans/document-buzzing-codd.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:47:41 +00:00
Hyungi Ahn 6d71116553 feat(events): PR-2 UI MVP — 4-tab + 빠른 행동 기록 + 상세/생성/이력
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 의 자연 사용으로 닫힘.
2026-05-11 07:56:31 +09:00
Hyungi Ahn 8ca27eb573 fix(markdown): img auth via ?token= query param (Authorization header 미지원)
`<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>
2026-05-10 14:47:09 +09:00
Hyungi Ahn 68fa86ea52 feat(markdown): persist extracted images with auth routes
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>
2026-05-10 14:05:41 +09:00
Hyungi Ahn d6e0f5de04 feat(frontend): Phase 1C — markdown viewer 완성 (PDF 통합 + status badge + image placeholder)
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>
2026-05-02 15:38:45 +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 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 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 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
Hyungi Ahn f005da2e83 ops(study): pressure 파이프라인 진단 패널 — raw/mapped/final 3단계 + tilt/buttons
사용자 분석: 수치 튜닝 무관해 보이면 pressure 입력 자체가 안 들어오는 케이스. perfect-
freehand 옵션 변경 의미 없음. 먼저 PointerEvent.pressure 가 실제로 변동하는지 확인 필요.

진단 패널 (?debug=1) 에 추가:
- PRESSURE PIPELINE 섹션:
  · raw  = PointerEvent.pressure 원본
  · mapped = getStrokePressure 의 inputP (raw 매핑 또는 속도 fallback)
  · final = fixedPressure update 후 perfect-freehand 에 전달되는 값
  · raw min/max — 세션 내 raw pressure 범위 (사용자가 펜 강약 시도 후 확인)
- tiltX, tiltY, ptr width/height, buttons — Pencil 추가 입력 필드.

판별:
- raw 가 항상 0.5 또는 1.0 → 디바이스/브라우저에서 pressure 미전달.
  현재 환경에서는 속도 기반 fallback 이 유일.
- raw 가 변동 (0.1~1.0) 인데 mapped/final 이 일정 → 우리 코드가 무시 중.
- raw + mapped + final 모두 변동 → perfect-freehand 가 무시 (thinning, simulatePressure).
2026-04-27 15:54:23 +09:00
Hyungi Ahn 8b27eadf2e feat(study): PEN_PRESET_NOTABILITY_LIKE — 사용자 지정 프리셋 적용
사용자 분석 + 1차 프리셋 반영:
- streamline 0.75 → 0.45. 입력 lazy 줄여 손끝-잉크 latency 감소.
- smoothing 0.99 → 0.82. 기계적 보정 줄여 자연스러운 필기감.
- thinning 0.35 → 0.45. 변동 폭 키워 필압 차이 명확.
- WIDTH_FACTOR { 0.35, 0.50, 0.85 } → { 0.38, 0.55, 0.90 }.
- MAX_GAP_PX 16 → 6. 빠른 stroke 점선 차단 (촘촘 보간).
- start.taper size×0.20 → ×0.15. end.taper ×0.40 → ×0.25. Notability felt.
- cap: false → true. 둥근 끝점.

Smart pressure 강화 (획 내부 균일):
- PRESSURE_FLOOR 0.5 → 0.6. 약한 입력에서도 선 사라지지 않음.
- FIRST_POINT_PRESSURE 0.7 → 0.72.
- FIXED_THRESHOLD 0.15 → 0.18. 잡음 범위 넓게.
- FIXED_ALPHA_NOISE 0.03 → 0.015. 잡음 더 강하게 무시 → 획 내부 균일.
- FIXED_LARGE 0.30 → 0.32.
- FIXED_ALPHA_INTENT 0.50 → 0.40.

getCoalescedEvents 이미 사용 중 — Chrome 의 raw sample 활용 보장.

테스트 기준:
1. 빠른 가로선 점선 안 됨.
2. 천천히 세로선 굵기 출렁이지 않음.
3. 강/약 stroke 차이 보이되 약한 stroke 도 끊김 없음.
4. 한글 자모 빠르게 이어쓸 때 두 번째 획 누락 없음.
5. Chrome 기준 우선 통과.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:51:10 +09:00
Hyungi Ahn 1ba425f07a fix(study): visual continuity — pressure floor 0.5 + thinning 0.35
사용자 보고: 빠른 stroke 가 점선처럼 끊겨 보임 ("선이 이어지지 않음").

원인: 속도/raw pressure 기반 inputP floor 가 0.25 ~ 0.3 → thinning 0.5 적용 시
outline 폭이 size × 0.5 미만 → 픽셀 단위 정렬 안 되면 dot 패턴.

Fix:
- 속도 기반 inputP floor 0.25 → 0.5. 가장 빠른 stroke 도 size × 0.825 폭 보장.
- raw pressure 매핑 0.3~1.0 → 0.5~1.0. min 폭 보장.
- thinning 0.5 → 0.35. 변동 폭 줄임 (min 폭 더 보장).

Trade-off: 굵기 변동 폭 줄어듦. 하지만 사용자 우선순위 = visual continuity.
inputP 0.5~1.0 + thinning 0.35 → 폭 변동 ±17.5% (충분히 보임).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:47:02 +09:00
Hyungi Ahn fb73f96d2e fix(study): 강한 압력 즉시 반응 — 3단계 threshold + dynamic range 확장 + thinning 키움
사용자 보고: 빡세게 눌러도 굵기 차이 거의 안 남.

원인 분석:
1. raw pressure 0.1~0.99 만 활용했는데 dynamic range 그대로 → 변동 작음.
2. 속도 기반 변동 폭 0.3~1.0 작음 + dist/25 비율 작음.
3. INTENT alpha 0.25 너무 느림 → 강한 변화도 stroke 내내 못 따라감.
4. thinning 0.4 변동 폭 부족.

Fix:
- raw pressure 0.1~0.99 → 0.3~1.0 으로 매핑. dynamic range 확장.
- 속도 기반 0.25~1.0 + 비율 dist/18. 변동 폭 키움.
- 3단계 threshold:
  · dev < 0.15 (잡음) → alpha 0.03 (fixed 유지)
  · 0.15 ≤ dev < 0.3 (의도적) → alpha 0.5 (이전 0.25 → 빠르게 따라감)
  · dev ≥ 0.3 (매우 큼, 빡세게 누름) → 즉시 update (alpha 1.0)
- thinning 0.4 → 0.5. 폭 변동 더 명확.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:44:05 +09:00
Hyungi Ahn 294bd775a9 feat(study): smart pressure (fixed + intentional change) + 굵기 균형 재조정
사용자 보고 통합:
1. "기본이 두꺼움" — 평소 stroke 가 두껍게 느껴짐
2. "힘 줘도 일정 이상 안 두꺼워짐" — max 굵기 부족
3. "약하게 그리면 점선" — min 폭 너무 작음
4. "압력 정해지면 stroke 그 굵기 유지" — Notability felt = stroke 내부 일정
5. "의도적 압력 변화 시 굵기 변동" — 단 명확한 변화는 따라옴

Fix:
- baseSize 6 → 7. max 두꺼움 보장.
- WIDTH_FACTOR { 0.4, 0.6, 1.0 } → { 0.35, 0.5, 0.85 }. 기본 살짝 가늘게.
  결과 normal = 7×0.5 = 3.5 (이전 3.6 비슷), thick = 5.95 (충분히 두꺼움).
- thinning 0.55 → 0.4. fixedPressure 가 잡음 흡수하니 폭 변동 더 키워도 안정.

Smart pressure (getStrokePressure):
- raw pressure 정상 시 → 그것 사용 (Pencil pressure 활용).
- 비정상 시 → 점 간 거리 기반 속도 추정 (mouse / Pencil 미지원 빌드).
- fixedPressure: stroke 시작 시 inputP 로 초기화. 그 후 hybrid update:
  · 변동 < 15% (잡음/평소) → alpha 0.03 (거의 무시) → 균일 굵기
  · 변동 ≥ 15% (의도적 변화) → alpha 0.25 (빠르게 따라감) → 굵기 변화
- simulatePressure: true → false. getStrokePressure 가 자체 처리.

기존 smoothPressureWindow 제거. fixedPressure 가 동일 역할 + Notability felt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:41:09 +09:00
Hyungi Ahn 56efc6ffc5 fix(study): simulatePressure: true 항상 — Pencil pressure 미도달 시 속도 기반 fallback
사용자 보고: 마우스도 Pencil 도 굵기 변화 없음. iPadOS Safari 의 일부 빌드에서
Apple Pencil PointerEvent.pressure 가 정상 도달 안 하거나 일정 → 우리 thinning 0.55
적용해도 input pressure 가 일정이라 효과 0.

Fix: perfect-freehand 의 simulatePressure: true 항상.
- 점 간 속도 (거리) 기반 자동 pressure 추정.
- 빠른 stroke = 가늘게, 천천히 = 굵게.
- Notability 도 동일 felt (속도 기반 ink flow).
- pen 의 실제 pressure 는 무시되지만, 들어오지 않는 빌드에서는 어차피 무관.

stroke 별 simPressure 필드 / serializableStrokes 로직은 유지 (향후 분기 옵션 위해).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:32:05 +09:00
Hyungi Ahn 1a93c9cbe6 fix(study): 필압 굵기 차이 대폭 키움 — thinning 0.55, MIN_PRESSURE 0.25
해석 오류 정정: 사용자 "필압 너무 차이나" = "차이가 너무 *안* 난다" 의미였음. 종이
만년필 reference (4 stroke 굵기 차이 5:1) 가 *원하는* 수준이었던 걸 반대로 해석해서
thinning 줄였던 회귀.

Fix:
- thinning 0.18 → 0.55. 폭 변동 ±55%.
- MIN_PRESSURE 0.4 → 0.25. dynamic range 넓게 (0.25~1.0).
- PRESSURE_WINDOW 12 → 8. 압력 변화 빠르게 따라옴.

조합 시 실제 굵기 비율: 약한 stroke ≈ size×0.42, 강한 stroke ≈ size×1.0 → 약 2.4:1.
종이 reference (5:1) 보다는 약하지만 만년필 felt 명확.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:28:39 +09:00
Hyungi Ahn 9af928b7d7 fix(study): 압력 일관성 + dot 제거 — thinning 0.18, window 12, cap false
사용자 보고: 필압이 너무 차이남 (stroke 마다 굵기 들쭉날쭉) + stroke 끝에 dot 점.
종이 만년필 reference 와 비교 시 우리 앱이 작은 압력 변동에 너무 민감.

Fix:
- thinning 0.28 → 0.18. 폭 변동 ±18%. 작은 압력 차이가 큰 굵기로 변환되지 않음.
- PRESSURE_WINDOW 8 → 12. 평균 더 안정 → stroke 간 일관성.
- cap: true → false. round cap 이 짧은 stroke 에서 dot 처럼 보이던 회귀 제거.
  taper 가 끝을 자연스럽게 마무리하므로 cap 불필요.
- start.taper size*0.15 → 0.2. end.taper size*0.3 → 0.4. cap 없으니 taper 가 직접
  마무리 — 살짝 더 길게 두어 만년필 nib felt 유지.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:26:37 +09:00
Hyungi Ahn 580f3ab728 fix(study): 작은 글자 stroke 인식 — streamline 완화 + taper 짧게
사용자 보고: 글자가 작아지면 제대로 인식 못 함 (스크린샷의 작은 "유" 가 부서져 보임).

원인:
1. streamline 0.86 = 입력 점이 펜 위치보다 lazy 하게 따라옴. 긴 stroke 에선 부드러움
   이지만 짧은 stroke (작은 글자) 에선 lag 누적 > stroke 길이 → 펜이 떨어져도
   stroke 가 못 따라감 → 부서진 dot 처럼 보임.
2. start.taper size*0.3 + end.taper size*0.5 = 짧은 stroke (length ≈ size × 1~2) 의
   거의 전체가 taper 영역 → stroke 가 모두 가늘게 그려짐.

Fix:
- streamline 0.86 → 0.75. 부드러움 + 짧은 stroke 정확성 균형.
- start.taper size*0.3 → 0.15.
- end.taper size*0.5 → 0.3.

만년필 nib felt 는 유지 (taper 비율 그대로) 하되 영향 길이 줄임.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:24:14 +09:00
Hyungi Ahn b7058ba40b feat(study): Notability felt — start/end taper + ease-out
사용자 보고: Notability 의 그 맛이 안 남. 만년필 nib 의 핵심 felt 누락.

Notability 의 만년필 stroke 특징:
- 시작 = nib 이 종이에 닿는 순간. 짧게 가늘게 시작.
- 끝 = nib 이 종이에서 떨어짐. 좀 더 길게 가늘어짐.
- ease-out 곡선: 빠르게 굵어졌다 천천히 안정.

Fix:
- start.taper: size * 0.3, easing: t * (2-t) (ease-out)
- end.taper: size * 0.5, easing: t * (2-t)
- cap: true 유지 (round 끝점)

이전에 taper 가 흔들림 원인이라 뺐었지만, 그건 thinning 0.18 + 보간 점 micro 변동 +
EMA 와 겹친 회귀였음. 지금은 마디/흔들림 모두 차단됐으니 taper 안전하게 도입 가능.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:21:34 +09:00
Hyungi Ahn 30d32ad90c fix(study): 약한 pressure 에서도 stroke 폭 보장 — MIN_PRESSURE floor + thinning 완화
사용자 보고: 쓰다보면 필압이 줄어드는데 그러면 "학" 의 ㅡ 같은 부분이 거의 안
보이고 점선처럼 됨. 사용감 별로.

원인: thinning 0.4 + Pencil pressure 0.2~0.3 (약한 누름) → stroke 폭이 너무 줄어듦.

Fix:
- normalizePressure 에 MIN_PRESSURE 0.4 floor. pressure 0.05~0.4 도 0.4 로 고정.
  dynamic range 0.4~1.0. 약한 pressure 에서도 stroke 가 충분히 보임.
- thinning 0.4 → 0.28. 폭 변동 줄임. floor 와 조합 시 ±17% 정도 변동.

기존 폭 시작점은 유지 (만년필 nib 변화 명확).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:19:18 +09:00