# task-2921 보고서 — 소식지 표영역 좌표기반 격자 복원 + 표페이지 육안확인 플래그

- **작업**: [Lv.2] InsuRo `server/pdf_extract.py` — 표 영역을 좌표(Y→행, X→열) 기반으로 마크다운 표로 복원하고, 격자 신뢰도가 낮으면 줄보존 + `[[TABLE_REVIEW page=N]]` 육안확인 플래그로 폴백
- **팀**: dev3-team (다그다) | 팀원: 루(Lugh, 백엔드)
- **브랜치**: `task/task-2921-dev3` | 원격 head **f26e50b** (base 8a7239b = worktree 생성 시점 origin/main)
- **worktree**: `/home/jay/projects/InsuRo/.worktrees/task-2921-dev3`

## Situation (상황)
소식지(text_based PDF) 추출 모듈 `pdf_extract.extract_markdown`이 표 영역을 마크다운 평문으로 평탄화하여, 매트릭스 표(상품×고지사항, 셀값 N/3/5/개월)의 행·열 구조가 한 줄로 뭉개져 의미불명이 됨. 회장 실사용 지적 — DB생명 위드유매거진 p16이 대표 사례.

## Complication (문제)
1. `pdfplumber`(선 기반)는 테두리 없는 표라 실패. 좌표 클러스터링이 유일한 경로.
2. `pdf_inspector.process_pdf_bytes().pages_with_tables`는 **과탐**한다 — 실 매거진에서 14개 페이지를 표로 플래그하지만 대부분은 마케팅 레이아웃(상품 라인업 그래픽)이고, 진짜 데이터 표는 없으며 p16은 헤더열(3열)과 데이터열(5~10열)이 **근본적으로 어긋난** 복잡 매트릭스.
3. 초기 구현은 신뢰도 게이트가 거꾸로 작동 — 마케팅 레이아웃을 **빈 셀 70~85%의 희소 마크다운 표**로 억지 변환(가독성 회귀)하면서 정작 p16은 뭉갬 유지. net-negative.

## Question (핵심 질문)
격자 복원을 어떻게 적용해야 **진짜 dense 표만 마크다운 표로 승격**하고, 복잡/희소 표는 **회귀 없이** 육안확인으로 안전하게 넘길 수 있는가?

## Answer (해결)
`server/pdf_extract.py`에 좌표기반 격자 경로를 추가하되, **sparsity 우선 신뢰도 게이트**로 억지 격자를 차단:

### 구현 요약 (pdf_extract.py — +249행)
- **표 페이지 신호**: `_extract_markdown_impl`에서 `process_pdf_bytes().pages_with_tables`(1-idx)를 graceful하게 추가 수집 → 0-idx 집합으로 `_page_markdown(page, items, page_width, page_idx, is_table_page)`에 전달. 실패/부재 시 빈 집합 폴백(회귀 0).
- **격자성 판정** `_looks_like_grid`: Y밴드 행 묶기(`_group_lines`, 기존 `_region_to_markdown` 로직 추출) → 행≥2 AND 다중아이템 행 과반이면 후보.
- **격자 복원** `_grid_to_markdown -> (markdown, confident)`: 아이템 x0 정렬 → 열 간격 클러스터링(경계 `max(10.0, med_font*0.9)`) → (행,열) 셀 배정(멀티토큰 공백 join, `|`→`\|` 이스케이프) → `| h1 | h2 |` + `| --- |` + 데이터행.
- **신뢰도 게이트(핵심 수정)**: `_GRID_MIN_FILL_FRAC=0.6` — 채워진 셀 비율<60%(빈 셀>40%)면 **1차 필터로 confident=False**. 이어 ncols∈[2,12], 열충돌 행<과반, fill std/mean<0.5.
- **저신뢰 폴백**: `confident=False`면 격자 강제 금지 → 기존 `_region_to_markdown` 출력 그대로 + `[[TABLE_REVIEW page=N]]` 마커 + `table_review=True` 플래그.
- **반환 하위호환**: top-level 필수 키(markdown/pages/pdf_type/ocr_pages) 불변. page dict에 `table_review`/`is_table_page` **추가 키만**. top-level에 `table_pages`/`table_review_pages`(0-idx) 편의 키 추가.
- graceful 불변 — 격자 경로 예외 시 해당 region은 기존 렌더링으로 폴백.

