Commit Graph

72 Commits

Author SHA1 Message Date
hyungi 3c42b7b97a feat(book): 공부도구 배선 — 노트/형광펜/암기카드(clause_study) + 책 리더 패널 2026-06-30 06:26:55 +00:00
hyungi a22b2c7647 feat(docs): 관련 문서(유사도 KNN) 엔드포인트+패널 + 법령/지침 splitter 2026-06-30 06:10:11 +00:00
hyungi 7487739aec fix(clause-kb): 절-문서 이미지를 부모 표준 document_images 로 폴백 해소(docimg 404 수정)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 23:38:37 +00:00
hyungi a8d3af2b62 fix(clause-kb): backlinks 엔드포인트 parent_id ORM 미매핑 → raw SQL 조회 (500 수정)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 23:34:18 +00:00
hyungi eb83d41ba5 feat(clause-kb): 책 API(절 목차/백링크) + /book/[id] 유기적 책 리더 + persist 스크립트 2026-06-29 23:13:34 +00:00
hyungi 8cdfe6006d feat(search): cloud-egress 게이트를 단건 문서 fetch 로 확장
GET /api/documents/{id} 가 egress=cloud 토큰일 때 search 와 동일한
cloud-eligibility 게이트(egress allowlist 갭2 + license 제한 B-4)를 통과한
문서만 반환. id 직접 fetch 로 비공개/인프라/개인/restricted 문서를 우회
열람하는 경로 차단 — 부적격은 404(존재 비노출). local 토큰=무회귀.

술어는 retrieval_service.cloud_eligible_doc_sql 로 단일화(_axis_sql
cloud_egress + _license_sql 합성) → search retrieval 과 byte-동일 게이트
공유, 경로별 드리프트 방지. MCP fetch_document 툴의 서버사이드 강제.

e2e: cloud 토큰 적격 Eng 200 / 인프라알림·리디북스memo·개인노트 404,
local 토큰 전부 200(무회귀).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:52:41 +00:00
hyungi 63be005c6f fix(security): 보안 위생 5건 — library admin 게이트·edit_url SSRF·보안헤더·8080 바인드·하드코딩 비번 제거
M3 library.py: categories POST/PATCH/DELETE + facets POST 를 get_current_user→require_admin
(공유 분류 CRUD 를 17주체→admin 한정, news/digest 패턴 정합).
M1 documents.py: update_document PATCH 에 edit_url validate_feed_url 가드 — 내부/메타데이터 주소
후속 fetch(fulltext_worker) latent SSRF 차단(API 레이어 무방비 해소, news.py 동형).
Caddyfile: 보안 헤더(nosniff·X-Frame SAMEORIGIN·Referrer-Policy·-Server). HSTS 는 edge 소관.
compose: caddy 8080:80 0.0.0.0→127.0.0.1 (LAN 우회 차단, 실 ingress=home-caddy→caddy:80 도커망).
scripts: 하드코딩 죽은 DB 비번 → os.environ (1차 감사 누락분, .env 한정 점검이 놓침).

별도(DB): test-% 계정 12개 비활성화 (공유풀 주체 17→5, 랜덤해시라 비번노출 아님·위생).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 05:48:02 +00:00
hyungi 403b05d971 fix(study): study/analyze LLM 타임아웃을 config 단일소스(llm_call_timeout_s)로 — 스테일 하드코딩 일소
study explanation/session-analysis/memo-card 워커 + study_questions/study_topics(subject-note·diagnosis)
+ documents.analyze 의 하드코딩 30~60s asyncio.timeout 7곳 제거. 빠른 Gemma 기준 리터럴이 Qwen 27B
교체(2026-06-11) sweep 누락 → 느린 콜을 잘라 사용자 대면 504 + 워커가 매 재시도마다 느린 콜 재실행해
문서가 큐에서 영영 못 빠지는 liveness halt. digest_llm_timeout_s 와 동형으로 config.pipeline.llm_call_timeout_s(300)
단일소스화. 다음 모델 교체 때 재발 차단.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 04:51:06 +00:00
hyungi 1f0be3312b feat(hier): 절 딥링크 — /clause 클릭 시 읽기뷰가 해당 절을 표시
문제: /clause 결과 클릭이 문서 첫 화면으로만 가고 해당 절로 안 감.
수정 2곳:
- /clause → /documents/{id}?section={chunk_id} 로 이동.
- 읽기뷰 defaultSelId 가 ?section=<chunk_id>(outline 에 존재 시)를 우선 선택 → 그 절 표시.
- 컨테이너 절(is_leaf=false 비-split, outline 부재: UG-136/UHX-13 등 26개 핵심절)은
  clause-lookup 이 문서순서상 첫 딥링크 가능 자손으로 점프 타깃 해소(검색은 그대로 찾되
  클릭은 그 절 내용으로). 26개 전부 해소 검증.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:41:00 +09:00
hyungi 3e2fa16e1d feat(hier): 크로스-doc 절 라벨 조회 엔드포인트 (U-1, 'UG-79 보여줘' 진입점)
GET /api/documents/clause-lookup?label=UG-79 → 절 식별자로 크로스-doc 위치 해소.
절(node_type=clause/clause_split)은 in_corpus=false(검색 비활성)라 의미검색으론
못 찾으므로, 라벨 prefix 정확매칭으로 (doc, char_start)를 직접 반환해 읽기뷰 점프 가능케 함.

라벨 중복도 실측: 1335 라벨 중 다중-doc 10건(0.7%, 부록 A-/E-/F- 한정) → 에디션 UI
불요, 단순 조회 + 드문 다중반환. /{doc_id} 앞 선언(라우트 매칭 순서). document_chunks
직접 조회는 정확지목(retrieval 아님)이라 코퍼스 격리의 의도적 예외(/sections 와 동일).

