# 작업 보고: task-2957 — InsuRo 증권분석 Phase 1b (보장분석 대구분 표)

- **팀**: dev2-team (오딘/Odin)
- **일시**: 2026-08-15
- **레벨**: critical (Lv.3+ 게이트 적용)
- **저장소**: `/home/jay/projects/InsuRo` · worktree `.worktrees/task-2957-dev2` · 브랜치 `task/task-2957-dev2`
- **base**: `origin/main 6eeb5b6`
- **PR**: [#218](https://github.com/Jeon-Jonghyuk/InsuRo/pull/218) — `state=open`, **`merged=false`**, 7파일 **+1225/−93**, base `main`
- **★ 머지·배포 HOLD** — ANU 독립 검증 + 회장 보고 후

---

## SCQA 요약

**S**: Phase 1a 백엔드(`policy_extract` / `policy_normalizer.build_analysis_table` / grouping)는 task-2943 PR#209로 머지됐다. 다중 증권 병합 표 생성 함수는 이미 구현·테스트 완료 상태였다.

**C**: 그러나 **런타임 호출자가 0건**이었다. `analyze-policy`(`server/main.py:6975`)는 Phase 1a 파이프라인을 전혀 import하지 않고 옛 AI 결과(`meta`+`coverages`)만 반환했고, 프론트 `aggregated` 탭은 개별 카드 + 단순 막대 합산이라 "표"가 아니었다. 즉 머지됐지만 **휴면 상태**였다.

**Q**: 기존 `analyze-policy` 응답과 개별 증권 카드를 한 줄도 깨뜨리지 않으면서, 확정 설계인 `build_analysis_table()`을 실제 사용자 화면까지 배선할 수 있는가?

**A**: 가능하다 — **병행 필드 추가**로 달성했다. 백엔드 561줄 추가/0줄 삭제(신규 모듈 + 배선 24줄), 프론트 `aggregated` 탭 교체(+195 신규 컴포넌트, 페이지 54추가/93삭제=죽은 코드 순감). 백엔드 회귀 **1820 passed / 신규 실패 0**, 프론트 **1187/1187 passed**, `npm run build` 성공(14.39s), 실브라우저 스크린샷 3장 확보. 확정 설계 파일 3종은 **diff 0줄**.

---

## 확정 설계 준수 (핵심 제약)

| 제약 | 검증 명령 | 결과 |
|---|---|---|
| `policy_normalizer.py` 무변경 | `git diff origin/main...HEAD -- server/policy_normalizer.py` | **빈 출력** ✅ |
| `policy_extract/**` 무변경 | 〃 | **빈 출력** ✅ |
| `policy_analyzer.py` 무변경 | 〃 | **빈 출력** ✅ |
| job result shape 무변경 | `json.dumps(results, ensure_ascii=False)` 라인 | **미수정** ✅ |
| 개별 증권 카드 탭 무변경 | `git diff ... \| grep -iE "individual\|togglePolicy\|expandedPolicies\|SilsonGenerationBlock"` | **0줄** ✅ |

표를 새로 만들지 않고 `build_analysis_table()`을 **그대로 재사용**했다.

---

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| /home/jay/projects/InsuRo/.worktrees/task-2957-dev2/server/policy_analysis_table.py | 신규 96줄 — 직렬화/추출래핑/payload 빌드 | grep "build_table_payload" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2957-dev2/server/main.py | +24줄 — import + _analyze_files 배선 3곳 | grep "analysis_table" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2957-dev2/server/tests/test_policy_analysis_table.py | 신규 446줄 — 계약/직렬화/실패모드/엔드포인트 통합 14건 | grep "analysis_table" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2957-dev2/src/pages/PolicyAnalysis.tsx | 54추가/93삭제 — 타입 5종 + aggregated 탭 교체 + 죽은 코드 제거 | grep "CoverageAnalysisTable" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2957-dev2/src/components/policy/CoverageAnalysisTable.tsx | 신규 195줄 — 대구분 표 컴포넌트 | grep "coverage-analysis-table" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2957-dev2/docs/analysis-table-contract.md | 신규 209줄 — 응답 위치/역할분리 계약 명문화 | grep "api/insuro/analyze-policy" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2957-dev2/src/pages/__tests__/fixtures/analysis-table-e2e.json | 신규 — E2E 픽스처(실제 백엔드 산출) | grep "UNKNOWN" OK | verified |

planned 항목 **0건**. (경로는 `/home/jay/projects/InsuRo/.worktrees/task-2957-dev2/` 기준 상대경로)

---

## 확정 JSON 계약 (팀장 선확정 → 백엔드·프론트 병렬 구현)

`analyze-policy` job result 배열의 **첫 번째 `"error"` 없는 결과 dict에만** 부착. 두 키는 상호배타.

```json
{ "analysis_table": { "columns": [PolicyColumn×11필드], "sections": [11개 고정순서] },
  "analysis_table_unavailable_reason": "no_extract_results" | "build_failed: {예외타입}" }
```

- 선례: task-2955 `silson_summary`의 3-state(키 없음=legacy / reason=불가 / 객체=정상)
- **소비자 규칙**: `results[0]` 인덱스 접근 금지 → `results.find(r => !r.error && r.analysis_table)` 필수. 배열 순회 **합산 금지**(이미 교차-증권 병합 단일 표라 중복 계산됨)
- 상세: `docs/analysis-table-contract.md`

---

## 테스트 결과 (정량)

| 항목 | 결과 |
|---|---|
| 백엔드 전체 pytest | **1820 passed, 2 skipped, 1 failed** (161.70s) |
| ⚠️ 실패 1건 (본 작업 범위 외) | `test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset` |
| ↳ pre-existing 교차검증 | `/tmp/insuro-base-2957`에 `origin/main 6eeb5b6` **clean worktree**(`.env` 없음) 체크아웃 후 동일 테스트 단독 실행 → **1 passed**. 원인은 worktree `.env:44`의 `INSURO_EXTENSION_ORIGIN`(2026-07-24 파일럿 flag-on)이 `main.py:42` `load_dotenv()`로 subprocess에 재주입되어 "미설정 시 fail-closed" 전제가 깨지는 **환경 오염**. task-2957 코드와 파일 교집합 0 |
| 신규 테스트 | `test_policy_analysis_table.py` **14 passed** |
| 프론트 테스트 | **80 test files / 1187 tests passed** (14.98s) |
| `npm run build` | **성공** (real 14.394s), `dist/` 재생성 확인 |
| red-team 스캔 | risk_level **low**, 취약점 **0건** |
| code-validator | Syntax/Static/Security **PASS** (Execution Test는 라이브러리 모듈 오탐 — returncode 0인데 stdout이 JSON이 아니라고 판정) |

**회귀 실패 0건.** (규칙 3 준수: 범위 외 실패 1건도 위와 같이 명시)

---

## L1 스모크테스트

- **서버 재시작**: **성공** — `python3 -c "import main"` exit 0, `uvicorn main:app --port 18957` → `Application startup complete`
- **API 응답 확인**:
  ```
  $ curl -s -i localhost:18957/api/insuro/policy-analysis-status/nonexistent
  HTTP/1.1 401 Unauthorized
  {"detail":"Missing or invalid authorization"}
  ```
  → 404를 예상했으나 실제는 401. 원인 확인: `Depends(verify_jwt)`가 job 조회보다 먼저 평가되는 **기존 라우트 설계**(`main.py:7103`). 버그 아님 — 예상과 다른 실측값을 그대로 기록함. 서버가 실제로 기동·응답한다는 목적은 달성
- **스크린샷** (실브라우저 Playwright, `npm run build` → `npm run preview`):
  - `/home/jay/workspace/memory/reports/screenshots/task-2957-aggregated-table.png` (전체 페이지)
  - `/home/jay/workspace/memory/reports/screenshots/task-2957-aggregated-table-closeup.png` (표 클로즈업)
  - `/home/jay/workspace/memory/reports/screenshots/task-2957-individual-tab.png` (개별 증권 탭 회귀 없음)
- **콘솔 에러**: **0건** (`Total messages: 5 (Errors: 0, Warnings: 2)`)
- **대구분 섹션 실제 렌더 순서**: 실손 → 진단비 → 수술비 → 일당 → 요양 → 사망 → 생활 → **미분류(맨 끝)** ✅ (빈 섹션 치료비/장애/운전은 `nonEmptySections` 필터로 미표시 — 명세대로)
- **팀장 직접 확인**: 스크린샷을 열어 육안 검증 — 3개 회사 열 메타(보험사/상품명/가입연월/납기·만기/보험료/잔여개월), `"-"`(흐림)·`"UNKNOWN"`(앰버) 구분 표기, 합산 정확성(암진단비 3000+5000+4000=**12000** ✅), `설계사 확인` 배지, 하단 금소법 고지 노출 확인
- `browser_close` 호출 완료, chrome 렌더러 잔존 0

---

## 게이트 결과

| 게이트 | 결과 |
|---|---|
| QC-RULES sha 핀 | `75cd7353ee3a` **일치** ✅ |
| **G1 — Codex 사전 검증** | **PASS** (`critical=false`, risks 5: high 2 / medium 2 / low 1) · `memory/events/task-2957.codex-gate` |
| G2 — 구현 검증 | 헤임달(구현자 분리) E2E + 팀장 독립 대조 |
| **G3 — 독립 검증** | **PASS** — `g3_independent_verifier.py --task-id task-2957` → `overall: PASS`, `fail_reasons: []` (report_parse/planned_check/file_existence/grep_verification/three_step_why 전부 PASS). WARN 1건: micro_commit — 검증기가 workspace 저장소 커밋을 세기 때문이며 **실제 커밋 8개는 InsuRo 저장소**에 존재(오탐) |
| PR 생성 | **PR #218** open · `merged=false` — `taskctl pr-open --auto`(bot token). `--action pr`은 task-2467로 자동 머지가 폐기되어 HOLD 안전 |

### Codex HIGH 2건 처리

1. **HIGH #1 "설계 문서는 Phase 1b 미착수 전제인데 코드가 이미 존재"** → **기각**. Codex 게이트가 구현 **완료 후** 실행되어 worktree 산출물을 본 것으로, 설계 기준선 불일치가 아니라 **게이트 실행 시점 artifact**. 코드 결함 아님.
2. **HIGH #2 "`analysis_table`이 첫 성공 결과에만 주입 → 소비자가 매 원소에 있다고 가정하면 누락/파싱 오류"** → **수용**. 설계는 유지(top-level 승격 = job result shape breaking change로 프론트 `map`·DB `policy_analyses.result`·export 3곳 동시 파괴 → task의 "Surgical" 제약 위반). 대신 `docs/analysis-table-contract.md`(209줄)로 응답 위치·`find()` 패턴 필수·**순회 합산 금지**를 명문화. 동반 MEDIUM(이중 소스 드리프트)도 같은 문서에서 `analysis_table`=표 전용 / `coverages`=개별 카드 전용 역할 분리로 해소.

MEDIUM(이전 분석 오해) → 프론트에 `includePrevious` 시 "이번 분석 증권만 반영" 고지 추가로 해소. LOW(실패 무음 흡수) → `insuro.policy_analysis_table` 로거 warning + 계약 문서에 모니터링 지침 기재.

---

## 완료 절차 (finish-task.sh)

- `.done` 생성 (수동 생성 아님 — `finish-task.sh` 경유), `.anu-notified`, `.completion.txt` 생성
- `task-timer end` → **completed**, 소요 **1시간 21분** (4,906초)
- **FINALIZE_ONLY=1** — 머지 금지 모드로 실행(HOLD 준수). `--action pr`도 task-2467로 자동 머지가 폐기됨
- result.json: `memory/events/p0b_inbox/task-2957.result.json` — `trip_wire` 5종 기록(`verify → passed: True, violations: []`), `callback_schedule_created: false`, `report_path` 명시, `pr_number: 218`, `merge_status: HOLD_pending_anu_and_chairman`
  - ⚠️ result.json이 프로젝트 경로에만 기록되는 알려진 이슈 → 워크스페이스 `p0b_inbox`로 복사 보정
- **ANU normal callback**: `dispatch.normal_fallback_callback_helper.launch_callback` 경유(자가발사 아님) → `ANU_OWNED_READY` → cron **`83BD78C3`** 등록·발사 완료
  - 검증 `assert_normal_callback_actually_registered` → **verdict: PASS** (`schedule_id`/`schedule_history`/`owner_key`/`inbound_receipt` 4/4 PASS)
  - schedule_history 실측: `status: ok`, `chat_id: 6937032012`, `duration_ms: 82736`
  - envelope-only 414 bytes (≤3900 한도), owner = ANU key(executor self-key 아님)

### 완료 절차 중 발견·해결한 문제 (2건)

1. **GOAL-GATE `npm run build` FAIL** — 게이트가 **workspace cwd + 30초 타임아웃**으로 실행해 실패. 워크트리 cwd에서 동일 명령 재현 → **rc=0, 14.65초 PASS** 확인 후 워크트리 cwd + `GOAL_CMD_TIMEOUT=300`으로 재실행하여 해소. (빌드 자체는 정상이었고 게이트 실행 컨텍스트 문제)
2. **CODEX-GATE 결과 파일 미발견** — `--workspace-root /home/jay/projects/InsuRo` 지정 때문에 결과가 `projects/InsuRo/memory/events/`에 생성됐으나 `finish-task.sh`는 `workspace/memory/events/`를 조회. 파일 복사로 해소 → `[CODEX-GATE] Codex 사전검증 결과 파일 존재: PASS`

---

## trip-wire 5종 (실측)

| 항목 | 실측 | 근거 |
|---|---|---|
| Critical7 | **0** | red-team 스캔 취약점 0건 + code-validator Security PASS |
| PII net-new | **0** | 신규 diff에 PII 패턴 0건. `PolicyColumn.source_file`/`insured_key`는 **타입 선언만 존재, UI 미렌더**(grep 확인). `insured_key`는 소스에서 이미 `insured_name_masked` |
| 회귀 실패 | **0** | 백엔드 신규 실패 0(범위 외 1건은 clean worktree 교차검증으로 분리), 프론트 1187/1187 |
| forbidden_paths 침범 | **0** | 변경 7파일 전부 배정 저장소(InsuRo) 내. 타 팀 디렉토리 0건. ※ 본 task는 `--allow-no-scope`(legacy)로 dispatch되어 `allowed_resources` 미정의 |
| nonce | **task-2957** | 일치 |

---

## 발견 이슈 및 해결

### 자체 해결 (4건)

1. **`build_analysis_table` 입력 타입 불일치 — 어댑터 부재** — `analyze-policy`는 `policy_analyzer`의 AI dict만 갖고 있고 `ExtractResult`가 없었음. 확정 설계대로 `policy_extract.extract_policy(tmp_path)`를 `os.unlink` **이전** 구간에서 직접 호출하도록 배선(어댑터를 지어내지 않음).
2. **직렬화 헬퍼 부재** — `AnalysisTable`에 `to_dict()`가 없는데 `policy_normalizer.py`는 수정 금지. 신규 모듈 `policy_analysis_table.py`에 `dataclasses.asdict()` 기반 직렬화를 분리(main.py 8,700줄에 로직 매몰 방지 + 테스트 가능성 확보). `json.dumps()` 통과를 테스트로 고정.
3. **계약 문서 엔드포인트 경로 오기** — 문서 초안이 `/api/policy/analyze-policy`로 기재. `grep 'app.post("/api/.*analyze-policy' server/main.py` → 실제는 `/api/insuro/analyze-policy`(`:6975`). 팀장이 직접 정정·커밋(`61919ba`).
4. **`includePrevious` 토글이 표에 영향 없음 → 사용자 기만 위험** — 표는 이번 배치 증권만 병합하므로 과거 분석과 섞으면 출처 오염. 토글은 유지하되 켜졌을 때 "이 표는 이번 분석에 업로드한 증권만 반영합니다 (이전 분석 N건은 미포함)" 고지를 표 위에 노출.

### 범위 외 미해결 (3건 — 회장/ANU 판단 필요)

1. **★ image PDF 시 AI 호출 2배** — 범위 외 사유: 비용/아키텍처 결정 사항. `extract_policy()`는 `doc_format=="image"`(또는 textlayer 담보 0건 → "mixed" 폴백)일 때 `AnuVisionClient`가 **별도 claude CLI 프로세스**를 띄운다. 기존 `run_policy_analysis`(claude CLI sonnet, 타임아웃 120초)와 합쳐 **파일당 AI 호출 2회**. textlayer 정상 파싱 건은 추가 호출 없음. → **완화(캐싱/조건부 스킵/vision 결과 공유) 여부는 회장 결정**.
2. **`analysis_table` top-level 승격** — 범위 외 사유: breaking shape change로 task의 "Surgical" 제약 위반. 후속 별도 task 권고.
3. **`recommended_total` 전부 `"UNKNOWN"`** — 범위 외 사유: 권장가입금액 DB 미연결(task 명시 범위 밖). 현재 표의 해당 열은 정직하게 전부 UNKNOWN으로 표시됨.

### 검증 못한 항목 (정직 기재)

- **실제 Supabase 로그인 + 실 PDF 업로드 왕복 미검증**. E2E는 route stub(백엔드가 **실제로 산출한** 픽스처 주입) + localStorage 세션 주입으로 대체. 따라서 `extract_policy` → `build_analysis_table` **실증권 end-to-end 왕복은 미검증**이며, 표시 계층과 계약 정합성만 검증됨.
- `policy-analysis-status` 404 경로 미검증(인증 우회 불가) — 401을 실측 그대로 보고.
- **정확도 과장 금지**: task-2943 P0 실측 recall **73.9%**(원문 실손담보 부재 갭). 미분류 버킷·UNKNOWN 셀은 이 알려진 한계로 **정상 발생**한다.

---

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 비고 |
|---|---|---|---|
| 토르(Thor) | 백엔드 배선 + 계약 문서 | sonnet | 코딩 작업 |
| 프레이야(Freyja) | 프론트 대구분 표 | sonnet | 코딩 작업 |
| 헤임달(Heimdall) | E2E/회귀 검증 | sonnet | 분석 작업 — haiku 금지 대상 |
| Explore ×2 | 구조 선행 파악 | (기본) | 읽기 전용 |
| 오딘(Odin) | 설계/계약확정/검토/통합 | opus | **직접 코딩 없음** (문서 오기 1줄 정정만) |

미미르(UX/UI) 미소집 — 표 스펙(열 구성·순서·표시 규칙)이 codex↔ANU 합의로 확정되어 UX 설계 여지 없음.

---

## 3문서

`memory/plans/tasks/task-2957/` — plan.md(status: **completed**) · context-notes.md(**3 Step Why A-B-C** 기록) · checklist.md(전 항목 [x])

---

## 머지 판단

- **머지 필요**: Yes — 단 **HOLD**
- **브랜치**: `task/task-2957-dev2`
- **워크트리**: `/home/jay/projects/InsuRo/.worktrees/task-2957-dev2`
- **머지 의견**: 확정 설계 3파일 diff 0줄, job result shape 무변경, 개별 카드 탭 무변경으로 **회귀 표면이 매우 좁다**. 신규 코드는 전 구간 try/except로 감싸 실패해도 기존 `meta`/`coverages`/`silson_summary` 흐름에 무영향(테스트로 고정). 다만 task 지시가 **"머지·배포 HOLD — ANU 검증 + 회장 보고 후"**이므로 팀장 자동 머지하지 않음.
- **머지 후 활성화 주의**: 머지 ≠ 활성화가 아니다. 이번 건은 **머지 즉시 런타임 경로가 활성화**된다(`analyze-policy` 호출 시 `extract_policy`가 실제로 실행됨). 위 "image PDF AI 호출 2배" 이슈가 **비용에 즉시 반영**되므로, 회장 결정 전 배포 금지.

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

---

## 머지 실행 결과 (2026-08-15 03:33 UTC · 오딘/dev2 추가 기록)

**HOLD 해제 근거**: ANU 독립 검증 전항목 PASS + 회장 머지 승인.

| 항목 | 값 |
|---|---|
| 머지 여부 | **merged = true** (PR state=closed) |
| 방식 | **squash** (경로: `gh api -X PUT pulls/218/merge`) |
| sha 핀 | `1f92a75cef778c57710b6ca598d76564b2974544` (핀 일치, 재검증 후 머지) |
| merge commit | `1d424a9da758c4b4b51704e83ce6c6dedddb5e56` |
| origin/main head | `1d424a9da758c4b4b51704e83ce6c6dedddb5e56` — **merge commit과 동일** |
| parent | `6eeb5b6845e1b94fae07357c38b2f2131bfc178e` (단일 부모 = squash 확인) |
| merged_by | `jeon-jonghyuk-taskctl-bot[bot]` |
| CI (check-runs) | **11/11 success** (머지 직전 head sha 기준 재확인) |
| 변경 규모 | 7파일 +1225/−93 (PR 원안과 동일) |
| 머지 중 코드 수정 | **0줄** |
| force push | **없음** |
| harness 무력화 | **없음** (인가된 봇 GITHUB_TOKEN API 경로) |
| 배포 | **미수행** — 회장 별도 go 대기 |

머지된 파일 7종 (main 커밋 실측):
`docs/analysis-table-contract.md` · `server/main.py` · `server/policy_analysis_table.py` · `server/tests/test_policy_analysis_table.py` · `src/components/policy/CoverageAnalysisTable.tsx` · `src/pages/PolicyAnalysis.tsx` · `src/pages/__tests__/fixtures/analysis-table-e2e.json`

**★ 잔여 리스크 (머지로 해소되지 않음)**
1. **머지 = 즉시 활성화** — 이번 건은 t2947/t2955와 반대로, 머지된 코드가 `analyze-policy` 런타임 경로에 바로 들어간다. 실제 반영 시점은 **서버 재기동/배포 시**이므로, 배포 전까지는 현행 동작 유지.
2. **image PDF AI 호출 2배** — 배포 시 vision 240s + 분석 120s, 순차 처리 3장 최악 ~18분. 비용·지연 즉시 반영.
3. **권장가입금액 열 전 UNKNOWN** — 기준 DB 미연결. 후속 과제.
4. **실증권 E2E 미검증** — 테스트는 픽스처 stub 기반. 배포 전 실제 증권 파일럿 권장.

이벤트 마커: `memory/events/task-2957-merge.done`

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