## L1 스모크테스트 결과 (필수 — 실측)
- **서버 재시작**: 해당없음 (순수 함수 모듈 — 서버 프로세스 무관, 실 PDF 실행으로 대체)
- **API 응답 확인**: 해당없음
- **실 PDF 실측 (스크린샷 대체 — 텍스트 산출물)**: 실제 `DB생명 위드유매거진 26.08.pdf`(19p)를 `extract_markdown`으로 추출 (worktree, PYTHONPATH=server):
  - `table_pages(0idx)`: `[1,2,3,4,7,8,9,10,11,12,14,15,17,18]`
  - `review_pages(0idx)`: `[1,2,3,4,7,9,10,14,15,17,18]`
  - **마케팅 레이아웃 회귀 제거**: 재작업 전 page2(빈셀84%)/3(70%)/7(78%)/9(85%) 희소 표 → **전부 review=True 폴백, 마크다운 표 0건**. 마크다운 표를 만든 페이지의 빈셀 비율 최댓값 = **0% (희소 표 0건, 합격기준 ≤50% 충족)**.
  - **p16(page_idx 15)**: `is_table_page=True`, `table_review=True`, 본문 앞에 `[[TABLE_REVIEW page=15]]` 마커. 격자 강제 대신 육안확인 플래그 — 헤더/데이터 열 어긋남이 심해 격자 강제 시 왜곡되므로 스펙상 정답.
  - **회귀 0 (직접 대조)**: origin/main(기능 도입 전) 렌더링과 19페이지 전체 대조 → **본문(마커 제외) 19/19 byte-identical, body-DIFF 0건**. 비표 8페이지는 마커 없이 완전 동일, 표 11페이지는 `[[TABLE_REVIEW]]` 마커 줄만 추가.
- **dense 격자 sanity**: 합성 3열 dense 격자 → `confident=True`, `| 상품 | 구분 | 고지사항 |` + `| --- |` 정상 마크다운 표 생성 확인 (기능이 no-op 아님).

## 테스트 결과
- **`server/tests/test_pdf_extract.py`: 21 passed** (신규 sparsity/폴백 3건 포함, 기존 dense-grid 성공 테스트 유지). 팀장 독립 재실행 확인.
- **전체 `server/tests/`: 1277 passed, 1 failed** — 실패 1건은 `test_cors_fail_closed_when_ext_origin_unset`(CORS/확장오리진, pdf_extract와 무관). **로컬 `.env`의 `INSURO_EXTENSION_ORIGIN` 파일럿값 오염 기인**으로 확정: 해당 라인 제거(CI-parity) 후 재실행 → **1 passed**. 기지 이슈(task-2916 동일), CI-clean GREEN.

## 발견 이슈 및 해결
1. **신뢰도 게이트 역작동(회귀)** — 초기 구현이 희소 마케팅 레이아웃을 표로 승격. → `_GRID_MIN_FILL_FRAC=0.6` sparsity 1차 필터 추가로 해결(실측: 4개 페이지 전부 폴백 전환, 회귀 제거).
2. **`git diff origin/main` 잡음(main.py 등)** — worktree 생성 후 다른 팀 #196(task-2918)이 origin/main을 fd5e91f로 전진시킨 것. **HEAD 기준 실제 diff는 pdf_extract.py + test_pdf_extract.py 2개뿐**(allowed_resources 준수). #196은 main.py/test_detect_company_filename.py만 건드려 내 파일과 무충돌.
3. **`TextItem.width==0` quirk**(pdf_inspector) — L3 XY-cut이 넓은 컬럼 표를 조기 분할할 수 있어 격자 복원은 L2 박스모드 경로에서 안정적. 회귀는 없으나 커버리지 한계로 인지(아래 비고).

## 모델 사용 기록
- 루(Lugh, 백엔드): **sonnet** — 격자 복원 알고리즘/신뢰도 게이트 구현 + 테스트. (팀장 Opus는 설계/검토/실측검증만, 직접 코딩 없음)

## 머지 판단
- **머지 필요**: Yes (단, merge_policy=none · 순서 B → **머지·배포는 ANU 소관**)
- **브랜치**: `task/task-2921-dev3` | 원격 head f26e50b
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2921-dev3`
- **머지 의견**: 회귀 0(19/19 본문 byte-identical) 실측 확인, 스코프 2파일 준수, dense 격자 성공 경로 동작 확인. 충돌 위험 없음(#196과 파일 무교차). ANU 독립검증 권장: 실 p16 육안(현 결과=텍스트 유지+육안플래그, 격자 미강제).

## 비고 (ANU 후속 참고)
- 이 매거진에는 pdf_inspector가 표로 인식하면서 실제 dense한 데이터 표 사례가 없음 → 실 매거진 산출물은 "회귀 0 + 표페이지 11곳 육안플래그"가 핵심 가치. dense 격자 승격은 합성 테스트로 검증(향후 진짜 데이터 표 소식지에서 발현).
- `_GRID_MIN_FILL_FRAC=0.6`은 이번 실측(희소70~85% vs dense 100%)에 넉넉한 마진. 향후 채움 40~60% 구간의 진짜 표가 오면 재튜닝 여지.
- **HOLD(ANU)**: ① 실 p16 육안(격자 미강제 결정 타당성) ② 머지 ③ 서버 systemd 재배포(pdf-inspector 환경은 task-2916에서 기설치).

## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회