A-1 후속: 절 타이핑(5180=550·5210=862·5209=43 라이브)으로 채워진 절을 사용자가 호출 가능케 함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:16:26 +09:00
hyungi 2eda8d3bdd feat(presegment): G2 인제스트 재활성 — 후보 A e2e 검증 PASS
합성 번들 e2e PASS(자식 3개 합성 file_path·range, uq 위반 0 + 자식 extract range-clamp 1110자
range_ok) 후 인제스트 presegment 재활성(documents.py upload + file_watcher 3곳). 非PDF/단일=통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:22:01 +09:00
hyungi 860c5c6b0c fix(presegment): G2 인제스트 비활성 — Option A vs uq_documents_file_path 충돌
★실번들 검증서 발견: 자식 Document(부모 file_path 공유, Option A)가 uq_documents_file_path
UNIQUE 제약 위반 → 자식 INSERT 실패. 검증된 G1 파이프라인 보호 위해 인제스트를 직접 extract 로
원복(documents.py/file_watcher 4곳). 스키마(362~364)+presegment_worker 코드는 보존(재설계 후 재활성).
재설계 후보: 자식 file_path=unique 합성값+부모 lineage 에서 실파일 해석 / file_path NULL+bundle_source_path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:07:38 +09:00
hyungi c3d5c33813 feat(presegment): G2 PR-2 — presegment 워커 + 큐 배선 + range-clamp (deterministic ToC)
extract 前 presegment 스테이지: 전 문서 진입, 非PDF/단일은 무변 통과, '명확한 번들' PDF만
ToC(level-1) deterministic 분할. LLM 폴백은 PR-3.
- presegment_worker: 보수적 게이트(pages>=60·자식>=5p·연속/단조/전범위·2<=N<=50) + 멱등
  (lineage segmented_from 존재 시 수렴) + 자식=부모파일 공유(Option A)+range
- queue_consumer: BATCH_SIZE/MAIN_QUEUE_STAGES/_load_workers + presegment->extract 전이,
  parent(번들원본)는 억제(자식이 직접 extract enqueue)
- ingest(documents.py upload·file_watcher): 첫 stage extract->presegment
- extract_worker/marker_worker: bundle_page_start/end 시 해당 범위만 추출/변환
  (NULL=일반문서 byte-identical 무회귀 — 검수 확인)

코드 검수 완료(무회귀·full_path 스코프·NOT NULL 커버·py_compile). **미배포** —
실제 번들 PDF 처리 검증 후 배포(PR-3 LLM 폴백과 함께).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:55:27 +09:00
hyungi 3a22d225a0 feat(documents): delete_file=true 큐드-감사삭제 — purge 마커 + retention sweep (R7)
delete_file 파라미터가 광고만 하고 본문에서 0회 참조(soft-delete만, 파일 영구 잔존 +
프론트가 실제 호출)되던 거짓 계약 구현. (c) 큐드삭제:
- 마이그 359: documents.purge_requested_at 컬럼(ADD COLUMN IF NOT EXISTS, replayable).
- delete_document: delete_file=true 시 purge_requested_at 마커 set(deleted_at 과 별도).
- document_purge_sweep cron(03:20 KST): purge_requested_at + grace(30일) 경과 + 파일 존재
  시 NAS 원본 unlink + AUDIT 로그. ★sweep 는 deleted_at 아니라 purge_requested_at 기준 —
  일반 숨김(delete_file=false)은 파일 보존(undelete 가능), 명시 purge 만 물리삭제(데이터 안전).
- DELETE 요청 경로엔 동기 비가역 op 0. 파일 존재 체크로 멱등. unlink 는 to_thread(R5 일관).

검증: py_compile 통과. migration txn 제어문 없음.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:48:25 +09:00
hyungi 8a625bfb27 fix(security): soft-delete 가드 구조화 — get_live_document 헬퍼 + paper-holder (R7)
조회/수정 경로는 deleted_at 을 일관 가드하나 파일/콘텐츠 서빙 5엔드포인트
(get_document_file·image_raw·save_content·preview·content)가 'if not doc' 만 검사 →
삭제 문서 원본/preview/전문/마커이미지가 doc_id(+토큰)만으로 노출·삭제 문서 NAS 재기록.
get_live_document(session, doc_id) 헬퍼(없거나 deleted_at 이면 404)로 통일 — '경로마다
deleted_at 기억' 대신 구조 강제(추가될 서빙 경로 자동 보호). save_content 는 삭제 문서
쓰기 차단까지. find_paper_holder 도 deleted_at IS NULL 필터 추가(dedup.find_canonical 대칭).

검증: py_compile 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:45:33 +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 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 79deae0644 feat(safety): C-1 검색 명시 필터 — material_type/jurisdiction/year 3-leg 동등 + documents exclude 해제
plan safety-library-1 C-1 (검색 핵심 경로 — byte 불변 invariant):
- AxisFilter + _axis_sql 헬퍼: 미지정 시 모든 SQL 절 빈 문자열(run_eval 회귀 0 보장)
- 3 leg 동등 적용: search_text(JOIN 후 WHERE) / _search_vector_docs(prod+cand) /
  _search_vector_chunks(★inner topk JOIN — R6 결정: outer post-filter면 ANN top-k 후
  좁은 필터 후보 붕괴. 미지정 시 JOIN 없음=byte 불변)
