# task-2881 보고서 — 생보 3대질병 Phase B: 웹앱 차액 계산·표시

- 팀: dev1-team (헤르메스) · 레벨: Lv.3 · merge_policy: none (머지=ANU)
- worktree: `/home/jay/projects/InsuRo/.worktrees/task-2881-dev1`
- branch: `task/task-2881-dev1` · base: `93882f0`(origin/main, #155 Phase A life_matrix 계약 포함)

## S — Situation (상황)
Phase A(task-2880, #155 머지)로 상담 레코드에 **life_matrix**(생보 3대질병 담보×생보사 격자) 저장 계약이 완료되고, 서버 read 엔드포인트(`GET /api/insuro/consultation-history/v1`)의 `_ALLOWED_RESPONSE_FIELDS`에 `life_matrix`가 포함되어 웹앱이 이 데이터를 수신 가능한 상태였다.

## C — Complication (복잡)
웹앱은 아직 life_matrix를 소비하지 않았다. 3사 손보 계단식 결과만 표시되고, 회장이 원하는 **생보 3대질병(암진단비·뇌혈관질환진단비·허혈성심장질환진단비) 손보-생보 차액**이 어디에도 없었다. 제약: 서버/확장 무변경(webapp-only), "절감" 단어 금지(차액만), 반올림 금지(exact 원), 기존 계단식·인포그래픽·신뢰뱃지 불변, e2e strict-mode 회귀 방지.

## Q — Question (질문)
life_matrix + 기존 손보 복합설계 결과(`coverages[].premium`)로부터 3대질병 차액을 계산해, 3사 계단식 결과 **아래**에 계단식 톤으로 표시하되 기존 동작·서버를 건드리지 않고 회귀 0으로 붙이는 방법은?

## A — Answer (답)
**경량 순수 헬퍼(lib) + 순수 렌더 컴포넌트 + 최소 배선** 3계층으로 분리. 고테스트 자산인 RpPresentation(781줄)은 무수정, ConsultationHistoryView의 success 분기에 `<RpPresentation>` 바로 아래로 신규 섹션을 형제 삽입(=3사 결과 아래).

### 데이터 흐름 (실측 근거)
- **손보 배정가**: `state.data`(CompositeMatrixResponse) = `composite-calculate-from-matrix`가 손보 analysis_matrix로 계산한 결과. `triple_best ?? dual_best ?? single_best`의 `coverages[].premium`을 담보명 매칭으로 취득.
- **생보 최저가**: `record.life_matrix.coverages[].premiums`에서 담보별 argmin(전 생보사 중 최저), 승자 key→회사명 해석.
- **차액 = Σ 담보별 (손보 배정가 − 생보 최저가)**, exact(반올림 없음), 음수 그대로.
- **담보명 매칭**: `resolveCoverageName`(task-2864 정상화) 후 공백 제거 정규화명에 keyword(암진단/뇌혈관/허혈성심장) 포함 여부. 정확 일치 우선.

### 생성/수정 파일 (6, +985줄, 전부 additive)
- `src/lib/lifeThreeCritical.ts` (신규, 238줄) — `computeLifeThreeCriticalDiff(lifeMatrix, composite, coverageNameMap?) → LifeThreeCriticalResult | null`. 담보별 argmin·손보 배정 매칭·exact 차액. composite_calculator 재사용 없음. life_matrix 없거나 매칭 0담보 → null(placeholder 신호).
- `src/lib/__tests__/lifeThreeCritical.test.ts` (신규, 16 tests) — 정상3담보/부분2담보/손보미매칭/null/dual fallback/exact홀수원(2,346)/음수차액/공백담보명.
- `src/components/composite/LifeThreeCriticalDiff.tsx` (신규, 114줄) — 순수 렌더. null→접힌 `<details>` 플레이스홀더("생보 3대질병도 캡처하면 차액 확인 가능"), 있으면 bg-sky-600 헤더 표(담보|손보 배정|생보 최저|차액) + text-2xl/3xl 큰 총액 "생보 3대질병 복합 시 차액 XXX원" + N년납 총액 병기. exact 원(`toLocaleString("ko-KR")`), "절감" 0.
- `src/components/composite/__tests__/LifeThreeCriticalDiff.test.tsx` (신규, 5 tests) — 정상3담보/절감부재/null플레이스홀더/exact(2,346)/20년납*240.
- `src/components/composite/ConsultationHistoryView.tsx` (수정, +12줄) — `ConsultationRecord`에 `life_matrix?` optional 추가, import, `<RpPresentation>` 바로 아래 `<LifeThreeCriticalDiff>` 배선.
- `tests/e2e/composite-design-vpath.spec.ts` (수정, +151줄) — 케이스 C 신규(실브라우저 렌더 검증).

## 수정 파일별 검증 상태

> 경로는 worktree 절대경로(외부 프로젝트 InsuRo). base=`/home/jay/projects/InsuRo/.worktrees/task-2881-dev1`.

| 파일 | 변경 | grep 키워드 | 검증 |
|---|---|---|---|
| /home/jay/projects/InsuRo/.worktrees/task-2881-dev1/src/lib/lifeThreeCritical.ts | 신규 | computeLifeThreeCriticalDiff | tsc clean + vitest 16 pass |
| /home/jay/projects/InsuRo/.worktrees/task-2881-dev1/src/lib/__tests__/lifeThreeCritical.test.ts | 신규 | computeLifeThreeCriticalDiff | 16 passed |
| /home/jay/projects/InsuRo/.worktrees/task-2881-dev1/src/components/composite/LifeThreeCriticalDiff.tsx | 신규 | life-three-critical-total | tsc clean + vitest 5 pass |
| /home/jay/projects/InsuRo/.worktrees/task-2881-dev1/src/components/composite/__tests__/LifeThreeCriticalDiff.test.tsx | 신규 | life-three-critical-section | 5 passed |
| /home/jay/projects/InsuRo/.worktrees/task-2881-dev1/src/components/composite/ConsultationHistoryView.tsx | 수정 | LifeThreeCriticalDiff | vitest 15 pass (회귀 0) |
| /home/jay/projects/InsuRo/.worktrees/task-2881-dev1/tests/e2e/composite-design-vpath.spec.ts | 수정 | life-three-critical-section | e2e 3 passed |

## 테스트 결과
- **tsc --noEmit**: CLEAN (에러 0).
- **vitest run**: **1066 passed / 72 files** (baseline 1045/70 → 신규 +21: lib 16 + component 5). **회귀 0**.
- **e2e (playwright, composite-design-vpath)**: **3 passed** — 기존 케이스 A/B 회귀 0(strict-mode 위반·텍스트 충돌 없음) + 신규 케이스 C PASS.
- forbidden_paths(server/extension/.github/src/config/api.ts/src/integrations) **무변경 확인**.
- "절감" 단어: 렌더 UI 문자열 0건(주석/테스트 설명만 잔존). 반올림 함수(Math.round/floor/ceil/toFixed) 0건.

## L1 스모크테스트 결과
- **서버 재시작**: 해당없음(webapp-only, 서버 무변경). 대신 Vite dev(`npm run dev`, :8080)를 Playwright webServer가 기동 → 실브라우저 렌더.
- **API 응답 확인**: e2e route mock으로 `consultation-history/v1`(life_matrix 포함 레코드) + `composite-calculate-from-matrix`(triple_best.coverages 3담보 배정) 주입 후 실렌더.
- **스크린샷**: `tests/e2e/screenshots/vpath/c-life-three-critical.png`(섹션 단독), `c-full.png`(전체). 스크린샷 직접 확인 — "생보 3대질병 진단비 비교" 헤딩 + 3담보 표(암진단비 테스트화재 30,000원 / 뇌혈관질환진단비 테스트화재 15,000원 / 허혈성심장질환진단비 제3화재 12,000원) + 생보 최저(행복생명/미래생명 파랑 강조) + 큰 총액 라인 렌더 확인.
- **결정적 차액 검증(실렌더 assert)**: 암 30,000-25,000=5,000 / 뇌혈관 15,000-13,000=2,000 / 허혈성 12,000-10,000=2,000 → **총액 9,000원**, 20년납(240개월) 총액 **2,160,000원**. 섹션 "절감" 0건.

## 발견 이슈 및 해결
- lib 1차 산출물에 미사용 파라미터(TS6133) + 표시명이 공백제거 정규화명이던 문제 → 불칸 재작업으로 시그니처 축소 + display(공백 유지) 분리 해결. tsc/vitest 재검증 통과.
- e2e 스크린샷 우측에 온보딩 툴팁("시작 가이드") 오버레이가 일부 겹침 — 테스트 환경의 무관 요소이며 섹션 렌더/데이터 정확성에는 영향 없음(assert는 testid 스코프로 통과).

## 머지 판단
- **머지 필요**: Yes (그러나 **merge_policy=none → 머지 주체=ANU**)
- **브랜치**: `task/task-2881-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2881-dev1`
- **머지 의견**: webapp-only 순수 additive(6파일, 서버/확장/config/integrations 무변경). tsc 클린, vitest 회귀 0(+21 신규), e2e 3 passed(회귀 0), 실브라우저 렌더 스크린샷 확보. 회장 제약(절감 금지·exact·계단식 톤·graceful 플레이스홀더) 전부 충족. 충돌 위험 낮음. **ANU 독립검증 후 머지 권장**. 후속(마이그레이션016 적용·서버재시작·확장빌드전송·CF Pages 배포)은 ANU가 Phase B 머지 시 일괄.

## 모델 사용 기록
- 불칸(백엔드/lib): sonnet — 계산 로직·정확도 요구.
- 이리스(프론트/컴포넌트·배선): sonnet — React·타입 정합.
- 아르고스(테스터/e2e): sonnet — Playwright fixture·assert 설계.
- 헤르메스(팀장): opus — 설계/분배/통합검증/스크린샷 인지검증. haiku 미사용.

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

