# [Lv.3] 웹앱 상담이력 재설계 — 코드→이름 + 테이블/매트릭스 구조 (Codex 조율 확정)

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "src/components/composite/CompositeComboResult.tsx"
    - "src/components/composite/ConsultationHistoryView.tsx"
    - "src/lib/coverageCatalog.ts"
    - "src/lib/**"
    - "src/**/__tests__/**"
    - "tests/e2e/**"
  forbidden_paths:
    - "server/**"
    - "extension/**"
    - ".github/**"
    - "src/config/api.ts"
    - "src/integrations/**"
  commands: ["npx vitest","npx tsc","git"]
  merge_policy: "none"
  ttl_hours: 16
```
> ★ merge_policy=none. base=현 origin/main. **webapp-only — 서버 변경 0**(Codex 확정: 필요 데이터 전부 서버 응답에 이미 있음). task-2861(서버전용)과 파일 무충돌 → 병렬 안전.

## 배경 (회장 파일럿 피드백 — "분석&도구 · 복합설계 계산기" 상담이력 화면)
현재 홍길동 42세·남·**F**·**000000111041** 헤더, 담보별 배정에 **a001/a017/a042/b002 코드** 노출, 선택담보/조합이 raw 나열. 회장 요구 = 코드→이름 + 테이블/매트릭스 구조화 + 요약 순서 재배치.

## 근본원인 (Codex 확정)
- 담보코드가 뜨는 진짜 원인: 서버 `analysis_matrix.coverages[].name`에 **코드값 자체**가 들어오고, `resolveCoverageName` 우선순위(`명시name > 카탈로그 > 코드`, coverageCatalog.ts:35-43)가 **코드값 명시name을 카탈로그보다 우선**시켜 카탈로그 이름을 무시함. 배지는 명시name 안 넘겨서 카탈로그로 이름이 뜸(CompositeComboResult.tsx:93 vs ConsultationHistoryView.tsx:145).
- 헤더 F=insurance_type raw, 000000111041=plan_id raw. `/api/insuro/composite-plans`가 plan_name("메리츠 The드림 종합보험")·plan_type_name 반환(서버변경 불필요, 웹앱-only 해석 가능).

## 작업

### A. 코드→이름 우선순위 수정 (coverageCatalog.ts `resolveCoverageName`) — 근본
- 우선순위 변경: **explicitName이 coverage_cd(코드)와 동일하거나 코드패턴이면 무시** → 카탈로그맵 우선. 즉 `유효한 사람이름 explicitName > 카탈로그맵 > 코드 fallback`.
- 판정: `explicitName && explicitName !== coverage_cd`(및 명백한 코드패턴 `^[a-z]\d{3}$` 제외) 일 때만 explicitName 채택.
- 결과: a001/a017/b002 → 카탈로그 이름. (a042 등 카탈로그 DB에 이름 없는 코드는 여전히 코드 — 별도 데이터 이슈, graceful. 담보별 배정 어디서도 코드값-명시name 때문에 코드가 뜨는 일 0.)

### B. 헤더 개선 (ConsultationHistoryView.tsx `QueryConditionBlock`)
- 현재 age세·gender·insurance_type(raw "F")·plan_id(raw "000000111041").
- 개선: **raw insurance_type/plan_id 제거**하고 대신 `/api/insuro/composite-plans` fetch → plan_id→plan_name 맵으로 **plan_name 표시**(예: "메리츠 The드림 종합보험"). plan_name 없으면 생략(끊기지 않게). insurance_type raw "F"는 표시하지 않음(원하면 plan_type_name).
- 결과: "홍길동 · 42세 · 남 · 메리츠 The드림 종합보험".

### C. 선택담보 → 테이블 (`SelectedCoveragesBlock`)
- Badge 나열 → `<table>`: 열 = **담보명 | 가입한도(만원)**. 담보명은 A의 resolveCoverageName. 가입한도=`amount`(만원).

### D. 1사 최저가 → 테이블 (CompositeComboResult 단일사 타일)
- 행=담보(같은 행=같은 담보). 열 = **담보명 | 가입한도 | 보험회사 | 월보험료**. 단일사라 회사=메리츠 반복 표기.
- **월보험료 합계 = 맨 아래 합계행**. 합계는 **서버 `single_best.total` 단일소스**(행 재계산 금지 — 불일치 방지).

### E. 2·3사 조합 → 테이블 + 요약 순서 재배치 (다중사 타일)
- 행=담보. 열 = **담보명 | 가입한도 | 선택회사 | 월보험료**. 회사는 담보별로 다름(메리츠/현대해상/농협 등).
- 담보표 위 요약 **순서 재배치**: **(위) 월보험료 합계 → (아래) 20년(240개월) 총 납입보험료 절감액**. 현재는 20년절감이 위·크게/월보험료 아래 → **뒤집는다**(회장: "월보험료 위, 20년절감 아래라야 구조가 안정적").
- 20년 총절감은 여전히 표시(강조 유지하되 위치만 아래). 합계 = **서버 `total` 단일소스**.
- 절감액 부호 정직표기 유지(>0 "절감" / =0 "1사와 동일" / <0 "1사보다 X원 비쌈"). **기존 카드 숨김 규칙은 변경하지 말 것**(레이아웃만, 의미변경 금지).

## 제약
- **webapp-only.** 서버/확장/마이그레이션 변경 0. 합계는 서버 total 단일소스.
- 가입한도 데이터는 서버 응답에 이미 존재(Codex 확인). 없는 담보는 "-" graceful.
- 기존 조합계산·vpath·삭제버튼(task-2860) 회귀 0.

## 검증
- `npx tsc` 클린. 컴포넌트/단위 테스트:
  - A: 코드값-explicit(a001)이 카탈로그 이름으로 해석 / 유효 explicit는 채택 / 카탈로그 없는 코드는 코드 fallback.
  - B: plan_name 렌더 / 없으면 생략 / raw insurance_type 미노출.
  - C·D·E: 3 테이블 구조(thead/tbody/행=담보/열 정확) / 합계=서버total / 20년절감 월보험료 아래 배치 / 부호 정직.
  - 기존 조합/vpath e2e 회귀 0.

## 완료 (★ 순서 B)
- 변경 = src/ (+tests/e2e) 만. 서버/확장 불변.
- **dev6 금지** — dev2. 커밋 → push → finish-task **foreground 1회** → .done 즉시 종료. **background+wait 금지.** ANU 검증·Codex 검토·머지·CF Pages 배포는 ANU.

## 후속(비차단, 발주 안 함 — 기록만)
- a042 등 `ohmy_coverages` 카탈로그 DB에 이름 없는 코드 → DB 카탈로그 확장 필요(별도 데이터 태스크). 이번 웹앱 수정으론 코드 fallback으로 graceful.
- 장기: 서버 `composite_calculator.py` analysis_matrix 이름 품질 보장(근본), 이번엔 웹앱 방어로 충분.

## goal_assertions (auto-generated)
- `npx tsc`