- SearchResult + material_type/jurisdiction/published_date (3 leg SELECT additive)
- year = COALESCE(published_date, created_at) (freshness 동일 사상)
- GET /documents/: material_type 지정 시 기본 exclude(news·law_monitor·note) 해제
- _axis_sql 단위 테스트 PASS (미지정=빈문자열+param0 / active 4절 / alias 분기)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 12:59:18 +09:00
hyungi 3feddd012b feat(safety): A-2 수집기 ingest 시점 분류 축 부여 — 레지스트리 전파 + 승인 가드 (mig 352~355)
plan safety-library-1 A-2 (classify-skip 경로 전수 커버):
- news_sources 에 material_type/license_scheme/license_redistribute + 안전·공학 12행 시드
- news_collector: 레지스트리 → documents 전파 (_material_axis — paper 는 jurisdiction NULL 강제)
- kosha(사례·첨부=incident, GUIDE=guide)/csb(incident·US)/api_std(standard·US)/law_monitor(law·KR)
  /file_watcher(KGS=law·KR 타깃 매핑) deterministic 부여 + extract_meta.license 주입
- published_date: 소스별 가용 날짜 (GUIDE 공표일·CSB lastmod·API 공지일·법령 공포일·뉴스 발행일)
- classify_worker: document_type→material_type 결정적 매핑 제안 (자동 전이 금지)
- accept-suggestion: material 제안 적용 + law=jurisdiction 필수(기본값 없음) + 청크 미러 1문 동기화
- chunk_worker: 비뉴스 문서 country=jurisdiction 미러 (R3-m3: 검색측 country 소비자 0 실측)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 06:23:22 +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 daf6a0ade9 feat(documents): S1 dedup·office-md·storage scaffold (B/C/D/E)
plan ds-s1-backend-1 잔여 구현 (A·C-1 은 16b0fe1):
- B 중복검사: services/dedup.py (OFF-list law_monitor 공용) + 업로드 채움(B-1)
  + GET /documents/duplicates(B-2) + post-upload near-dup 비동기(B-3)
  + backfill_dedup.py(B-4) + 야간 dedup_reconcile 잡(03:30 KST 멱등 재계산)
- C MD-first: marker_worker office/hwp 분기 _process_office(C-2) + md_status
  상태머신 postcondition success|failed(C-5) + backfill_nonpdf_markdown.py(C-4)
  + requirements markitdown
- D 스토리지: services/storage ABC+Range 계약 / LocalBackend / NasApiBackend 503
  (D-1) + /file resolver 경유, 로컬 동작 불변(D-2)
- E 운영: pre-change pg_dump + rollback_287.sql + apply runbook(E-3) + 테스트(E-1)

비파괴 불변식 유지(기존 응답 shape 무변경, md_status success→completed read-time 매핑).
어드버서리얼 리뷰 확정 1건(soft-delete canonical 승격 시 stale duplicate_of) → B-1
승격 정규화 + 야간 재계산으로 정합.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 03:05:30 +00:00
hyungi 68e2d7ea04 feat(documents): S1-ADD dedup·원본명 3컬럼 + md_status success→completed 매핑 (A) + office→md PoC (C-1)
plan ds-s1-backend-1 (r5 수렴). 코드만 스테이징 — migration 미적용(restart 보류, E-2 Soft Lock 예외창).

A (앱 v1 디코딩 비파괴 최소선):
- A-1 migrations/287_documents_dedup_fields.sql: original_filename TEXT / duplicate_of BIGINT FK ON DELETE SET NULL
  / duplicate_count INTEGER NOT NULL DEFAULT 0. 단일 statement·PG16 fast-path·BEGIN/COMMIT 금지. backfill 미포함(B-4).
- A-2 app/models/document.py: 1계층 블록에 3 mapped_column (+ ForeignKey import). md_* 는 기존.
- A-3 app/api/documents.py: DocumentResponse 3필드(duplicate_count=0 non-opt) + DocumentDetailResponse
  field_validator(success→completed, mode=before) — read-time DB→API 단방향, write(ORM) 미적용.
- A-4 tests/test_s1_dedup_shape.py: success→completed 동작 + 비-success 통과 + 3필드 디폴트/roundtrip
  + ds-app contract fixture 디코드(skip-if-absent). py_compile OK. ★ backend 절반 — 전체 비파괴는 S3 render 테스트와 AND.

C-1 PoC (워커 미연결 — C-2 에서 marker_worker 분기 연결):
- app/workers/office_md.py: OOXML=markitdown(신규 dep, lazy) / hwp·hwpx=LibreOffice headless→HTML→markdownify(기존 dep).
  실패·빈출력·타임아웃·dep부재 → OfficeMdError raise (success+빈md 금지 = C-5 postcondition 의 변환기 계약).
- scripts/poc_office_md.py: 표 fidelity 측정 하니스. E-1 = prod LibreOffice 버전핀 안전컨텍스트 실행(hwpx 필터 버전 의존).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 03:05:30 +00:00
hyungi 5a19cde38c fix(documents): 도메인 트리 카운트를 문서함 list 제외와 일치
트리(/documents/tree)는 deleted 만 제외하고 뉴스/법령/메모를 다 세는데, 문서함 list 는
source_channel news/law_monitor + file_type note 를 기본 제외 → '트리는 N건인데 클릭하면
0건' 불일치(예: Philosophy/Aesthetics 5건 전부 news+note 라 클릭 시 0). 트리 쿼리에 동일
제외 적용해 카운트=실제 표시 일치. 영향: Philosophy 12→2, General 189→84 등 정상화.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:57:47 +09: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 Ahn a08b620894 refactor(search): swap 10 call sites to acquire_mlx_gate(Priority.*) (B-1)
DS-Mac-mini-26B-Priority-Gate-1 — 사용자-facing 7 + worker 3 = 10 site 의
`async with get_mlx_gate():` → `async with acquire_mlx_gate(Priority.*):` 교체.

Foreground 6 (user-facing path):
- app/services/search/evidence_service.py:315 (/ask evidence stage)
- app/services/search/classifier_service.py:103 (/ask classifier stage)
- app/services/search/synthesis_service.py:299 (/ask synthesis stage)
- app/api/documents.py:1306 (수동 analyze API)
- app/api/study_topics.py:1183 (subject note 동기 생성)
- app/api/study_questions.py:1560 (study explanation 동기 API)

