# task-3046 — 신규설계 비교를 고객중심 흐름으로 재구성

**팀**: dev3-team (다그다) · **레벨**: Lv.3 · **작성**: 2026-08-28
**저장소**: `Jeon-Jonghyuk/InsuRo` · **브랜치**: `task/task-3046-dev3` · **HEAD**: `b940348`
**PR**: **#266 draft OPEN** (base `main`) — ★ 머지는 ANU. 자동 머지 금지.
**worktree**: `/home/jay/projects/InsuRo/.worktrees/task-3046-dev3`

**S**: 신규설계 비교 화면이 기존 증권을 두 번 업로드하게 유도하고 있었다.
**C**: 고객을 선택하면 저장된 증권분석이 있는데도 화면이 그것을 전혀 불러오지 않았다(`customer-policy-analyses` 참조 0건).
**Q**: 재업로드를 없애고 고객 중심 흐름으로 재구성할 수 있는가?
**A**: 재구성 완료. 재업로드 UI·핸들러·API 호출을 전부 제거하고 봉인 테스트로 못을 박았다.

## S — 상황

회장님 확정 흐름(2026-08-26):
1. 기존 증권분석 메뉴에서 고객 증권·약관을 올려 현재 상황을 정리한다.
2. 신규 설계안 비교는 두 역할 — (1) 설계안끼리 비교해 최적안 선택, (2) 최적안 + 기존 해지분을 종합해
   **1번과 비교한 보장 변화 + 보험료 합계 변화**를 고객 관점으로 정리.

지적: "지금은 **기존 증권을 두 번 업로드하게 유도**하고 있다. A고객을 선택하면 DB에 저장돼
두 번 업로드할 필요 없어야 하는데 정리가 안 돼 있다."

## C — 복잡성

- 화면이 `analyze-policy` 를 직접 호출해 증권을 **재업로드**시키고 있었다(`:65`, `:265`).
- 고객명을 **자유 입력**으로 받고 있었다("증권 분석 API 가 고객명을 필수로 요구합니다").
- 서버 비교 엔진은 봉인 영역이라 손댈 수 없다.
- 공식 비교표가 0행으로 나올 수 있는데(지식DB 등록률), 그 정직 고지가 화면에 도달하지 않고 있었다.

## Q — 질문

재업로드를 제거하고 고객 선택 → 자동 로드 → 설계안 비교 → 해지 선택 → 최종 종합을 만들 수 있는가?

## A — 답변 / 결과

### 완료 조건(DoD) 대비

| DoD 항목 | 상태 | 증거 |
|---|---|---|
| 고객을 선택하면 기존 증권분석이 자동으로 불러와진다 | 완료 | 스크린샷 `03-analysis-loaded.png`, 봉인테스트 2번 |
| 재업로드가 없다 | 완료 | grep 5종 전부 0건, 봉인테스트 4건, 변이검증 |
| 신규 설계안끼리 비교해 최적안을 고른다 | 완료 | `06-plan-compare.png` (2건 나란히 + 라디오 선택) |
| 해지분을 선택해 최종 보장·보험료 변화를 본다 | 완료 | `07-cancellation.png`, `08-final-synthesis.png` |

### 재업로드 제거 실증 (grep 실측, 모두 0건)

| 대상 | `src/pages/NewDesignComparison.tsx` 잔존 |
|---|---|
| `existing-policy-upload` (증권 업로드 필드) | 0 |
| `runExistingUpload` (업로드 핸들러) | 0 |
| `ANALYZE_POLICY_PATH` | 0 |
| `POLICY_ANALYSIS_STATUS_PATH` | 0 |
| `existing-customer-name` (고객명 자유입력) | 0 |

`증권 분석 시작` 문자열 1건이 남아 있으나 **삭제 경위를 설명하는 주석**(`:542-545`)이며 UI 컨트롤이 아니다.

### 봉인 테스트 + 변이 검증 (팀장이 직접 재현)

`src/pages/__tests__/NewDesignComparison.noReupload.seal.test.tsx` (4건)

| 단계 | 실측 결과 |
|---|---|
| 변이 전 | 4 passed |
| 변이 주입(재업로드 input 부활) | **3 failed / 1 passed** |
| 변이 원복 | `git status` 구현파일 변경 0, grep 0건 |
| 원복 후 | 4 passed |

3번 케이스(`analyze-policy` 미호출)만 변이 하에서 통과했다. 정적 input 추가는 fetch 를 만들지 않으므로
설계상 정상이며, 나머지 3건이 무력화를 잡는다. **봉인이 no-op 이 아님이 증명되었다.**

### 명세 전제 반증 2건

