Commit Graph

173 Commits

Author SHA1 Message Date
Hyungi Ahn
e0f928f429 feat(deploy): Phase 1.3 reranker (TEI bge-reranker-v2-m3) 서비스 추가
docker-compose.yml에 reranker 서비스 추가:
- image: ghcr.io/huggingface/text-embeddings-inference:1.5
- MODEL_ID=BAAI/bge-reranker-v2-m3
- MAX_BATCH_TOKENS=8192, MAX_CONCURRENT_REQUESTS=4
- GPU 1개 할당 (RTX 4070 Ti Super, CUDA 13.0)
- expose 80만 (host 노출 X, internal network 전용)
- reranker_cache volume으로 모델 영속화
- fastapi가 depends_on 안 함 → 단독 시작 가능, reranker 없어도 fastapi 동작
  (rerank_service가 RRF fallback)

다음 단계:
- GPU에서 docker pull로 호환성 확인
- docker compose up -d reranker → warmup
- config.yaml의 rerank.endpoint를 http://reranker:80/rerank로 갱신 (GPU 직접)
- fastapi rebuild + 평가셋 측정 (rerank=true)
2026-04-08 13:16:37 +09:00
Hyungi Ahn
25ef3996ec feat(chunk): Phase 1.2-G embedding 입력 강화 (title + section + text)
Phase 1.2-G hybrid retrieval 측정 결과 Recall 0.66 정체 + 진단:

직접 nl 쿼리 시도 결과 일부 정답 doc(3854, 3981, 3982, 3920, 3921)이
top-100에도 못 들어옴. doc은 corpus + chunks + embedding 모두 정상.

진짜 원인: 자연어 query ↔ 법령 조항 의미 거리 + 짧은 본문 embedding signal 약함.
- query: '유해화학물질을 다루는 회사가 지켜야 할 안전 의무'
- 본문: '화학물질관리법 제4장 유해화학물질 영업자'
- bge-m3 입장: chunk text만으로는 같은 의미인지 못 알아봄

해결: chunks embedding 입력에 doc.title + section_title 포함.
- before: embed(c['text'])
- after:  embed('[제목] {title}\n[섹션] {section}\n[본문] {text}')

기대 효과:
- 짧은 조항 문서 매칭 회복 (3920/3921 등 300자대)
- 자연어 query → 법령 조항 의미 매칭 개선
- Recall 0.66 → 0.72~0.78

영향: chunks embedding 차원/구조 변경 X — 입력 텍스트 prefix만 다름.
재인덱싱 1회로 모든 chunks 재생성 필요.
2026-04-08 13:08:23 +09:00
Hyungi Ahn
2ca67dacea feat(search): Phase 1.2-G hybrid retrieval (doc + chunks)
Phase 1.2-C 평가셋: chunks-only Recall 0.788 → 0.660 catastrophic.
ivfflat probes 1 → 10 → 20 진단 결과 잔여 차이는 chunks vs docs embedding의
본질적 차이 (segment 매칭 vs 전체 본문 평균).

해결: doc + chunks hybrid retrieval (정석).

신규 구조:
- search_vector(): 두 SQL을 asyncio.gather로 병렬 호출
- _search_vector_docs(): documents.embedding cosine top N (recall robust)
- _search_vector_chunks(): document_chunks.embedding window partition
  (doc당 top 2 chunks, ivfflat top inner_k 후 ROW_NUMBER PARTITION)
- _merge_doc_and_chunk_vectors(): 가중치 + dedup
  - chunk score * 1.2 (segment 매칭 더 정확)
  - doc score * 1.0 (recall 보완)
  - doc_id 기준 dedup, chunks 우선

데이터 흐름:
  1. query embedding 1번 (bge-m3)
  2. asyncio.gather([_docs_call(), _chunks_call()])
  3. _merge_doc_and_chunk_vectors → list[SearchResult]
  4. compress_chunks_to_docs (그대로 사용)
  5. fusion (그대로)
  6. (Phase 1.3) chunks_by_doc 회수 → reranker

