# task-2955 — InsuRo 실손 세대지식 설계사 노출 MVP (silson_summary 배선)

- **팀**: dev2-team (오딘)
- **일자**: 2026-08-15
- **레벨**: Lv.3 / critical
- **PR**: [#216](https://github.com/Jeon-Jonghyuk/InsuRo/pull/216) — head `b610ae6e` · base `main`(`2ccc121`) · **머지 HOLD**
- **브랜치**: `task/task-2955-dev2` / worktree `/home/jay/projects/InsuRo/.worktrees/task-2955-dev2`

---

## SCQA

**S**: silson 실손 세대지식은 PR#210~#214(task-2947·2949·2952·2953)로 4차례에 걸쳐 표준약관 1차출처까지 정본화됐다. 그러나 **런타임 호출자가 0**이었다 — 지식은 저장소에 있지만 설계사는 볼 수 없는 휴면 상태였다.

**C**: 직전 task-2954가 **설계 미합의로 취소**(PR#215 미머지)됐다. 취소 시점 ANU 지적은 "설계사 화면에 내부 KB 식별자 원문이 그대로 렌더된다"(자기부담금 셀 1,355자 · `ref` 13개 · 카드 높이 4,262px)였다. 또한 세대판별의 정식 입력(`coverage_start_date`/`contract_date`)이 현재 분석 결과에 존재하지 않는다는 구조적 제약이 있었다.

**Q**: 합의된 스키마를 축자 준수하면서, ① 내부 식별자 노출 재발 없이 ② 부정확할 수 있는 날짜 proxy를 확정판정처럼 보이지 않게 배선할 수 있는가?

**A**: codex↔ANU 합의 확정본 그대로 구현했다. `meta.enrollment_date`를 ISO 정규화해 세대판별 입력으로 쓰고 `date_source="enrollment_date_proxy"`로 표기하며, 표시 계층은 **화이트리스트 렌더**로 내부 식별자를 물리적으로 차단했다. 실브라우저 실측 결과 블록 높이 **238px**(이전 결함 4,262px), 금지 문자열 **0건**.

---

## 변경 파일 (6종 · +1,111 / -0 · 삭제 0)

| 파일 | 구분 | 내용 |
|---|---|---|
| `server/silson/analysis_summary.py` | 신규 198줄 | 확정 스키마 조립 순수 함수 (`normalize_enrollment_date` / `is_silson_relevant` / `build_silson_summary`) |
| `server/main.py` | 수정 +5 | import 1줄 + `results.append(validated)` 직전 부착 4줄 |
| `server/tests/test_silson_analysis_summary.py` | 신규 305줄 | 백엔드 34케이스 |
| `src/components/SilsonGenerationBlock.tsx` | 신규 242줄 | 세대 블록 + 화이트리스트 추출 헬퍼 |
| `src/pages/PolicyAnalysis.tsx` | 수정 +9 | 타입 2필드 + 블록 삽입 |
| `src/components/__tests__/SilsonGenerationBlock.test.tsx` | 신규 352줄 | 프론트 10케이스 |

**무변경 확인(diff 0건)**: `silson/data/**`, `gating.py`, `classify.py`, `knowledge.py`, `models.py`, `analysis_bridge.py` — 고객 자동판정 gating 경로 무변경.

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|---|---|---|---|
| /home/jay/projects/InsuRo/.worktrees/task-2955-dev2/server/silson/analysis_summary.py | 확정 스키마 조립 순수 함수 신규 | grep "build_silson_summary" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2955-dev2/server/main.py | analyze-policy 부착 배선 | grep "silson_summary" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2955-dev2/server/tests/test_silson_analysis_summary.py | 백엔드 34케이스 | grep "normalize_enrollment_date" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2955-dev2/src/components/SilsonGenerationBlock.tsx | 세대 블록 + 화이트리스트 추출 | grep "formatItemLabel" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2955-dev2/src/components/__tests__/SilsonGenerationBlock.test.tsx | 프론트 10케이스 | grep "SilsonGenerationBlock" OK | 완료 |
| /home/jay/projects/InsuRo/.worktrees/task-2955-dev2/src/pages/PolicyAnalysis.tsx | 타입 2필드 + 블록 삽입 | grep "silson_summary" OK | 완료 |

---

## 구현 요약

### 합의 스키마 축자 준수
최상위 키 정확히 8종(`generation_id, generation_label, effective_period, date_source, key_coverages, sources, verification_notice, disclaimer`). 금지 필드(`mri`/`outpatient_limit`/`inpatient_limit`/`three_nonpayment`) 신설 0건. `coverage_limit`은 원문형 단일 문자열 유지.

### 세대판별 입력 (최대 리스크 항목)
- `meta.enrollment_date` 형식은 `"YYYY. M. D"`(공백·비제로패딩)인데 silson 파서 `SUPPORTED_DATE_FORMATS`는 이를 **지원하지 않는다** → 정규화 없이는 100% 판별 실패. `normalize_enrollment_date()`가 필수 부품인 이유.
- 연-월만 있는 경우(`"2018. 5"`)는 **추측 없이 판별 불가 처리**.
- 부정확성 표기 3중: 필드값 `enrollment_date_proxy` + 화면 캡션 + 고정 disclaimer.

### 내부 식별자 차단 (재발 방지 핵심)
`deductible`/`payout_rate` raw는 세대별 185~1,390자이며 내부에 `plan_type.*.{value, source_id, ref}`를 포함한다. API 계층은 합의 스키마를 축자 유지하고, **표시 계층에서 `.value`·평면키·`원문`만 화이트리스트 추출**한다. 추출 헬퍼는 `source_id`/`ref`에 **접근하는 코드 자체가 존재하지 않도록** 작성했다.

---

## 발견 이슈 및 해결

| # | 이슈 | 발견 경로 | 해결 |
|---|---|---|---|
| 1 | 판별 실패 사유에 내부 영문 코드(`unparsable_date` 등)가 설계사 화면으로 노출 | 팀장 코드 리뷰 | `_REASON_MESSAGES` 4종 한국어 매핑 + fail-safe fallback (`8a1f898`) |
| 2 | 내부 OCR 스키마 필드명 `입원_급여` 화면 렌더 | **팀장이 스크린샷 직접 열람** | `formatItemLabel()` 표시용 정규화 (`7aa1a05`) |
| 3 | 미사용 import/상수(죽은 코드) | Pyright | 제거 (`7db0b0a`) |
| 4 | `meta.insurer` 키 불일치 (실제는 `insurance_company`) → 항상 None | Codex 게이트 low | `_meta_insurer()` 우선 매핑 (`43eae32`) |
| 5 | 프론트 회귀 테스트 부재 | Codex 게이트 medium | UI 테스트 10케이스 신규 (`1a02764`) |
| 6 | 스크린샷이 수정 커밋보다 4분 먼저 촬영(증거 신선도) | 마아트 독립 검증 | `-v2` 재촬영 후 팀장 직접 육안 확인 |

**교훈**: 이슈 1·2는 grep·테스트·빌드를 모두 통과한 상태에서 **렌더 결과를 사람이 직접 봐야** 잡혔다.

### 미해결 (범위 외 · ANU 판단 요청)
- **연-월만 있는 가입일**: 현재는 판별 불가. 후속 제안 — 해당 월 1일과 말일이 같은 세대로 판정되면 그 달 어느 날이어도 결과가 동일하므로 확정 가능(추측 아님). 경계에 걸치면 불가. 본 MVP 범위 밖.
- **프론트 분기 미세 불일치**: `silsonSummary===null` + `unavailableReason` 없음 → 기본 문구 노출(아무것도 렌더 안 함이 아님). 백엔드 계약상 `summary=None`이면 항상 비어있지 않은 사유가 함께 오므로 **실제 발생하지 않는 분기**. 제품 코드 미수정, 테스트는 실동작 기준으로 작성.

---

## ★ 합의문에 없던 팀장 추가 판단 2건 (ANU 승인 대상)

1. **실손 관련성 게이트** (`is_silson_relevant`) — `attach_generation`은 날짜만으로 판정하므로 종신·암보험 증권에도 "3세대 착한실손" 라벨이 붙는다. 실손 담보(`category=='실손'`) 또는 상품명/분류에 '실손'이 있을 때만 부착하고, 아니면 `null` + 사유.
2. **판별 실패 사유 한국어 매핑** — 내부 코드를 설계사 화면에 노출하지 않기 위함.

두 건 모두 **합의 스키마 8키를 건드리지 않고**, 합의된 "부재 → null + 사유" 메커니즘 안에서의 보정이다. 마아트 독립 검증 의견도 **"합의 위반이 아니라 필요한 안전장치"**(2건 모두 타당).

---

## 테스트 결과

| 항목 | 결과 |
|---|---|
| 백엔드 회귀 (**clean worktree, CI parity**) | **1806 passed / 0 failed / 3 skipped** |
| 백엔드 회귀 (봇 worktree) | 1806 passed / 1 failed — 실패 1건은 worktree 루트의 **비추적 `.env` 오염**(`test_cors_fail_closed_when_ext_origin_unset`)이며 clean 트리에서 소멸함을 마아트가 재현·확증 |
| 신규 백엔드 테스트 | 34 passed |
| 프론트 전체 (`npm test`) | 80 files / **1187 passed**, 신규 실패 0 |
| 신규 프론트 테스트 | 10 passed |
| `npx tsc --noEmit` | 에러 0 |
| `npm run build` | 성공 (14~15s), `dist/` 갱신 |
| 레드팀 스캔 (백/프론트) | risk **low**, 취약점 0 |
| PII net-new | **0건** |

> `code-validator.py` 의 "Execution Test ❌"는 sys.path 아티팩트다 — **이미 main에 머지된 `analysis_bridge.py`(PR#214)도 동일하게 실패**함을 대조 확인했다(오탐).

---

## L1 스모크테스트

- **서버 재시작**: 해당없음 — 백엔드 변경은 순수 함수 + 부착 2줄이며, analyze-policy 실호출은 실증권 PDF(PII)와 유료 AI 호출이 필요해 실행하지 않음. 대신 **실제 `build_silson_summary()` 를 4세대(GEN1/GEN2Ⅱ/GEN3/GEN5)에 대해 직접 실행**해 payload 를 생성하고, 그 산출물을 E2E 입력·테스트 픽스처로 사용했다.
- **API 응답 확인**: 실브라우저 E2E에서 `/api/insuro/analyze-policy` 및 상태조회 응답을 **실제 백엔드가 생성한 payload**로 목킹해 `PolicyAnalysis` 실페이지(`/crm/policy-analysis`)에 렌더 — 업로드 → "일괄 분석 시작" → 결과 표시까지 실사용자 플로우 재현.
- **스크린샷**:
  - `/home/jay/workspace/memory/reports/screenshots/task-2955-silson-block-GEN3-v2.png` ← **최신·최종 확인본**
  - `…-GEN3.png` / `…-GEN3-expanded.png` / `…-GEN1.png` / `…-ALL.png` (1차, 이슈 2 수정 전)
- **실측**: 블록 접힘 높이 **238.25px**(기준 ≤320px) · 금지 문자열(`source_id`/`ref`/`KB\d`/내부 reason 코드) **0건** · 언더스코어 필드명 **0건** · 필수 문구 3종 존재 · 콘솔 에러 0건(기존 제3자 스크립트 경고만)
- **브라우저 정리**: `browser_close` 호출 완료, chrome 프로세스 감소 확인

---

## 게이트 결과 (Lv.3)

| 게이트 | 결과 |
|---|---|
| G1 — 3문서 | 작성 완료 (plan/context-notes/checklist + ui-spec) |
| G1 — Codex 사전 검증 | **PASS** (critical 0 / medium 2 / low 1) — 3건 전부 대응 완료 |
| G1 — 3 Step Why (A-B-C) | context-notes.md 기록, 일관성 확인 |
| G1 — sanitize | 외부 AI(Codex)에 전달된 코드에 PII 0건 (net-new 스캔 0) |
| G2 — 마아트 독립 검증 | **PASS** (V-1~V-6, clean worktree 재실행 대조 포함) |
| G2 — Gemini | 미적용 (sunset, `gemini-review-gate` check success) |
| G3 — 머지 | **HOLD** (task 지시) |

---

## trip-wire 5종 (실측)

| 항목 | 실측치 | 판정 |
|---|---|---|
| Critical7 | 0 | PASS |
| PII net-new | 0 | PASS |
| 회귀 실패 | 0 (clean worktree) | PASS |
| forbidden_paths 침범 | 0 | PASS |
| nonce | task-2955 | PASS |

---

## 머지 판단

- **머지 필요**: Yes (단, **정책상 HOLD**)
- **브랜치**: `task/task-2955-dev2` (원격 `b610ae6e`)
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2955-dev2`
- **머지 의견**: 기술적으로는 머지 가능 상태다. 합의 스키마 축자 준수·surgical 변경(삭제 0)·clean worktree 회귀 0·PII 0·실브라우저 재발방지 실측 통과를 모두 독립 검증으로 확인했다. 다만 task 지시에 따라 **ANU 검증 + 회장 보고 후 머지**, 실배포는 **별도 회장 go** 필요.
- **배포 시 주의**: 머지만으로는 활성화되지 않는다. 서버(`insuro-api.service`) 재기동 + 프론트 빌드/CF Pages 배포가 있어야 설계사 화면에 반영된다.

## 모델 사용 기록

| 팀원 | 모델 | 비고 |
|---|---|---|
| 토르(백엔드) | sonnet ×4 / **haiku ×1** | haiku는 "미사용 import/상수 제거"(단순 삭제 + grep 확인)에만 사용 — 판단 요소 없는 기계적 작업 |
| 프레이야(프론트) | sonnet ×2 | |
| 미미르(UX/UI) | sonnet | 표시 규칙 명세 — 설계 작업이라 haiku 금지 대상 |
| 헤임달(QA) | sonnet ×2 | E2E·테스트 설계 |
| 마아트(횡단 독립검증) | sonnet | task-timer cross-start/end 로깅 완료 |

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



---

## 병합 실행 결과 (2026-08-15, 오딘/dev2)

- **승인 근거**: ANU 독립검증 PASS + 회장 승인 (머지 HOLD 해제)
- **병합 전 재확인**: PR #216 `state=open`, `mergeable=true`, `mergeable_state=clean`, head `b610ae6e35abc076c828f44a16631eeee62f3b7a`, `+1111/-0`
- **CI**: check-runs `total_count=11`, 전부 `completed/success`
  (cancel-kill-switch, ci, ci/guard, diagnostic, e2e-test, gemini-review-gate, guard, hidden-path-audit, lock-in-check, merge-safety-check, qc-check)
- **병합 방식**: `PUT /repos/Jeon-Jonghyuk/InsuRo/pulls/216/merge`, `merge_method=squash`, `sha` 핀 = `b610ae6e35abc076c828f44a16631eeee62f3b7a` (force 미사용, 코드 수정 0)
- **결과**: `merged=true`, `merged_at=2026-08-14T19:20:14Z`, `state=closed`
- **merge commit**: `6eeb5b6845e1b94fae07357c38b2f2131bfc178e`
- **origin/main head**: `6eeb5b6845e1b94fae07357c38b2f2131bfc178e` (merge commit과 동일 — 반영 확인)
- **배포**: 수행하지 않음. 회장 별도 go 대기 (서버 `insuro-api.service` 재기동 + 프론트 빌드/CF Pages 배포 필요)
- **주의**: 머지 ≠ 활성화. 현재 상태는 origin/main 반영까지만이다.

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