**반증①: ②설계안끼리 비교는 서버 엔진을 쓸 수 없다.**
```
server/new_design_comparison/engine.py:248-250
    def _validate(request):
        if not request.existing_analysis.analysis_table.columns:
            raise NewDesignComparisonError("EMPTY_ANALYSIS_TABLE", "증권분석 결과에 증권(열)이 없습니다.")
```
엔진이 기존 증권 컬럼 0개를 400 으로 하드 거부한다. 엔진 변경은 봉인 영역이므로
설계안 간 비교는 **클라이언트 측 비교**가 유일한 길이다.
→ 명세가 요구한 판단("고객 선택 없이도 진행 가능해야 하는지")의 답: **가능하며, 그렇게 해야 한다.**

**반증②(파생): 전량 해지 시 엔진 호출 불가.**
명세는 "전체 해지 / 일부 해지 둘 다 가능"을 요구한다. 전량 해지하면 retained columns = 0 이 되어
반증①과 같은 400 이 확정된다. → 엔진을 호출하지 않고 `synthesis-all-cancelled` 로 정직하게 안내한다.

### 정직성 설계 (거짓 정보 방지)

**부분 해지 시 보험료 감소액을 계산하지 않는다.** 기존 증권 보험료는 `columns[].premium_won`
즉 **증권(상품) 단위로만** 존재하고 담보별 보험료가 없다. 비례배분은 곧 추측이므로 금지했다.

합계 히어로 숫자는 아래 4조건이 **모두** 참일 때만 렌더한다(`FinalSynthesisPanel.tsx:220-224`):
```
heroReady = premiumChange.computable
         && existingTotalWon !== null
         && newDesignPremiumReconciled === true
         && newDesignPremiumWon !== null
```
히어로와 "계산할 수 없습니다" 박스는 **DOM 상 상호배타**다(테스트로 단언).

그 외:
- 서버 `meta.coverage_notices` 를 **처음으로 화면에 노출**했다. 그전에는 `src/` 전체 grep 0건이라
  공식 비교표 0행의 정직 고지가 사용자에게 도달하지 못했다. 공식 비교표 **바로 위**에 배치해
  설계사가 "담보는 다 보이는데 비교표가 비었다"를 버그로 오해하지 않게 했다.
- 약관 섹션의 사실과 다른 문구("이 파일은 1단계 증권 분석 시작에도 함께 전송됩니다")를
  실제 동작에 맞게 정정했다. 그 배선이 사라졌기 때문이다.
- 제안서 다건 처리 시 한 건이 실패해도 나머지를 계속 처리하고, 실패 건을 사유와 함께 목록에 남긴다.

### 수정 파일별 검증 상태

| 파일 | 줄수 | tsc | 테스트 | 비고 |
|---|---|---|---|---|
| /home/jay/projects/InsuRo/.worktrees/task-3046-dev3/src/pages/NewDesignComparison.tsx | 1141 | PASS | PASS | 4단계 흐름 재구성 |
| /home/jay/projects/InsuRo/.worktrees/task-3046-dev3/src/pages/__tests__/NewDesignComparison.noReupload.seal.test.tsx | 273 | PASS | 4 passed | 봉인 |
| /home/jay/projects/InsuRo/.worktrees/task-3046-dev3/src/pages/__tests__/NewDesignComparison.uploadFlow.test.tsx | 683 | PASS | 12 passed | 새 흐름으로 교체 |
| /home/jay/projects/InsuRo/.worktrees/task-3046-dev3/src/components/policy/customerAnalyses.ts | 218 | PASS | 17 passed | 고객검색·분석로드 |
| /home/jay/projects/InsuRo/.worktrees/task-3046-dev3/src/components/policy/cancellationPlan.ts | 369 | PASS | 30 passed | 해지·보험료 계산 |
| /home/jay/projects/InsuRo/.worktrees/task-3046-dev3/src/components/policy/CustomerAnalysisPicker.tsx | 238 | PASS | 8 passed | 고객·분석 선택 |
| /home/jay/projects/InsuRo/.worktrees/task-3046-dev3/src/components/policy/ExistingCancellationTable.tsx | 239 | PASS | 5 passed | 해지 선택 |
| /home/jay/projects/InsuRo/.worktrees/task-3046-dev3/src/components/policy/PlanCandidateCompareTable.tsx | 226 | PASS | 10 passed | 설계안 비교 |
| /home/jay/projects/InsuRo/.worktrees/task-3046-dev3/src/components/policy/FinalSynthesisPanel.tsx | 554 | PASS | 11 passed | 최종 종합 |
| /home/jay/projects/InsuRo/.worktrees/task-3046-dev3/src/components/policy/NewDesignComparisonTable.tsx | +17 | PASS | PASS | coverage_notices 타입 선언만 |

## L1 스모크테스트

- **서버 재시작**: 해당없음 (서버 변경 0줄 — 이번 작업은 프론트 전용)
- **API 응답 확인**: 실서버 `localhost:8001` (main=`af5d602`) 실측