검증 게이트 (회복 목표):
- Recall@10 ≥ 0.75 (baseline 0.788 - 0.04 이내)
- unique_docs per query ≥ 8
- natural_language_ko Recall ≥ 0.65
- latency p95 < 250ms
2026-04-08 13:02:23 +09:00
Hyungi Ahn
2cfe4b126a merge: origin/main (search Phase 1.2-C) → design-system
- 백엔드 search/chunk 개선 (Phase 1.2-AB → 1.2-C) 통합
- frontend와 충돌 없음 (backend만 변경)
- Phase C/D/F/E 프런트엔드 작업 유지
2026-04-08 12:52:59 +09:00
Hyungi Ahn
4938b25d12 feat(ui): Phase E — PreviewPanel 분할 + detail inline + viewer Tabs
E.1 PreviewPanel 7개 editors/* 분할:
- frontend/src/lib/components/editors/ 신설 (7개 컴포넌트):
  * NoteEditor — 사용자 메모 편집
  * EditUrlEditor — 외부 편집 URL (Synology Drive 등)
  * TagsEditor — 태그 추가/삭제
  * AIClassificationEditor — AI 분류 read-only 표시
    (breadcrumb + document_type + confidence tone Badge + importance)
  * FileInfoView — 파일 메타 dl
  * ProcessingStatusView — 파이프라인 단계 status dl
  * DocumentDangerZone — 삭제 (ConfirmDialog 프리미티브 + id 고유화)
- PreviewPanel.svelte 344줄 → 60줄 얇은 wrapper로 축소
  (header + 7개 editors 조합만)
- DocumentMetaRail (D.1)과 detail 페이지(E.2)가 동일 editors 재사용

E.2 detail 페이지 inline 편집:
- documents/[id]/+page.svelte: 기존 read-only 메타 패널 전면 교체
- 오른쪽 aside = 7개 editors 스택 (Card 프리미티브로 감쌈)
- 왼쪽 affordance row: Synology 편집 / 다운로드 / 링크 복사
- 삭제는 DocumentDangerZone이 담당 (ondelete → goto /documents)
- loading/error 상태도 EmptyState 프리미티브로 교체
- marked/DOMPurify renderer 유지, viewer 분기 그대로

E.3 관련 문서 stub:
- detail 페이지 오른쪽 aside에 "관련 문서" Card
- EmptyState "추후 지원" + TODO(backend) GET /documents/{id}/related

E.4 DocumentViewer Tabs 프리미티브:
- Markdown 편집 모드의 편집/미리보기 토글 → Tabs 프리미티브
- 키보드 nav (←→/Home/End), ARIA tablist/tab/tabpanel 자동 적용

검증:
- npm run build 통과 (editors/* 7개 모두 clean, $state 초기값
  warning은 빈 문자열로 초기화하고 $effect로 doc 동기화해 해결)
- npm run lint:tokens 204 → 168 (detail 페이지 + PreviewPanel 전면
  token 기반 재작성으로 -36)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:51:10 +09:00
Hyungi Ahn
f4f9de4402 fix(search): Phase 1.2-C doc-level aggregation으로 다양성 회복
Phase 1.2-C 평가셋: Recall 0.788 → 0.531, natural_language 0.73 → 0.07.

진단:
  단순 chunk top-N(limit*5=25)으로 raw chunks 가져왔는데 같은 doc의
  여러 chunks가 상위에 몰림 → unique doc 다양성 붕괴.
  warm test debug: 'chunks raw=16 compressed=5 unique_docs=10'

해결 (사용자 추천 C):
  Window function ROW_NUMBER() PARTITION BY doc_id로 doc당 top 2 chunks만 반환.

SQL 흐름:
  1. inner CTE topk: ivfflat 인덱스로 top inner_k chunks 빠르게
     (inner_k = max(limit*10, 200))
  2. ranked CTE: PARTITION BY doc_id ORDER BY dist ROW_NUMBER
  3. outer: rn <= 2 (doc당 max 2 chunks) + JOIN documents
  4. limit = limit * 4 (chunks 단위, ~limit*2 unique docs)

reranker 호환:
  doc당 max 2 chunks 그대로 반환 → chunks_by_doc 보존
  compress_chunks_to_docs는 그대로 동작 (best chunk per doc)
  Phase 1.3 reranker가 chunks_by_doc에서 raw chunks 회수 가능

핵심 원칙: vector retrieval은 chunk로 찾고 doc으로 선택해야 한다.
2026-04-08 12:47:22 +09:00
Hyungi Ahn
7a38c95f3f feat(ui): Phase F — Inbox 분류 UX + review_status hotfix
F.1 review_status 버그 fix + 승인 UX 가드:
- PATCH body에 review_status: 'approved' 누락 버그 수정 (hotfix)
  → 기존에는 승인해도 문서가 inbox에서 사라지지 않던 증상 해결
- isApprovable(doc): effective domain(override or original)이 비어 있으면 false
- 미분류 행: 체크박스 disabled + ⚠ "도메인 선택 필요" Badge 인라인 표시
  + 카드 border-warning 강조. 클릭 자체가 막힘 (toast 경고 아님)

F.2 runes 마이그레이션 + 프리미티브 전환:
- let → $state/$derived/$derived.by, onMount 유지
- Card/Button/Select/TextInput/Badge/EmptyState/Skeleton/Modal/
  ConfirmDialog/FormatIcon/TagPill 프리미티브로 전면 재작성
- 기존 bg-[var(--*)] 클러스터 전부 제거

F.3 필터 row:
- source / format Select 드롭다운 (현재 documents에서 동적 집계)
- confidence는 백엔드 ai_confidence 필드 추가 대기 — 주석 TODO(backend)

F.4 처리 단계 가시성:
- extracted_at / ai_processed_at / embedded_at 3개 Badge
  (success tone = 완료, neutral = 대기) + source_channel 표시
- backend 전용 endpoint 없이 기존 응답 필드만으로 stop-gap

F.5 행별 override:
- Map<id, { domain?, tags? }> 로컬 state
- 도메인 select 변경 시 overrides에 기록, 원복 버튼으로 clear
- 승인(approveOne) 시점에 override를 PATCH body에 병합
- 도메인 override로 미분류 → 분류 전환 가능 (바로 승인 가능해짐)

F.6 배치 override + 재시도 stub:
- 선택 toolbar: 일괄 도메인 / 일괄 태그 modal
- 배치 override는 로컬 Map만 갱신, 실제 PATCH는 승인 시 1회
- 재시도 버튼: disabled stub (TODO backend POST /queue/retry)
- 선택 상한 50건, pLimit(5) + Promise.allSettled 일괄 승인

검증:
- npm run build 통과 (a11y 경고 fix: label → span + aria-label)
- npm run lint:tokens 229 → 204 (inbox 레거시 var() 토큰 전부 제거, -25)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:46:03 +09:00
Hyungi Ahn
d83842ccd8 feat(ui): Phase D.4/D.5 — keyboard nav + density toggle
D.4 useListKeyboardNav:
- 신규: frontend/src/lib/composables/useListKeyboardNav.svelte.ts
  j/k/Arrow/Enter/Esc, isTypingTarget 가드 (input/textarea/select/
  contenteditable 포커스 시 비활성)
- documents/+page.svelte: kbIndex $state, kbSelectedId $derived,
  items 변경 시 clamp, URL 변경 시 0 리셋
- DocumentTable/DocumentCard: kbSelectedId prop → data-kb-selected
  속성 + ring-accent-ring 시각 표시
- scrollSelectedIntoView: queueMicrotask + querySelector로 현재
  커서를 뷰포트 내로 스크롤 (block: nearest)

D.5 Table density:
- DocumentTable: density prop (compact/comfortable), rowPaddingClass
  ($derived: py-1 | py-2.5), rowTextClass (text-[10px] | text-xs)
- documents/+page.svelte: tableDensity $state, toggleDensity 헬퍼,
  localStorage.tableDensity persistent, 테이블 뷰에서만 토글 버튼
  노출 (Rows2/Rows3 아이콘)
- 뷰 모드 버튼도 token 기반으로 리팩토링

검증:
- npm run build 통과
- npm run lint:tokens 231 → 229 (뷰 모드 버튼 token swap으로 -2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:42:03 +09:00
Hyungi Ahn
76e723cdb1 feat(search): Phase 1.3 TEI reranker 통합 (코드 골격)
데이터 흐름 원칙: fusion=doc 기준 / reranker=chunk 기준 — 절대 섞지 말 것.

신규/수정:
- ai/client.py: rerank() 메서드 추가 (TEI POST /rerank API)
- services/search/rerank_service.py:
    - rerank_chunks() — asyncio.Semaphore(2) + 5s soft timeout + RRF fallback
    - _make_snippet/_extract_window — title + query 중심 200~400 토큰
      (keyword 매치 없으면 첫 800자 fallback)
    - apply_diversity() — max_per_doc=2, top score>=0.90 unlimited
    - warmup_reranker() — 10회 retry + 3초 간격 (TEI 모델 로딩 대기)
    - MAX_RERANK_INPUT=200, MAX_CHUNKS_PER_DOC=2 hard cap
- services/search_telemetry.py: compute_confidence_reranked() — sigmoid score 임계값
- api/search.py:
    - ?rerank=true|false 파라미터 (기본 true, hybrid 모드만)
    - 흐름: fused_docs(limit*5) → chunks_by_doc 회수 → rerank_chunks → apply_diversity
    - text-only 매치 doc은 doc 자체를 chunk처럼 wrap (fallback)
    - rerank 활성 시 confidence는 reranker score 기반
- tests/search_eval/run_eval.py: --rerank true|false 플래그

GPU 적용 보류:
- TEI 컨테이너 추가 (docker-compose.yml) — 별도 작업
- config.yaml rerank.endpoint 갱신 — GPU 직접 (commit 없음)
- 재인덱싱 완료 후 build + warmup + 평가셋 측정
2026-04-08 12:41:47 +09:00
Hyungi Ahn
b80116243f feat(search): Phase 1.2-C chunks 기반 vector retrieval + raw chunks 보존
retrieval_service.search_vector를 documents.embedding → document_chunks.embedding로 전환.
fetch_limit = limit*5로 raw chunks를 넓게 가져온 후 doc 기준 압축.

신규: compress_chunks_to_docs(chunks, limit) → (doc_results, chunks_by_doc)
- doc_id 별 best score chunk만 doc_results (fusion 입력)
- 모든 raw chunks는 chunks_by_doc dict에 보존 (Phase 1.3 reranker용)
- '같은 doc 중복으로 RRF가 false boost' 방지

SearchResult: chunk_id / chunk_index / section_title optional 필드 추가.
- text 검색 결과는 None (doc-level)
- vector 검색 결과는 채워짐 (chunk-level)

search.py 흐름:
1. raw_chunks = await search_vector(...)
2. vector_results, chunks_by_doc = compress_chunks_to_docs(raw_chunks, limit)
3. fusion(text_results, vector_results) — doc 기준
4. (Phase 1.3) chunks_by_doc → reranker — chunk 기준

debug notes: raw=N compressed=M unique_docs=K로 흐름 검증.

데이터 의존: 재인덱싱(reindex_all_chunks.py 진행 중) 완료 후 평가셋으로 검증.
2026-04-08 12:36:47 +09:00
Hyungi Ahn
3375a5f1b1 feat(ui): Phase D.3 — multi-select + batch actions (pLimit)
- DocumentTable/DocumentCard: selectable/selectedIds/onselectionchange props
  * Table: 왼쪽 6px 너비 체크박스 컬럼
  * Card: 좌상단 absolute 체크박스 (hover 또는 selected 시 표시)
  * 체크박스 onclick stopPropagation으로 행 select와 분리
- documents/+page.svelte:
  * selectedIds = $state(new Set()), URL/필터 변경 시 자동 초기화
  * sticky 선택 toolbar (selection > 0): N건 / 전체 선택 / 선택 해제 /
    일괄 도메인 / 일괄 태그 / 일괄 삭제
  * 50건 상한 UI 가드 (초과 시 경고 + 모든 bulk 버튼 disabled)
  * Bulk modals:
    - 일괄 도메인: Select (Knowledge/* 6종 + Reference)
    - 일괄 태그: TextInput (기존 ai_tags에 추가, 중복 skip)
    - 일괄 삭제: ConfirmDialog (delete_file=true)
  * runBulk 헬퍼: pLimit(5) + Promise.allSettled로 concurrency 제한,
    성공/실패 카운트 toast (5대 원칙 #4)
  * TODO(backend): POST /documents/batch-update — 단일 트랜잭션으로 교체

검증:
- npm run build 통과 (새 경고 없음, label → span 교체로 a11y clean)
- npm run lint:tokens 231 유지 (신규 코드 위반 0)
- 기존 pLimit.ts (Phase A 머지) 재사용, 외부 의존성 없음

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:34:02 +09:00
Hyungi Ahn
42dfe82c9b feat(chunk): Phase 1.2-E reindex 스크립트 추가
tests/scripts/reindex_all_chunks.py — 전체 documents chunk 재인덱싱 도구.

핵심 요건 (사용자 정의):
- asyncio.Semaphore(N) — 동시 처리 수 제한 (기본 3, Ollama bge-m3 부하 조절)
- checkpoint resume — JSON 파일 atomic swap, 중간 실패/중단 후 재시작 가능
- rate limiting — 작업 간 sleep 0.1초 (Ollama API 보호)
- 진행 로그 — [REINDEX] N/total (P%) ETA: ... fails: N (~2% 단위)

CLI:
- --concurrency, --checkpoint, --rate-limit, --limit (dry-run), --skip-existing

야간 배치 (00:00~06:00):
  PYTHONPATH=app .venv/bin/python tests/scripts/reindex_all_chunks.py \
    --concurrency 3 --checkpoint checkpoints/reindex.json \
    > logs/reindex.log 2>&1 &
2026-04-08 12:31:29 +09:00
Hyungi Ahn
8f312f50a7 feat(ui): Phase D.2 — filter chips + URL sync
- 검색바 아래 새 필터 칩 row: domain/tag/format/source 활성 필터를
  인라인 칩으로 렌더, 각 칩에 X 버튼으로 제거.
- `+ 태그` popover: 현재 결과의 상위 20개 태그 클라이언트 집계
  (items.flatMap(d => d.ai_tags).counts + sort) → 선택 시 ?tag=...
- `+ 형식` popover: FORMATS 화이트리스트 (pdf/hwp/hwpx/md/docx/xlsx/png/jpg)
  → 선택 시 ?format=...
- 바깥 클릭으로 popover 자동 close ($effect + document listener)
- filterFormat $derived + loadDocuments params 확장 + hasActiveFilters 확장
- 결과 헤더는 카운트만 남기고 필터 표시/초기화는 칩 row로 이전 (중복 제거)
- addFilter/removeFilter 헬퍼로 URL 라운드트립 관리 (domain 제거 시 sub_group 함께)
- 백엔드 변경 없음 (GET /documents/가 이미 tag/format 지원)

검증:
- npm run build 통과
- npm run lint:tokens 236 → 231 (신규 코드 0 위반, 결과 헤더 리팩토링으로
  5건 organically 감소)
- popover 키보드 a11y (role=listbox/option, aria-expanded, aria-selected)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:27:13 +09:00
Hyungi Ahn
731d1396e8 fix(chunk): _chunk_legal 영어 법령 sliding window fallback
영어/외국 법령(ai_domain Foreign_Law 등)은 '제N조' 패턴이 없어 split 결과가
1개 element만 나옴 → 서문 chunk(첫 1500자)만 생성되고 본문 대부분 손실.

발견: doc 3759 (Industrial Safety, 93KB 영어) → 1개 chunk만 생성.

수정: parts split 결과가 1개 이하면 _chunk_sliding fallback 호출.
한국어 법령(제N조 패턴 있음)은 기존 분할 로직 그대로 작동.

Phase 1.2-D smoke test에서 발견. 재인덱싱 전 fix 필수.
2026-04-08 12:26:38 +09:00
Hyungi Ahn
ffac4975b9 feat(ui): Phase D.1 — 3-panel layout + DocumentMetaRail + useMedia
- 가로 flex 최상위 + 가운데 flex-1 (기존 list/viewer 세로 split 그대로 보존)
- xl+ (≥1280px): 우측 320px persistent rail, 접기 시 40px sliver.
  localStorage.metaRailOpen 으로 상태 유지.
- < xl : 기존 수동 drawer 제거하고 ui/Drawer primitive + uiState 사용.
- 리사이즈 시 xl+ 진입하면 drawer 자동 close (rail로 승계).
- handleKeydown → ui.handleEscape() 로 중앙화.
- ℹ 버튼 token 기반 재작성 (isXl 분기로 rail/drawer 토글).
- PreviewPanel.svelte 한 글자도 수정 없음 (Phase E 영역).

신규:
- frontend/src/lib/composables/useMedia.svelte.ts — matchMedia runes 컴포저블
- frontend/src/lib/components/DocumentMetaRail.svelte — PreviewPanel wrapper

검증:
- npm run build 통과
- npm run lint:tokens 241 → 236 (신규 코드 0 위반, 레거시 drawer/ℹ 버튼
  제거로 5건 organically 감소)
- PreviewPanel diff 0줄

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:18:06 +09:00
Hyungi Ahn
b3124928a6 feat(ui): Phase C — 대시보드 위젯 그리드 + dashboardSummary 공유 fetch
- dashboardSummary store 구독으로 SystemStatusDot과 fetch 1회 공유 (60초 폴링)
- Svelte 5 runes + Card/EmptyState/Skeleton/FormatIcon 프리미티브
- 12-col 그리드 (sm 1열 / md 2열 / lg 표 그대로):
  * 행1: stat 4장 (전체/Inbox/법령/시스템 상태)
  * 행2: 파이프라인 가로 막대 차트(8) + 오늘 도메인 누적바(4)
  * 행3: 최근 문서(8) + CalDAV stub(4)
- 신규 util: domainSlug.ts — ai_domain → bg-domain-{slug} + 라벨 매핑
- 새 코드에 bg-[var(--*)] 0건 (lint:tokens 통과)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:13:36 +09:00
Hyungi Ahn
f9af8dd355 fix(search): trigram threshold 0.3 → 0.15 (set_limit)
Phase 1.2-B 평가셋 결과 recall 0.788 → 0.750 회귀.
원인: trigram default threshold 0.3이 multi-token 쿼리에서 너무 엄격.

예: '이란 미국 전쟁 글로벌 반응' 같은 5단어 한국어 뉴스 쿼리는
title/ai_summary trigram 매칭이 거의 안 됨.

해결: search_text 시작 시 set_limit(0.15) 호출.
- trigram 매칭 더 관대 (recall ↑)
- precision은 ORDER BY similarity 가중 합산이 보정
- p95 latency 169ms 여유 충분 (목표 500ms)
2026-04-08 11:58:41 +09:00
Hyungi Ahn
ca3e1952d2 fix(search): trigram % operator escape 수정 (%% → %)
SQLAlchemy text() + asyncpg dialect에서 trigram operator 위치의 %%는
unescape 안 되어 'text %% unknown' 에러 발생. 단일 %로 변경.
ILIKE의 string literal 안의 %%는 PostgreSQL에서 두 wildcard로 동작했으나,
operator 위치는 escape 처리 경로가 다름.
2026-04-08 11:53:24 +09:00
Hyungi Ahn
fab3c81a0f fix(search): Phase 1.2-B UNION 분해로 trigram/FTS 인덱스 강제 활용
EXPLAIN 진단: OR 통합 WHERE는 PostgreSQL planner가 인덱스 결합 못 함
(small table 765 docs라 Seq Scan 선택). Filter 524ms.

해결: WHERE OR을 CTE candidates UNION으로 분해.
- title trigram     → idx_documents_title_trgm        (0.5ms)
- ai_summary trigram → idx_documents_ai_summary_trgm  (length>0 매치 추가)
- FTS @@            → idx_documents_fts_full          (0.05ms)

EXPLAIN 측정: 525ms → 26ms (95% 감소).
본 SELECT(similarity 가중 합산 + ORDER BY) 추가하면 100~150ms 예상.
2026-04-08 11:51:06 +09:00
Hyungi Ahn
22117a2a6d feat(search): Phase 1.2-AB — migration 016 + trigram retrieval
migration 016: documents FTS 확장 + trigram 인덱스 (1.5초 빌드)
- idx_documents_fts_full — title+ai_tags+ai_summary+user_note+extracted_text 통합 FTS
- idx_documents_title_trgm — title 단독 trigram
- idx_documents_extracted_text_trgm — 본문 trigram (NULL 제외)
- idx_documents_ai_summary_trgm — AI 요약 trigram
- CONCURRENTLY 불필요 (765 docs / 6.5MB)

retrieval_service.search_text: ILIKE 완전 제거 → trigram % + similarity()
- WHERE: title %, ai_summary %, FTS @@ (모두 인덱스 활용)
- ORDER BY: 5컬럼 similarity 가중 합산 + ts_rank * 2.0
- 가중치 그대로 (title 3.0 / tags 2.5 / note 2.0 / summary 1.5 / extracted 1.0)
- threshold default 0.3 (필요 시 set_limit으로 조정)

목표: text_ms 470ms → 100~200ms (ILIKE 풀스캔 제거 효과)
2026-04-07 14:36:22 +09:00
Hyungi Ahn
0c63c0b6ab feat(ui): Phase B — sidebar drawer + SystemStatusDot + 키보드 nav
- +layout.svelte: 햄버거 → IconButton, 우측 nav → Button ghost,
  sidebar overlay → Drawer (uiState 단일 slot),
  Esc 글로벌 핸들러 ui.handleEscape() 위임 (5대 원칙 #2)
- lib/stores/system.ts (신규): dashboardSummary writable + 60s 폴링,
  단일 fetch를 SystemStatusDot(B)와 dashboard(C)가 공유
- SystemStatusDot.svelte (신규): 8px 도트 + tooltip,
  failed > 0 → error / pending > 10 → warning / 그 외 → success
- Sidebar.svelte: 트리에 ArrowUp/Down 키보드 nav,
  활성 도메인 row에 aria-current="page"
2026-04-07 13:52:24 +09:00
Hyungi Ahn
a4eb71d368 feat(search): Phase 1.1a 모듈 분리 — services/search/ 디렉토리
검색 로직을 services/search/* 모듈로 분리. trigram 도입은 Phase 1.2 인덱스와 함께.

신규:
- services/search/{__init__,retrieval_service,rerank_service,query_analyzer,evidence_service,synthesis_service}.py
- retrieval_service는 search_text/search_vector 이전 (ILIKE 동작 그대로)
- 나머지는 Phase 1.3/2/3 placeholder

이동:
- services/search_fusion.py → services/search/fusion_service.py (R100)

수정:
- api/search.py — thin orchestrator로 축소 (251줄 → 178줄)

동작 변경 없음 — 구조만 분리. 회귀 검증 후 Phase 1.2 진입.
2026-04-07 13:46:04 +09:00
Hyungi Ahn
e0f45f9ce0 fix(deploy): primary endpoint 8801로 갱신
ai-gateway 환경 변수 PRIMARY_ENDPOINT를 8800 → 8801로 갱신.
mlx-proxy 경유 라우팅에 맞춰 정합성 확보.
2026-04-07 13:26:55 +09:00
Hyungi Ahn
378fbc7845 feat(chunk): Phase 0.1 chunk 인덱싱 — ORM/worker/migration 정리
GPU 서버에 untracked로만 존재하던 Phase 0.1 코드를 정식 commit:
- app/models/chunk.py — DocumentChunk ORM (country/source/domain 메타 포함)
- app/workers/chunk_worker.py — 6가지 chunking 전략 (legal/news/markdown/email/long_pdf/default)
- migrations/014_document_chunks.sql — pgvector + FTS + trigram 인덱스
- app/models/queue.py — ProcessingQueue enum에 'chunk' stage 추가
- app/workers/queue_consumer.py — chunk stage 등록, classify→[embed,chunk] 자동 연결

Phase 1 reranker 통합 작업의 전제 조건. document_chunks 테이블 기반 retrieval에 사용.
2026-04-07 13:26:37 +09:00
Hyungi Ahn
a2941487fe fix(documents): detail view에 raw markdown fetch fallback 추가
A-8 작전 후 사용자 보고: 마크다운 전체보기에서 "텍스트 추출 대기 중"
fallback이 뜨는 문서가 있음.

원인: split view의 DocumentViewer는 extracted_text 없으면 원본 .md
파일을 fetch해서 보여주는데, detail view (routes/documents/[id]/+page.svelte)
는 fetch fallback이 없어 즉시 fallback 메시지로 떨어짐. 두 view의 동작
불일치가 A-8 작업 중 사용자 시각 검증 과정에서 드러남.

A-8 회귀 아님 — 이 페이지는 routes 잔존 그룹(36 hits)이라 A-8 batch에서
한 줄도 변경 안 됨 (git diff fcce764..c294df5로 검증).

해결: DocumentViewer와 동일한 fetch fallback 로직을 detail view에도 추가.
fallback 우선순위:
  1. doc.extracted_text 있으면 사용
  2. 없으면 raw markdown fetch 시도
  3. 둘 다 없으면 "*텍스트 추출 대기 중*" 메시지

scope:
- script onMount: vt가 markdown/hwp-markdown이고 extracted_text 없으면
  /api/documents/{id}/file fetch
- template: renderMd fallback chain에 rawMarkdown 추가

routes 색상 토큰 swap (이 페이지의 36 hits)은 별도 이슈 — Phase D에서
정식 처리. 본 hotfix는 콘텐츠 표시 문제만 해결.
2026-04-07 12:55:59 +09:00
Hyungi Ahn
c294df5987 refactor(tokens): A-8 Batch 3 — PreviewPanel / DocumentViewer / +layout(toast)
가장 큰 위험 batch. Phase A 디자인 시스템 정착 마지막 mechanical refactor
(8 파일 8/8 누적 — core components 0 hit 달성).

PreviewPanel (53 hits → 0):
- bg-[var(--sidebar-bg)] → bg-sidebar (메인 aside)
- bg-[var(--bg)]         → bg-bg (input 배경)
- bg-[var(--surface)]    → bg-surface (hover)
- bg-[var(--accent)]     → bg-accent + hover:bg-accent-hover (저장 버튼)
- bg-[var(--error)]      → bg-error (삭제 확인)
- text/border 토큰 일괄 swap
- focus:border-accent (input)
- confidence 색상 (green/amber/red palette)은 plan B3 명시 없어 그대로

DocumentViewer (28 hits → 0):
- 뷰어 본체 bg-surface border-default
- 툴바 bg-sidebar
- 마크다운 편집 탭 bg-surface, edit textarea bg-bg
- 상태별 hover 토큰 swap
- 뉴스 article 태그 blue-900/30 그대로 (lint:tokens 미검출)

+layout.svelte (10 hits → 0):
- nav 잔여 var() (햄버거, 로고, 메뉴 링크) 토큰 swap
- 로딩 텍스트 text-dim
- toast 영역 의미 swap (plan B3 명시):
  * green-900/200  → bg-success/10 + text-success + border-success/30
  * red-900/200    → bg-error/10 + text-error + border-error/30
  * yellow-900/200 → bg-warning/10 + text-warning + border-warning/30
  * blue-900/200   → bg-accent/10 + text-accent + border-accent/30
- class:* 디렉티브 8개 → script TOAST_CLASS dict + dynamic class binding
  (svelte 5에서 슬래시 포함 클래스명을 class: 디렉티브로 못 씀)

검증:
- npm run lint:tokens : 360 → 269 (-91, B3 파일 0 hit)
- 누적 진행: 421 → 269 (-152 / 8 파일 완료, plan 정정 목표 정확 달성)
- npm run build       : 
- npx svelte-check    :  0 errors
- ⚠ 3-risk grep       : hover/border-border/var() 잔여 0건

A-8 종료 시점 상태:
- core components 8 파일: lint:tokens 0 hit 
- routes 7 파일 잔존 (~269): news 92, settings 47, documents/[id] 36,
  +page 28, documents 26, inbox 25, login 15
- lint:tokens 강제화 (pre-commit hook)는 Phase D + F 완료 후 별도 commit

플랜: ~/.claude/plans/compressed-churning-dragon.md §A.4 Batch 3
2026-04-07 12:14:48 +09:00
Hyungi Ahn
8ec89517ee refactor(tokens): A-8 Batch 2 — Sidebar / DocumentCard / DocumentTable
목록/사이드바 영역의 var() 토큰을 의미 토큰으로 swap. Phase A 디자인
시스템 정착의 두 번째 mechanical refactor batch (8 파일 중 5/8 누적).

Sidebar:
- bg-[var(--sidebar-bg)]  → bg-sidebar  (이름 변경)
- border-[var(--border)]  → border-default
- text-[var(--text)]      → text-text
- text-[var(--text-dim)]  → text-dim
- bg-[var(--accent)]/15   → bg-accent/15
- hover:bg-[var(--surface)] → hover:bg-surface
- domain 색상 inline style (DOMAIN_COLORS)은 그대로 유지

DocumentCard:
- bg/border/text/hover 토큰 일괄 swap
- DOMAIN_COLORS의 var(--domain-*) 유지 (plan B2 비고)
- blue-400/blue-900/30 (news icon, data_origin work) 그대로
  (lint:tokens 미검출 + plan 명시 없음)

DocumentTable:
- 헤더 + 행 + selected 상태 + 컬럼 텍스트 일괄 swap
- border-l-[var(--accent)] → border-l-accent
- border-default/30 opacity suffix (행 구분선) v4 시각 검증 필요

검증:
- npm run lint:tokens : 407 → 360 (-47, B2 파일 0 hit)
- npm run build       : 
- npx svelte-check    :  0 errors
- ⚠ 3-risk grep       : hover/border-border/var() 잔여 0건

플랜: ~/.claude/plans/compressed-churning-dragon.md §A.4 Batch 2
2026-04-07 12:04:37 +09:00
Hyungi Ahn
451c2181a0 refactor(tokens): A-8 Batch 1 — TagPill / UploadDropzone
색상 시스템을 의미 토큰으로 swap. Phase A 디자인 시스템 정착의 첫
mechanical refactor batch (8 파일 중 2 파일).

TagPill: 4가지 prefix별 색상을 의미 토큰화
- @상태/  amber → warning
- #주제/  blue  → accent
- $유형/  green → success
- !우선순위/ red → error
- fallback bg-[var(--border)] → bg-default,
           text-[var(--text-dim)] → text-dim

UploadDropzone: 드래그 오버레이 + 업로드 진행 영역
- bg-[var(--accent)]/10 → bg-accent/10
- bg-[var(--surface)]   → bg-surface
- border-[var(--border)] → border-default
- text-[var(--text-dim)] → text-dim
- 상태별 텍스트: text-success / text-error / text-accent / text-dim

검증:
- npm run lint:tokens : 421 → 407 (-14, B1 파일 0 hit)
- npm run build       : 
- npx svelte-check    :  0 errors
- ⚠ 3-risk grep       : hover/border-border/var() 잔여 0건

플랜: ~/.claude/plans/compressed-churning-dragon.md §A.4 Batch 1
참고: 본 plan은 161ff18(search Phase 0.5 commit)에 styleguide 2개 파일이
의도와 다르게 묶여 main에 들어왔음. 기능 영향 0 — Option A 결정으로
commit history 미수정.
2026-04-07 11:44:29 +09:00
Hyungi Ahn
fcce764e9d chore: pre A-8 token swap snapshot 2026-04-07 09:39:45 +09:00
Hyungi Ahn
6b2747de96 chore: allow /__styleguide in dev public paths
A-9 styleguide 라우트가 dev 환경에서 auth gate를 우회할 수 있도록
PUBLIC_PATHS / NO_CHROME_PATHS에 /__styleguide 추가.

production 영향 0 — +page.ts의 dev 가드가 비-dev 환경에서는 /로
redirect하므로 styleguide 라우트 자체에 도달 못 함.

A-8 토큰 swap 작전과 의미적으로 무관한 dev-only 변경이라
revert 단위 분리를 위해 단독 commit.
2026-04-07 09:39:39 +09:00
Hyungi Ahn
8021a1debd test(search): Phase 0.5 fusion 전략 A/B 비교 결과
23개 평가셋 × 3 전략(legacy/rrf/rrf_boost) 측정 + 분석.

핵심 발견:
- 전체 NDCG: legacy 0.705 → rrf 0.699 → rrf_boost 0.700 (미세 차이)
- RRF가 약간 나쁜 이유: kw_001(산업안전보건법 제6장)에서 RRF가 4041
  (근로기준법 안전과 보건)을 false positive로 promotion. NDCG 1.000→0.906.
- boost가 가치 입증한 사례: news_004(guerre en Iran)에서 RRF의 미스를
  완벽 보정해 legacy NDCG 복원.
- RRF의 진짜 가치는 Phase 1+ 다중 신호(trigram, reranker, multi-query)
  통합 시 발휘됨. 현 평가셋은 너무 단순해서 차이가 noise에 묻힘.

결정: rrf_boost를 default로 유지. Phase 1 후 재측정.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:25:49 +09:00
Hyungi Ahn
161ff18a31 feat(search): Phase 0.5 RRF fusion + 강한 신호 boost
기존 weighted-sum merge를 Reciprocal Rank Fusion으로 교체.
정확 키워드 매치에서 RRF가 평탄화되는 문제는 boost로 보완.

신규 모듈 app/services/search_fusion.py:
- FusionStrategy ABC
- LegacyWeightedSum  : 기존 _merge_results 동작 (A/B 비교용)
- RRFOnly            : 순수 RRF, k=60
- RRFWithBoost       : RRF + title/tags/법령조문/high-text-score boost (default)
- normalize_display_scores: SearchResult.score를 [0..1] 랭크 기반 정규화
  (프론트엔드가 score*100을 % 표시하므로 RRF 원본 점수 노출 시 표시 깨짐)

search.py:
- ?fusion=legacy|rrf|rrf_boost 파라미터 (default rrf_boost)
- _merge_results 제거 (LegacyWeightedSum에 흡수)
- pre-fusion confidence: hybrid는 raw text/vector 신호로 계산
  (fused score는 fusion 전략마다 스케일이 달라 일관 비교 불가)
- timing에 fusion_ms 추가
- debug notes에 fusion 전략 표시

telemetry:
- compute_confidence_hybrid(text_results, vector_results) 헬퍼
- record_search_event에 confidence override 파라미터

run_eval.py:
- --fusion CLI 옵션, call_search 쿼리 파라미터에 전달

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:58:33 +09:00
Hyungi Ahn
1af94d1004 fix(search): timing 로그를 setup_logger로 출력
logging.getLogger("search")만 사용하면 uvicorn 기본 설정에서 INFO가
stdout에 안 나옴. 기존 core.utils.setup_logger 패턴 사용:
- logs/search.log 파일 핸들러
- stdout 콘솔 핸들러

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:43:26 +09:00
Hyungi Ahn
473e7e2e6d feat(search): Phase 0.4 debug 응답 옵션 + timing 로그
?debug=true로 호출 시 단계별 candidates + timing을 응답에 포함.
디버그 옵션과 별개로 모든 검색에 timing 라인을 구조화 로그로 출력
(사용자 feedback: 운영 관찰엔 debug 응답만으론 부족).

신규 응답 필드 (debug=true 시):
- timing_ms: text_ms / vector_ms / merge_ms / total_ms
- text_candidates / vector_candidates / fused_candidates (top 20)
- confidence (telemetry와 동일 휴리스틱)
- notes (예: vector 검색 실패 시 fallback 표시)
- query_analysis / reranker_scores: Phase 1/2용 placeholder

기본 응답(debug=false)은 변화 없음 (results, total, query, mode).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:41:33 +09:00
Hyungi Ahn
e104d1b47c feat: Layer 프리미티브 (Drawer / Modal / ConfirmDialog / Tabs)
UX/UI 개편 Phase A-7. uiState와 결합한 layer/dialog 컴포넌트.

신규 컴포넌트 (lib/components/ui/)
- Drawer.svelte: 단일 slot drawer (id: 'sidebar' | 'meta').
  ui.isDrawerOpen(id)로 표시 여부 결정. 새 drawer 열면 이전 drawer 자동 close.
  side(left/right) + width(sidebar/rail). backdrop 클릭으로 close.
  z-drawer 사용. 8대 원칙 #2.

- Modal.svelte: stack 지원 modal (5대 원칙 #2 — confirm 위에 nested 가능).
  native <dialog> 대신 div 기반 — top-layer가 단일이라 <dialog>로는 stack 불가.
  z-index = z-modal + (stackIndex * 2): backdrop과 panel을 별개의 stacking
  context로 두기 위해 *2. 최상단 modal만 focus trap + 키보드 nav 활성,
  아래는 inert 처리. 수동 Tab/Shift+Tab cycling.
  closable + IconButton(X) 헤더, footer snippet 지원.

- ConfirmDialog.svelte: Modal 위 얇은 wrapper. 삭제/되돌릴 수 없는 작업에
  사용. tone(danger/primary), confirmLabel/cancelLabel, onconfirm 콜백.
  ui.openModal(id)로 호출.

- Tabs.svelte: ARIA tablist + tab + tabpanel.
  좌우 화살표 / Home / End 키 nav, \$props.id() 기반 SSR-safe ID.
  tabs: { id, label, disabled? }[], value \$bindable.
  children snippet은 (activeId) => UI 시그니처 — DocumentViewer 편집/미리보기
  토글 등 단일 컨테이너 레이아웃에 쓰기 좋게 설계.

이로써 Phase A 프리미티브 13종 완비:
  Button, IconButton, Card, Badge, Skeleton, EmptyState,
  TextInput, Textarea, Select,
  Drawer, Modal, ConfirmDialog, Tabs.

모든 컴포넌트는 Svelte 5 runes mode strict, @theme 토큰만 사용,
focus-visible ring 통일, slot은 {@render children?.()}로 작성.

svelte-check: 0 errors / 8 warnings (전부 기존 latent, 새 코드 무관)
build: 2.07s 무경고

남은 Phase A:
- A-8 토큰 swap (Sidebar/TagPill/UploadDropzone/PreviewPanel/DocumentCard/
  DocumentTable/+layout toast — baseline 421건 → 0건)
- A-9 __styleguide 라우트 (전체 시각 검증 + Modal stack 데모)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:40:08 +09:00
Hyungi Ahn
ad23925ed5 feat: 입력 프리미티브 (TextInput / Textarea / Select) + tsconfig 보정
UX/UI 개편 Phase A-6.

신규 컴포넌트 (lib/components/ui/)
- TextInput.svelte: \$bindable value, label/error/hint, leading/trailing icon,
  \$props.id() 기반 SSR-safe 자동 id, aria-describedby 자동 연결.
- Textarea.svelte: TextInput과 동일 구조 + autoGrow 옵션
  (\$effect로 scrollHeight 동기화, maxRows 지원).
- Select.svelte: 네이티브 <select> 래퍼, ChevronDown 표시.
  options: { value, label, disabled? }[]

빌드 환경 보정
- frontend/tsconfig.json 신규: svelte-kit 자동 생성 .svelte-kit/tsconfig.json을
  extends. 이게 없으면 svelte-check가 \$lib path mapping과 .svelte.ts 모듈
  resolution을 못 잡아 "Cannot find module" 에러 발생. SvelteKit 표준 패턴.
  strict는 false로 시작 (기존 코드 implicit any 다수 — 점진적 정리 예정).
- Button/IconButton/EmptyState/TextInput의 icon prop 타입을 IconComponent(any)로
  완화. lucide-svelte v0.400은 legacy SvelteComponentTyped 기반이라 Svelte 5의
  Component<P, E, B> 시그니처와 호환 안 됨. v0.469+ 업그레이드 후 좁힐 예정.

svelte-check: 0 errors / 8 warnings (전부 기존 latent, 새 코드 무관)
build: 2.07s 무경고

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:39:44 +09:00
Hyungi Ahn
70b27d4a51 fix(search): confidence 임계값 완화 + hybrid +vector boost 가산
baseline 평가셋 실행 시 'summary+vector' top_score 2.39가 임계값 2.5에
미달해 정답 쿼리(산업안전보건법 제6장)가 low_confidence로 잘못 잡힘.

- 텍스트 매치 임계값 0.5씩 완화 (실측 분포 반영)
- '+vector' 접미사가 있으면 hybrid 합성 매치이므로 confidence +0.10 가산
- 정답률 5/5 → 4/5 false-positive 1건 제거 기대

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:37:13 +09:00
Hyungi Ahn
50e6b5ad90 fix(search): confidence 휴리스틱 vector-only amplify 버그 수정
vector-only 매치(match_reason == 'vector')에서 raw 코사인 0.43이
0.6으로 잘못 amplify되어 low_confidence threshold(0.5)를 못 넘기던 문제.

- vector-only 분기: amplify 제거, _cosine_to_confidence로 일관 환산
- _cosine_to_confidence: bge-m3 코사인 분포 (무관 텍스트 ~0.4) 반영
- 코사인 0.55 = threshold 경계(0.50), 0.45 미만은 명확히 low

smoke test 결과 zzzqxywvkpqxnj1234 같은 무의미 쿼리(top cosine 0.43)가
low_confidence로 잡히지 않던 문제 해결.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:33:25 +09:00
Hyungi Ahn
f005922483 feat(search): Phase 0.3 검색 실패 자동 로깅
검색 실패 케이스를 자동 수집해 gold dataset 시드로 활용.
wiggly-weaving-puppy 플랜 Phase 0.3 산출물.

자동 수집 트리거 (3가지):
- result_count == 0           → no_result
- confidence < 0.5            → low_confidence
- 60초 내 동일 사용자 재쿼리   → user_reformulated (이전 쿼리 기록)

confidence는 Phase 0.3 휴리스틱 (top score + match_reason).
Phase 2 QueryAnalyzer 도입 후 LLM 기반으로 교체 예정.

구현:
- migrations/015_search_failure_logs.sql: 테이블 + 3개 인덱스
- app/models/search_failure.py: ORM
- app/services/search_telemetry.py: confidence 계산 + recent 트래커 + INSERT
- app/api/search.py: BackgroundTasks로 dispatch (응답 latency 영향 X)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:29:12 +09:00
Hyungi Ahn
7fa7dc1510 feat: 디자인 시스템 기반 — 유틸 헬퍼 + CI rule + 첫 6개 프리미티브
UX/UI 개편 Phase A-3 / A-4 / A-5. 후속 phase가 곧바로 소비할 수 있도록
디자인 시스템의 코어 자산을 한꺼번에 도입한다.

A-3 — 유틸 헬퍼 (lib/utils/)
- pLimit.ts: 동시 실행 N개 제한 (5대 원칙 #4 — 일괄 PATCH/DELETE에서
  GPU 서버/SSE 부하 방지). 외부 의존성 없음.
- mergeDoc.ts: PATCH/SSE 응답을 로컬 cache에 머지할 때 updated_at으로
  stale 갱신 차단 (5대 원칙 #6 — optimistic update conflict resolution).
  dropDoc 헬퍼 포함.

A-4 — CI 토큰 차단 (5대 원칙 #1)
- scripts/check-tokens.sh: bg-[var(--*)] 등 임의값 토큰 우회 grep 차단.
- npm run lint:tokens 등록.
- 현재 baseline 421건 — A-8 토큰 swap에서 0으로 떨어진 후 pre-commit 강제화.

A-5 — 첫 6개 프리미티브 (lib/components/ui/)
- Button.svelte: variant(primary/secondary/ghost/danger) × size(sm/md),
  loading/disabled, icon 슬롯, href 자동 a 변환, focus-visible ring.
- IconButton.svelte: 정사각형, aria-label 필수, Button과 동일 variant 체계.
- Card.svelte: bg-surface + rounded-card + border-default 패턴 1군데화.
  padded/interactive 옵션, interactive면 button 시맨틱.
- Badge.svelte: 의미적 tone(neutral/success/warning/error/accent) 표시.
  TagPill과 별개 (TagPill은 도메인 prefix 코드 전용).
- Skeleton.svelte: ad-hoc animate-pulse div 통합. w/h/rounded prop.
- EmptyState.svelte: icon + title + description + action slot.

모든 프리미티브는 Svelte 5 runes mode strict (\$props/\$derived/\$bindable),
@theme 토큰만 사용 (bg-surface, text-dim, border-default 등 — bg-[var(--*)] 미사용),
focus-visible ring 통일, slot은 {@render children?.()}로 작성.

svelte-check: 0 errors, 8 warnings (모두 기존 latent 이슈, 새 코드 무관).
build: 1.95s 무경고.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:26:35 +09:00
Hyungi Ahn
8742367bc2 refactor: stores 분리 — toast / uiState 단일 책임화
UX/UI 개편 Phase A-2. lib/stores/ui.ts에 섞여 있던 toast 시스템과
UI layer 상태(미사용 dead export 포함)를 의미 단위로 분리한다.
한 파일이 비대해지는 시나리오를 처음부터 차단(plan 8대 원칙 #7).

- lib/stores/toast.ts 신규 — toasts/addToast/removeToast (Toast interface export)
- lib/stores/uiState.svelte.ts 신규 — drawer 단일 slot + modal stack 클래스 (5대 원칙 #2)
  · openDrawer/closeDrawer/isDrawerOpen
  · openModal/closeTopModal/isModalOpen/modalIndex/topModal
  · handleEscape (modal stack 우선 → drawer)
- lib/stores/ui.ts 삭제 — sidebarOpen/selectedDocId는 어디서도 import되지 않은 dead export였음
- 11개 파일 import 경로 갱신: \$lib/stores/ui → \$lib/stores/toast

uiState는 아직 어디서도 사용 안 함 — Phase B에서 sidebar/meta drawer가 전환될 때
ui.openDrawer('sidebar') 형태로 채택. 동작 변경 0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:26:11 +09:00
Hyungi Ahn
ec36ea3d6d test(search): Phase 0.2 baseline 측정 결과
23개 쿼리에 대한 현재 검색(FTS+ILIKE+Vector hybrid) baseline.
Phase 1+ 개선 비교 기준점으로 보존.

전체: Recall@10 0.788 / NDCG@10 0.705 / Top-3 0.95 / p95 1695ms

핵심 약점 (Phase 1+ 타겟):
- news_crosslingual catastrophic (Recall 0.14) → domain-aware 필수
- failure-case precision 0/3 → confidence threshold 부재
- p95 1695ms (목표 500ms의 3배) → trigram/parallel retrieval
- nl 쿼리 top-3 ordering 약함 → chunk-level + reranker

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:22:53 +09:00
Hyungi Ahn
8490cfed10 test(search): Phase 0.2 평가셋 + 평가 스크립트
22개 쿼리(6개 카테고리)와 Recall/MRR/NDCG@10 + latency p50/p95
측정 스크립트 추가. wiggly-weaving-puppy 플랜 Phase 0.2 산출물.

- queries.yaml: 정확키워드/한국어자연어/crosslingual/뉴스/실패 케이스
  실제 코퍼스(2026-04-07, 753 docs) 기반 정답 doc_id 매핑
- run_eval.py: 단일 평가 + A/B 비교 모드, CSV 저장

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:19:38 +09:00
Hyungi Ahn
f523752971 feat: Tailwind v4 @theme 토큰 도입 — 디자인 시스템 기반 마련
UX/UI 개편 Phase A-1. CSS 변수를 Tailwind 유틸리티로 노출해서
이후 컴포넌트가 bg-surface / text-dim / border-default 형태로 작성될
수 있도록 한다. bg-[var(--*)] 임의값 패턴은 후속 lint 규칙으로 차단 예정.

- app.css에 @theme 블록 추가 (color/radius/z/spacing/domain 토큰)
- 기존 :root 변수는 .markdown-body 호환 위해 공존 유지
- +layout.svelte nav 한 줄 swap으로 v4 빌드/HMR 인식 검증 (동일 색상값)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 08:15:33 +09:00
Hyungi Ahn
fc50008843 feat: 뉴스 페이지 모바일 최적화 — 데스크톱/모바일 공존
데스크톱: 사이드바 필터 + 하단 미리보기 (기존 유지)
모바일: 드롭다운 필터 + 전체화면 미리보기 + 하단 원문 버튼
- body scroll lock (모바일 전체화면 시)
- 스크롤 위치 복원
- active 터치 피드백
- 안읽음 건수 표시
- 페이지네이션 10개 제한

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:56:33 +09:00
Hyungi Ahn
db34a06243 feat: 뉴스 미리보기 — AI 요약 상단 + 본문/메모 분리
- AI 요약: 파란 박스로 상단에 별도 표시
- 본문 입력: extracted_text에 추가 (기사 전문 붙여넣기)
- 메모: user_note에 저장 (개인 메모)
- 기사 선택 시 편집 상태 초기화

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:42:33 +09:00
Hyungi Ahn
e10b0f2883 fix: 뉴스 분야 필터 — file_path 폴더명 기반 매칭
경향신문/문화 → file_path LIKE 'news/경향신문 문화/%'

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:27:01 +09:00
Hyungi Ahn
8bb2ea4f29 fix: 뉴스 필터 트리 — 신문사명 정확 추출 + API datetime 수정
- PAPER_NAMES 매핑으로 'Le Monde', 'Der Spiegel' 등 정확 분리
- NewsSourceResponse datetime 타입 수정

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:18:50 +09:00
Hyungi Ahn
3cd65e4c26 fix: 사이드바 트리에서 News 제외 + 뉴스 페이지 ☰ 숨김
- tree API: ai_domain != 'News' 필터
- +layout: /news 경로에서 사이드바 토글 버튼 숨김
- DB: 뉴스 ai_sub_group을 신문사명으로 재설정

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:15:15 +09:00
Hyungi Ahn
557165db11 feat: 뉴스 필터 트리 (신문사 → 분야) + ai_summary 우선 표시
- 좌측 필터: 신문사 펼침 → 분야별 필터 (News/경향신문/문화)
- API: source 파라미터 '신문사' 또는 '신문사/분야' 지원
- 리스트: ai_summary 있으면 우선, 없으면 extracted_text fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:08:50 +09:00