Background 4 (worker queue / fire-and-forget):
- app/services/search/query_analyzer.py:240 (V0 grep 확인: fire-and-forget only,
  search_pipeline.py:179 trigger_background_analysis 만, docstring rule
  "analyze() 동기 호출 금지" 부합 → BACKGROUND 확정)
- app/workers/deep_summary_worker.py:110 (classify-escalate worker)
- app/workers/study_explanation_worker.py:149
- app/workers/study_session_analysis_worker.py:237

Cleanup:
- query_analyzer._get_llm_semaphore() 제거 — self-only, unused, signature 거짓말
  (이제 get_mlx_gate 가 Semaphore 아닌 context manager 반환)

기존 get_mlx_gate() legacy wrapper 는 보존 (BACKGROUND 매핑). user-facing path
잔재 0 — closure gate grep 검증 통과 (별 commit 에서).
2026-05-17 08:51:57 +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 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 6e3ce91de6 fix(library): library-neighbors JSONB cast 오류 — EXISTS 서브쿼리로 교체 2026-04-27 12:43:26 +09:00
Hyungi Ahn e92bf3c06b feat(library): 모바일 학습 detail 최적화 + 다음 자료 네비 (PR-E)
[Backend]
- /api/documents/{id}/library-neighbors — 같은 library_path 내
  prev/next 자료 (title_asc 정렬). user_tags 의 첫 @library/* 태그를
  path 로 사용. category='library' 만 응답.

[Frontend]
- routes/documents/[id]/+page.svelte:
  · 마크다운 본문: 모바일 prose-base (가독성), lg+ prose-sm 유지
    + leading-relaxed
  · onMount 시 자료실 자료면 loadNeighbors 자동 호출
  · 모바일 sticky 하단 바 (lg:hidden):
    [< 이전] [✓ 1회독 완료 + 다음 (primary)] [다음 →]
    - 가운데 버튼: POST /read 후 next 자료로 goto. 마지막 자료면
      "1회독 완료 (마지막 자료)" 텍스트 + next 버튼 disabled.
    - 좌/우 버튼: 회독 카운트 안 함, 단순 이동 (이전 자료 / 회독 안 한 다음)
  · 본문 하단 패딩 (lg:hidden h-20) — sticky 바에 가리지 않음
2026-04-27 12:41:43 +09:00
Hyungi Ahn fb4897e256 feat(library): 자료실 회독 추적 frontend (PR-B)
PR-A backend 위에 사용자 UI:

[ReadCounter]
- frontend/src/lib/components/ReadCounter.svelte 신규
- "1회독 완료" 명시 클릭 → POST /read → 토스트
- 현재 N회독 / 마지막 읽음 (방금/N분 전/날짜) 표시
- ↩ 버튼 → DELETE /read/last → 마지막 1건 취소 (confirm)
- 자동 +1 

[자료 detail]
- routes/documents/[id]/+page.svelte 우측 editor stack 상단에
  ReadCounter 마운트 — category='library' 일 때만
- doc 응답의 read_count / last_read_at 으로 초기값 (추가 fetch 불필요)

[자료실 카드 회독 배지]
- DocumentCard.svelte 우측 메타에 텍스트 배지
  안 봄 / 1회독 / 2회독 / N회독 — 색은 매우 약하게 (오해 방지)
- doc.category === 'library' 만

[안 본 자료만 필터]
- backend: /api/documents/library 에 unread bool 파라미터
  Document.id NOT IN (현재 사용자 회독 doc_id) — scalar_subquery
- frontend: /library 페이지에 토글 버튼 (정렬 옆)
  URL ?unread=true 동기화, activeUnread reactive
2026-04-27 12:19:11 +09:00
Hyungi Ahn 49d8f68986 feat(library): 자료실 회독 카운트 추적 (PR-A backend)
자료실 자료를 사용자가 명시적으로 "1회독 완료" 클릭 시 +1 누적.
detail 진입 자동 카운트 . append-only 로그.

데이터:
- migrations 174~176: document_reads 테이블 + 인덱스 2개 (단일 statement 분할)

ORM:
- app/models/document_read.py: DocumentRead (user_id, document_id, read_at)

API (app/api/document_reads.py, /api/documents prefix):
- POST   /api/documents/{id}/read       — 회독 +1
- GET    /api/documents/{id}/read-stats — {read_count, last_read_at}
- DELETE /api/documents/{id}/read/last  — 현재 사용자의 그 문서 마지막 1건만
  · ownership: WHERE user_id=current_user.id AND document_id=:doc_id
  · documents 에 user_id 부재 (single-user). multi-user 전환 시 ownership
    check 추가 필요 — 코드 주석 명시.

응답 확장:
- DocumentResponse: read_count(default 0), last_read_at(default None)
- /api/documents/library: 페이지 N건 한정 LEFT JOIN 으로 read 통계 매핑 (N+1 회피)
- /api/library/tree CategoryTreeNode: unread_count 추가
  · 기존 path_docs 가 ancestor 누적 구조라 그대로 활용 — 하위 경로 합산 자동

규칙 (사용자 명시 — 변경 금지):
  · 같은 날 여러 번 클릭 → 각각 별개 회독
  · 실수 클릭 취소 = DELETE /read/last
  · documents 에 read_count 컬럼 추가 , 로그 기반 COUNT(*) 만

plan: ~/.claude/plans/scalable-chasing-stonebraker.md
브랜치: feature/library-reads (손글씨 트랙과 분리)
2026-04-27 12:08:36 +09:00
Hyungi Ahn 6fdc48e5b6 feat(ai): B-1 summary tier 분할 — triage(4B) + deep_summary(26B)
PR-A policy 레이어를 재사용하여 classify_worker 에 tier triage 경로를 추가.
Legacy ai_summary / ai_domain / ai_suggestion 은 유지 (회귀 0), tldr/bullets/
detail/inconsistencies 는 별도 필드로 분리.

Migrations (156~160):
- 156 documents: ai_tldr, ai_bullets, ai_detail_summary, ai_inconsistencies,
  ai_analysis_tier 5컬럼
- 157 process_stage 에 'deep_summary' ADD VALUE 단독 (Postgres 동일 트랜잭션
  제약 회피)
- 158 processing_queue.payload JSONB (envelope 전달)
- 159 analyze_events 에 tier + suppressed_reason
- 160 suppressed_reason partial index

Models/ORM:
- Document: 5컬럼 Mapped 추가
- ProcessingQueue: deep_summary enum 확장 + payload 필드, enqueue_stage 에
  payload 옵션
- AnalyzeEvent: PR-A shadow 6컬럼 + PR-B tier/suppressed_reason

Workers:
- classify_worker: 기존 legacy 경로 뒤에 _run_tier_triage 추가.
  - _match_subject_domain(doc, text): source_channel + 본문 keywords + ai_domain
    prefix 로 PR-A policy 의 subject_domain 이름 결정 (category 매칭 금지).
  - R1 TriageOutput pydantic + JSON 깨짐 fallback (triage_json_invalid).
  - R2 _check_backlog_guard(): 30분 window ratio > threshold OR pending 초과면
    soft escalate suppress. hard escalate 는 통과.
  - R3 _slice_text_ranges(): 260k 초과 시 head 120k + mid 20k + tail 120k 3조각.
  - escalate 시 EscalationEnvelope 구성 + {envelope, subject_domain} payload 로
    deep_summary enqueue.
- deep_summary_worker (신규): queue payload 에서 envelope + subject_domain 읽기 →
  render_26b("p3c_deep_summary", subject_domain) + MLX 호출 (llm_gate Semaphore(1)
  경유) → ai_detail_summary + ai_inconsistencies 저장 + ai_analysis_tier='deep'.
  _filter_inconsistencies 로 허용 kind (version_drift / procedure_conflict /
  source_conflict / missing_basis) 만 통과 — 구매/계약 kind drop.
- queue_consumer: workers dict 에 deep_summary 추가 + BATCH_SIZE=1. next_stages
  는 건드리지 않음 — classify → embed/chunk 는 그대로, deep_summary 는 독립 체인.

Telemetry:
- record_analyze_event: subject_domain / risk_flags / escalation_reasons /
  confidence / policy_version / shadow_would_route_to / tier / escalated_to_26b /
  suppressed_reason 파라미터 확장. classify/deep worker 가 mode="summary_triage"
  또는 "summary_deep" 로 기록.

API:
- DocumentResponse 에 ai_tldr / ai_bullets / ai_detail_summary /
  ai_inconsistencies / ai_analysis_tier 5필드 노출.

Prompts:
- classify.txt 에 DEPRECATED 주석만 추가 (파일 유지 — rollback 경로 보존).
- PR-A 의 app/prompts/policy/p3a_short_summary.txt (4B) 와 p3c_deep_summary.txt
  (26B) 를 그대로 사용. 내 소유의 summary_triage.txt / summary_deep.txt 는 중복
  이라 별도 커밋에서 제거하지 않고 바로 생성 전 삭제.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:22:40 +09:00
Hyungi Ahn e88640d3d8 feat(category): law 카테고리 분리 — enum + backfill + classify skip
- migrations/152: ALTER TYPE doc_category ADD VALUE 'law' (DDL only; PG16 단일-트랜잭션 제약상 backfill 은 별도)
- models/document.py: Enum 에 'law' 추가 (7 활성 + 3 유보)
- workers/law_monitor.py: Document(..., category='law') — 신규 유입부터 세팅
- workers/classify_worker.py: source_channel='law_monitor' early-return + 최소 필드 (ai_domain='법령', ai_tags=['법령'], importance='medium'). AI classify skip — 법령 구조 고정/외부 source of truth/자동 재수집
- scripts/backfill_category.py: law 분기 + WHERE re-target ((source_channel='law_monitor' AND category='document')) + VERIFY cat_law/law_source_count + fail 조건
- api/documents.py: default 목록 제외에 law_monitor 추가 (news 와 동일 패턴)
- api/dashboard.py: documents count FILTER 에 law_monitor 제외 (category_counts.law 는 기존 GROUP BY category 로 자동 노출)
- frontend/Sidebar.svelte: '법령 알림' 버튼 ?source=law_monitor → ?category=law (explicit category 경로가 default exclusion 을 skip)

plan: ~/.claude/plans/stateless-churning-raccoon.md
axis 원칙: category=UI 축, policy/telemetry=source_channel+ai_domain 축 (feedback_category_vs_ai_domain_axis.md)

배포 순서: push → GPU pull → compose up --build fastapi frontend → backfill --dry-run → --apply.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 09:14:56 +09:00
Hyungi Ahn a8e24ab114 fix(documents): accept-suggestion 항상 409 버그 + compose 127.0.0.1 바인딩
- accept-suggestion: documents.updated_at != expected stale 검사 제거.
  classify_worker 가 source_updated_at 을 pre-commit 값으로 저장하는데
  SQLAlchemy onupdate 가 commit 에서 updated_at 을 bump → 항상 불일치 →
  승인 영구 불가. payload 교체 검사 하나만으로 core race 는 막힘.
  사용자 직접 편집 감지는 별도 user_updated_at 컬럼 도입 시 재논의.
- docker-compose.yml: postgres/kordoc/fastapi/frontend 포트 127.0.0.1
  바인딩. GPU 서버 로컬에만 있던 drift 를 main 으로 승격. UFW-Docker
  우회 컨텍스트에서 불필요한 LAN 노출 축소.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 07:20:38 +09:00
Hyungi Ahn 8f25d396df feat(upload): §4-독립 — error_code 체계 + .uploading orphan cleanup + 진행률/abort UX
plan: ~/.claude/plans/luminous-sprouting-hamster.md §4 (1GB/stt/dashboard 외 독립 항목)

backend:
- _upload_error(status, code, msg) 헬퍼 정의 (§3 가 호출만 추가했던 누락 수정).
  detail = {error_code, message} — 프론트가 error_code 로 분기.
- upload_document 의 모든 HTTPException 을 _upload_error 로 전환:
  body_too_large / invalid_input / empty_file / unsupported_codec / internal
- ClientDisconnect → 499 network_abort + 임시파일 정리.
  asyncio.TimeoutError → 408 upload_timeout.
- 쓰기 중 .uploading 임시명 → 완료 후 staging.replace(target) atomic rename.
  → 프로세스 크래시 잔존물은 cleanup_orphan_uploads 가 수거.
- file_watcher SKIP_EXTENSIONS 에 .uploading 추가 (오해 픽업 방지).

cleanup scheduler:
- workers/upload_cleanup.py 신규. 10분 주기로 Inbox 하위 *.uploading 중
  mtime > orphan_max_age_sec(3600) 인 파일 삭제.
- 최근 3회 (≈30분) 누적 삭제 수가 cleanup_warn_threshold(10) 이상이면
  WARNING 로그. in-memory deque (재시작 시 리셋) — 집요한 이슈만 잡는 목적.
- core/config.py UploadConfig 에 두 임계치 필드 (defaults — config.yaml override 무관).

frontend:
- api.ts: ApiError 에 optional errorCode/errorMessage 필드 (detail string 유지로
  기존 5+ 소비자 호환). parseDetail() 가 {error_code, message} 객체 응답을 풀어
  정규화. uploadFile(path, formData, {signal, onProgress}) XHR 헬퍼 신규
  (fetch() 가 upload progress 미지원이라 XHR). 401 refresh 1회 정책 동일.
- UploadDropzone.svelte 재작성: 진행률 바, 파일별/전체 abort 버튼, 페이지 이탈
  beforeunload 경고, errorCode 별 토스트 메시지 분기 (7 코드 — body_too_large /
  upload_timeout / network_abort / empty_file / invalid_input / unsupported_codec /
  internal). 컴포넌트 unmount 시 진행 중 업로드 abort.

보류:
- max_bytes 1GB 상향 + Caddyfile 1100MB (별도 결정으로 100MB 유지)
- /dashboard 카테고리 카드 (별도 plan)
- docs/categories.md (§1-3 정의 안착 후)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 06:57:02 +09:00
Hyungi Ahn 1e2c004dd4 feat(media): §3 audio STT + video 재생 인프라
plan: ~/.claude/plans/luminous-sprouting-hamster.md §3

스키마:
- migrations/147_audio_segments_table.sql: audio_segments (STT 타임스탬프
  세그먼트)
- migrations/148_audio_segments_idx.sql: (document_id, start_s) idx
- migrations/149_document_media_cols.sql: documents.thumbnail_path +
  needs_conversion
- migrations/150_queue_stage_stt.sql: process_stage += 'stt'
- migrations/151_queue_stage_thumbnail.sql: process_stage += 'thumbnail'
- app/models/audio_segment.py, document.py (thumbnail_path/needs_conversion)

서비스:
- services/stt/{Dockerfile, requirements.txt, server.py} — faster-whisper
  large-v3 GPU 컨테이너. /transcribe (filePath/langs/beamSize) +
  /health + /ready (cuda device_count + model_loaded). NFC/NFD 경로
  resolver (OCR 교훈).
- docker-compose.yml: stt-service 추가 (GPU 1 예약, :3300, NAS ro mount,
  stt_models volume, start_period 300s), fastapi env 에 STT_ENDPOINT.

파이프라인 (의존 §1 category):
- app/workers/stt_worker.py 신규: stage='stt' pickup → STT_ENDPOINT 호출 →
  extracted_text + audio_segments 저장. Timeout 30분.
- app/workers/thumbnail_worker.py 신규: ffmpeg 50% 지점 1장 →
  PKM/Videos/.thumbs/{id}.jpg + thumbnail_path 세팅.
  needs_conversion=true 는 skip.
- app/workers/file_watcher.py 확장: PKM/{Inbox, Recordings, Videos}
  스캔. 확장자→category, audio→stage=stt, video .mp4/.webm→
  stage=thumbnail, video .mov/.mkv/.avi→needs_conversion=true + stage
  없음. settings.roon_library_path prefix skip.
- app/workers/queue_consumer.py 확장: stt + thumbnail workers 등록,
  BATCH_SIZE(stt=1, thumbnail=3), next_stages 에 stt→[classify] 추가
  (audio 는 extract 건너뜀).
- app/Dockerfile: ffmpeg 추가 (썸네일 subprocess 용).

API (의존 §1):
- /api/audio/{id}/segments — AudioSegment ORDER BY start_s
- /api/video/{id}/thumbnail — thumbnail_path FileResponse (쿼리 토큰)
- /api/documents/{id}/file: media_types 에 audio/video mime 포함 (§2
  커밋에 이미 포함). Starlette FileResponse 가 Range 자동.
- upload_document: .mov/.mkv/.avi 웹 업로드 거부 (error_code
  unsupported_codec). NAS 드롭은 file_watcher 가 quarantine 수용.

프론트:
- AudioPlayer.svelte: HTML5 audio + 전사 세그먼트 sticky 패널 + 줄
  클릭 seek. activeIdx 하이라이트.
- VideoPlayer.svelte: HTML5 video direct play + needs_conversion 안내
  카드. poster 는 thumbnail endpoint.
- /audio (목록 grid) + /audio/[id] (플레이어)
- /video (썸네일 grid + 변환 필요 배지) + /video/[id] (플레이어)
- Sidebar.svelte: Mic/Film 아이콘 + audio/video 네비 활성, count
  배지 (§2 /stats/category-counts 재사용).

설정:
- app/core/config.py: stt_endpoint + roon_library_path.

DoD 배포 후 smoke: /ready cuda:true, 회의 mp3 transcribe, audio
extract 없이 classify 진행(queue 회귀), /audio 재생, .mp4 재생,
.mov 웹 400, .mov NAS quarantine, Sidebar 네비 + count.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 06:47:36 +09:00
Hyungi Ahn a93d1689d8 feat(documents): §2 카테고리 전용 페이지 + 승인 UI
plan: ~/.claude/plans/luminous-sprouting-hamster.md §2

- GET /api/documents/stats/category-counts — Sidebar/Dashboard 용
  카테고리별 문서 건수 + library_pending_suggestions
- DocumentResponse 에 category / ai_suggestion 필드 노출 (§1 과 동일
  수정, rebase 시 합쳐짐)
- SuggestionReview.svelte 신규 — ai_suggestion.proposed_category='library'
  제안 카드 리스트. 단건 승인/반려 + 체크박스 대량 승인. 409 stale 시
  warning toast + 자동 refetch
- /library 상단에 SuggestionReview 배치 (자료실 + 승인 대기함 겸).
  승인/반려 후 tree/docs/facet 재조회
- Sidebar 재구성: 카테고리 내비(문서/자료실/뉴스/메모/검색) + 자료실
  pending 배지. /api/documents/stats/category-counts 바인딩. audio/video
  자리는 §3 주석 예약

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:36:22 +09:00
Hyungi Ahn 8fdea88676 feat(documents): §1 category enum + ai_suggestion 승인 파이프
plan: ~/.claude/plans/luminous-sprouting-hamster.md §1

- migrations/143_category.sql: doc_category enum (6 활성 + 3 유보) +
  documents.category + documents.ai_suggestion JSONB + 2 idx.
- app/models/document.py: category (Enum, create_type=False), ai_suggestion (JSONB).
- app/prompts/classify.txt: document_type enum 에 7 실무 doctype 추가
  (발주서/세금계산서/명세표/도면/증명서/계획서/시방서) + facet_doctype
  필드 directive.
- config.yaml: document_types 에 7 항목 추가 (worker 검증 통과).
- app/workers/classify_worker.py: FACET_DOCTYPES / LIBRARY_SUGGESTION_DOCTYPES
  상수, facet_doctype 파싱(기존값 미덮어씀), 발주서/세금계산서/명세표
  감지 시 ai_suggestion={proposed_category=library, proposed_path=@library/
  거래/{YYYY}/{doctype}, source_updated_at=doc.updated_at.isoformat(), ...}.
  category / user_tags 자동 전이 금지 (suggestion-only).
- app/api/documents.py:
  · DocumentResponse 에 category / ai_suggestion 노출
  · GET /documents ?category=<cat> / ?has_suggestion / ?proposed_category
    (category 지정 시 기본 news/memo 제외 해제 — §2 승인 UI 계약)
  · GET /documents/library 를 Document.category=='library' 기반으로 재구현
    (path subquery 는 user_tags 유지 — 분류 내부 서가 경로)
  · POST /documents/{id}/accept-suggestion — FOR UPDATE + idempotent no-op +
    dual 409 stale (payload source_updated_at / documents.updated_at) +
    user_tags idempotent append
  · DELETE /documents/{id}/suggestion — idempotent, stale 검사 없음
- scripts/backfill_category.py: dry-run / apply. 매핑(news/memo/@library/else)
  + 3-way 상대 검증 (all_rows==categorized, uncategorized==0,
  cat_library==has_library_tag — 자동 전이 금지 정책 검증).

남은 DoD (원격 배포 후): docker compose up → migration 143 적용 → backfill
apply → smoke (drive_sync 발주서 업로드 suggestion 생성 / category 유지,
accept-suggestion idempotency + 409 stale 두 벡터, /documents?category=library
== /documents/library 건수 일치).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:32:01 +09:00
Hyungi Ahn 7d2e678ea1 feat(upload): 스트리밍 size 검증 + 0바이트 reject + 고아 레코드 방지
기존 `await file.read()` 는 임의 크기 파일을 메모리에 전부 적재한 후 저장해
디스크 고갈 / OOM 공격 벡터 였음. Caddy/home-caddy 프록시 한도에만 의존했고
FastAPI 측 policy enforcement 가 전무했음. 이 커밋으로 서버가 authoritative
으로 강제 집행.

변경:
- `Request` DI 추가 → Content-Length 사전 차단 (max_bytes * slack_ratio 초과 시 413)
- `await file.read()` → 청크 루프 스트리밍 (stream_chunk_bytes 단위)
- 누적 size > max_bytes 시 스트리밍 중 413 (Content-Length 위조 방어)
- 0바이트 파일 → 400 reject (정책: 유의미한 문서 ingest 대상 아님)
- 파일 저장 완료 + close 이후 에만 file_hash 및 DB 레코드 생성
- Document 레코드 와 processing_queue 는 단일 트랜잭션으로 묶고,
  DB 예외 시 session rollback + partial file unlink 로 원자적 정리
- 예외 시 `except Exception` 으로 cleanup (BaseException 계열은 의도적으로 패스)

설정 값: config.yaml `upload.{max_bytes, content_length_slack_ratio, stream_chunk_bytes}`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 08:03:43 +09:00
Hyungi Ahn 8a8096a444 feat(api): Phase E.2 — analyze_events 테이블 + 로깅
POST /documents/{id}/analyze 호출을 DB에 기록. failure mode 분류 + source 식별.

- migrations/137: analyze_events 테이블 (doc_id FK, mode, truncated, layers_returned JSONB, cached, latency_ms, error_code, source TEXT NOT NULL DEFAULT 'document_server', prompt_version)
- ORM: models/analyze_event.py 신규
- services/document_telemetry.py: record_analyze_event() + sanitize_source() 서버 fallback 강제 (enum 외 → unknown, None → document_server)
- app/api/documents.py:
  · X-Source 헤더 + BackgroundTasks 의존성 추가
  · try/finally 패턴으로 성공/cache/에러 모든 exit에서 background insert
  · error_code: None(성공) | not_found | no_text | timeout | llm | parse | missing_summary

Phase F에서 nanoclaude가 X-Source: synology_chat 헤더로 호출하면 source 구분 가능.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:58:58 +09:00
Hyungi Ahn ee090089e9 fix(api): D.5 analyze timeout 20→60초, text_limit 15000→12000
doc 5271(29,837자) 등 큰 문서에서 20초 timeout 빈발.
- ANALYZE_TIMEOUT_S: 20 → 60 (safety margin 포함)
- ANALYZE_TEXT_LIMIT: 15000 → 12000 (Gemma 입력 부담 완화)
- 프론트 안내 '10초' → '10~40초 소요'

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:39:01 +09:00
Hyungi Ahn d9caf075e5 feat(api): Phase D.5 — POST /documents/{id}/analyze 문서 분석 엔드포인트
전문 15,000자 → Gemma 4 구조화 분석 (근거/해설/사례/요약 4층).
- MLX gate + 20초 timeout (gate 안쪽)
- 인메모리 캐시 TTL 30분, 키 = doc_id + updated_at(fallback: created_at)
- 층별 최소 50자 + 억지 채움 문구 제거
- summary 필수 (없으면 422)
- 에러: 404 text 없음 / 504 timeout / 502 llm / 422 parse

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:32:44 +09:00
Hyungi Ahn 4f63938a04 feat(api): GET /documents/{id}/content 전문 텍스트 엔드포인트
Tier 2 문서 전문 분석을 위한 서비스 호출용. 15,000자 상한 + truncated 표시.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:35:19 +09:00
Hyungi Ahn ef9687b0bf feat(library): Phase 2B 문서 상세 facet 편집 + 업로드 facet 전달
FileInfoView에 회사/주제/연도/문서유형 select 4개 추가.
facet 옵션은 /api/library/facets에서 로드, 세션 캐시.
업로드 엔드포인트에 facet Form 파라미터 4개 추가.
업로드 시 현재 선택 facet 자동 전달 + 미리보기 텍스트.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:27:03 +09:00
Hyungi Ahn ba19c6fb79 feat(library): Phase 2A facet 탐색 기반 — 컬럼 + API + 필터
documents 테이블에 facet_company/topic/year/doctype 4개 축 추가.
facet_values 사전 테이블 + CRUD API.
facet-counts 집계 API (교차 필터링 지원).
문서 목록 API에 facet 필터 파라미터 추가.
DocumentResponse/DocumentUpdate 스키마에 facet 필드 포함.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:09:25 +09:00
Hyungi Ahn 751cdc5be8 fix(queue): enqueue 경로 중복 방어 — partial unique index + 중앙 enqueue_stage 함수
기존 UNIQUE(document_id, stage, status)는 pending+processing 동시 존재를
허용해서 stale 복구 시 충돌 발생. 2-layer 방어로 근본 차단:

1) DB: partial unique index uq_queue_active — 활성 행(pending/processing)은
   (document_id, stage)당 최대 1개만 허용
2) App: enqueue_stage() 중앙 함수 — INSERT ON CONFLICT DO NOTHING으로
   모든 9개 경로의 check-then-insert TOCTOU race 제거

migration 117은 guard check 포함 — 활성 중복이 남아있으면 RAISE EXCEPTION
으로 중단, 수동 정리 유도.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:37:32 +09:00
Hyungi Ahn ef89d48bfe fix(library): 자료실 루트 업로드 시 @library/ 태그 누락 수정
폴더 미선택 상태에서 업로드하면 doc_purpose='business'만 설정되고
@library/ 태그가 빠져서 자료실에 문서가 표시되지 않던 버그 수정.
백엔드: business 업로드에 library_path 없으면 @library/미분류 자동 태깅.
프론트: activePath 없을 때 기본값 '미분류' 전송.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:36:40 +09:00
Hyungi Ahn 5c58778a41 feat(library): doc_purpose 필드 + 자료실 업로드 기능
지식/업무 문서 1차 구분을 위한 doc_purpose(business|knowledge) 추가.
- 마이그레이션: document_purpose enum + 컬럼
- AI 분류: docPurpose 자동 추론 (빈 값만 채움)
- 업로드 API: doc_purpose + library_path Form 파라미터
- 자료실 업로드: business 기본값 + 선택 경로 자동 태깅
- FileInfoView: 용도 select (수동 변경, 실패 롤백)
- DocumentCard: 업무/참조 배지

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:26:59 +09:00
Hyungi Ahn deb5c1b704 feat(library): 자료실 — 태그 기반 트리 문서 관리 기능
목적성 문서(양식, 템플릿, 연간보고서)를 @library/ 태그로 분류하고
트리 구조로 탐색하는 자료실 페이지 추가.

백엔드: 경로 정규화 유틸, library-tree/library 엔드포인트,
다운로드 Content-Disposition 개선(원본/PDF 분리, 한글 filename*)
프론트: /library 페이지, LibraryPathEditor, 상단 nav/사이드바 링크

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 14:55:45 +09:00