| 엔드포인트 | 코드 | 판정 |
|---|---|---|
| `/api/insuro/search-customers?q=kim` | 401 | 존재(인증필요) — 이번 작업이 쓰는 경로 |
| `/api/insuro/customer-policy-analyses/{uuid}` | 401 | 존재(인증필요) — 이번 작업이 쓰는 경로 |
| `/api/insuro/new-design-comparison` (GET) | 405 | 존재(POST 전용) |
| `/api/insuro/proposal/upload` | **404** | **부재** — PR #259 미머지 |
| `/api/insuro/proposal/status/{id}` | **404** | **부재** — PR #259 미머지 |

404 원인 확정: `grep -c proposal_upload_v1 server/main.py` = **0**,
`git merge-base --is-ancestor task/task-3024-dev3 HEAD` = **false**.

- **스크린샷**: `/home/jay/workspace/memory/reports/task-3046-shots/` (11장, 실브라우저)
  - 인증은 task-2990 의 localStorage 세션 주입 패턴 재사용 — **JWT 위조가 아니라 렌더 경로 검증**이다.
  - API 는 `page.route()` 로 가로채 현실적 응답을 주입했다.
  - 팀장이 `01`·`05`·`08` 을 **눈으로 확인**했다. 로그인월 아님, 실제 화면이 렌더됨.
  - ★ `08`·`09`·`10` 은 **md5 동일**(`caa0001d…`) — 같은 상태의 fullPage 스샷 1장에
    최종종합·보험료 계산불가·coverage_notices 가 **모두 들어 있어** 파일명만 3개다. 3회 촬영이 아니다.

## 회귀

| 항목 | 기준선 (`d1ea01e`, 변경 전) | 현재 (`b940348`) | 델타 |
|---|---|---|---|
| vitest Test Files | 103 passed | 110 passed | +7 |
| vitest Tests | 1575 passed / 0 failed | **1658 passed / 0 failed** | +83 / 0 |
| tsc | EXIT 0 | EXIT 0 | — |

기준선은 명세 수치가 아니라 **base 커밋에서 직접 재측정**했다. worktree 에 `.env` 가 없어 오염 없음.

## trip-wire 5종

| 항목 | 실측 |
|---|---|
| Critical7 | 0 |
| PII net-new | 0 |
| 회귀 실패 | 0 (1658 passed / 0 failed) |
| forbidden_paths 침범 | 0 (자체 저작분 기준 — 아래 상속분 별도) |
| nonce = task_id | task-3046 일치 |

## [CRITICAL] ANU 판단 필요 — scope-guard 구조적 충돌

scope-guard 판정식은 `merge-base(origin/main, HEAD)..HEAD` 다(`finish-task.sh:857,878`).
실측 merge-base = `af5d602`(= origin/main tip). 그 결과 **base 브랜치 `task/task-3024-dev3`(PR #259, 미머지)가
만든 `server/**` 4건이 우리 diff 에 상속**된다:

```
server/main.py
server/policy_extract/proposal_mapping.py
server/routes/proposal_upload_v1.py
server/tests/test_proposal_upload_v1_task3024.py
```

task-3046 명세는 `server/**` 를 `forbidden_paths` 로 금지한다.

**저작자 확인 (전부 task-3024 커밋)**: `3a84d16`, `59dc3a1`, `2e96278`, `63c1f65`.
단 `server/main.py` 는 제 병합 커밋 `d1ea01e` 에서도 접촉했다 — **명세가 "최신 origin/main 을 먼저 병합하라"고
지시**했고 충돌이 났기 때문이다. 내용은 기존 import 2줄(`proposal_upload_v1`, `naver_benchmark_v1`)을
**양쪽 보존**한 것뿐이며 신규 로직 0줄이다.

원인은 명세가 **"task/task-3024-dev3 위에서 이어가라"와 "server/** 금지"를 동시에 요구**한 구조적 충돌이다.
**자가해소하지 않았다.** ANU 판단을 요청한다. (해결됨 — 근거 박제 완료, 조치는 ANU 몫)

부수 조치: E2E 스크립트를 `e2e/` 에 두면 `allowed_resources` 밖이 되므로 **repo 밖으로 옮겼다**
(`/home/jay/workspace/memory/reports/task-3046-e2e/task-3046-customer-flow.e2e.mjs`).
회피 가능한 범위 이탈은 만들지 않았다.

## ★ 머지 전 확인 사항

1. **머지 순서 = PR #259 → PR #266.** 실서버 실측상 `proposal/upload`·`proposal/status` 가 404 다.
   ②설계안 비교는 #259 머지 후 동작한다. **①고객 선택·자동 로드(회장 지적의 핵심)는 현재 API 만으로 동작한다.**
2. 중복 PR 정리: 초기에 base 를 `task/task-3024-dev3` 로 연 **#265 를 닫고 #266(base main)으로 일원화**했다.
   두 PR 의 head 는 동일(`b940348`)이었다. #265 에 사유 코멘트를 남겼다.

## 범위 밖 / 미해결

- **가성비 계산(단위 가입한도당 보험료)** — 명세대로 **자리만 남기고 구현하지 않았다**(별도 태스크).
- **공식 비교표 0행** — 지식DB 담보 등록률(메리츠 14/299) 문제이며 별건이다. 고치지 않았고,
  `coverage_notices` 로 사용자에게 **전달**되게만 했다.
- **검증용 실 PDF 가 1종뿐** — `memory/samples/` 에 메리츠 제안서 1건만 있다.
  명세 검증 4번이 요구한 "제안서 2건 이상"은 **실 PDF 2종이 아니라 E2E 주입 픽스처 2건**으로 확인했다.
  실 PDF 2종 교차검증은 하지 못했다. 이 사실을 감춘 채 "2건 검증 완료"라고 쓰지 않는다.
- `policy-analysis-status` 의 `partial_failure`/`failed_count` 가 프론트 타입에 없어 유실된다
  (`existingAnalysisUpload.ts:54-59`). 이번 범위 밖이라 손대지 않았다.

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 산출물 |
|---|---|---|---|
| 루 (Lugh) | 백엔드(순수 TS 데이터계층) | sonnet | `customerAnalyses.ts`, `cancellationPlan.ts` + 테스트 38건 |
| 브리짓 (Brigid) | 프론트엔드 | sonnet | 선택 UI 3종 + 최종종합 패널 + 페이지 배선 |
| 아네 (Aine) | UX/UI 설계 | sonnet | ③단계 정보구조·4케이스 보험료 표현·한국어 문구 사전 |
| 모리건 (Morrigan) | 테스트/QA | sonnet | 봉인 테스트 4건 + 변이검증 + 실브라우저 E2E 11 시나리오 |

haiku 미사용. 팀장(Opus)은 설계 판단·검토·통합만 수행했고, 팀원 산출물의 문법 오류 1건
(테스트명 따옴표 이스케이프 누락)만 직접 수정했다.

## 팀장이 뒤집은 팀원 판단

아네(UX)가 "서버 엔진은 해지를 모르므로 공식 비교표는 해지와 무관하다"고 설계했으나,
**엔진에 무엇을 보낼지는 우리가 정한다.** 해지 반영 후 표(`buildRetainedTable`)를 보내면
비교표도 해지가 반영된다. 회장 지시가 "해지한 부분 + 신규 준비분을 종합해 **최종적으로 어떤 보장을
받게 되는지**"이므로 이쪽이 맞다. 엔진 로직은 그대로 두고 **입력만** 바꾸므로 봉인 위반이 아니다.
문구도 "해지 선택과 무관하게" → "해지 선택을 반영한" 으로 교정했다.

## 팀원 보고 검증 (표면 통과 ≠ 실제 동작)

- 브리짓 1차 보고 "tsc exit 0 / 23건 통과" → **실제로는 파싱 에러**가 있었다.
  테스트명 문자열 `it("값 없는 셀("-")에는…"` 의 따옴표 미이스케이프. 팀장이 잡아 수정 후 재측정했다.
- 브리짓 2차에서 지시와 다르게 구현한 부분을 스스로 신고했고, 확인 결과 **판단이 옳았다**
  (`computable` 의미를 바꾸면 기존 테스트 2건이 깨짐 → 순수함수 계약 유지 + 패널에서 4조건 게이트).
- 모리건의 봉인 변이검증은 **팀장이 독립적으로 재현**했다(3/4 FAIL → 원복 → 4/4 PASS).
- E2E 스크린샷은 **팀장이 이미지를 직접 열어 확인**했다. 첫 촬영본은 백지였고(dev 모드 env 누락),
  모리건이 원인을 찾아 환경변수로 해결한 뒤 재촬영한 것이 최종본이다.

## 커밋

```
b940348 E2E 스크립트를 repo 밖으로 이동 — allowed_resources 에 e2e/ 경로 없음
96c130f 모리건: 재업로드 부활 방지 봉인 테스트 4건 + 실브라우저 E2E 11 시나리오
004db01 브리짓: 고객선택 자동로드로 화면 재구성 — 재업로드 제거, 설계안 다건 비교, 해지분 선택, 최종 종합
2bfb333 브리짓: 최종 종합 패널 + 보험료 미상 판정 보강
d0682f4 루+브리짓: 고객 증권분석 로드/해지계획 데이터계층 + 선택 UI 3종
d1ea01e Merge origin/main (server/main.py import 충돌 해소 — 양쪽 보존)
```

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


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


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


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


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


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


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


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

