# task-2864 보고서 — RP 배치1: 담보명 fallback(a042) + 만기·상품유형 저장(골든 계약 동기)

- **팀**: dev2-team (오딘) / 구현: 토르(백엔드, Sonnet)
- **레벨**: Lv.2 게이트 (task 표제 Lv.3, 배치1=캡처·저장·계약)
- **프로젝트**: InsuRo (`/home/jay/projects/InsuRo`)
- **브랜치**: `task/task-2864-dev2` (worktree)
- **base**: origin/main `c683eb8`
- **merge_policy**: none → 머지는 ANU

## Situation (상황)
복합설계 RP 상담용 저장 레코드가 (a)담보명 정상 (b)납입기간·만기 (c)상품유형(비갱신)을 담아야 웹앱(배치2)이 X년 총액·비교블록을 그릴 수 있다. 현재 저장 레코드는 이 3요소가 누락/불완전.

## Complication (문제)
- **담보명 a042 버그**: `buildAnalysisMatrix`가 `name: coverage_cd`로 폴백 → `coverage_name` 속성이 빈 담보는 화면에 보이는 실제 이름("가족일상생활중배상책임")이 저장 안 됨. `buildAnalysisMatrix`는 순수함수라 담보명 소스가 없었음.
- **만기·상품유형 미저장**: 확장이 만기(`content.js:249` regex)를 이미 파싱 중이나 `query_condition`에 실리지 않음. 상품유형은 캡처 자체가 없음.
- **골든 계약 원자 동기 리스크**: 서버 `QueryCondition`이 `extra="forbid"` → 스키마 선반영 없이 새 필드 전송 시 422. 확장↔서버 골든 exact-match 정합 유지 필요.

## Question (질문)
migration 없이, fingerprint(dedup) 4요소를 불변으로 유지하면서, 확장 캡처 → 서버 스키마 → 골든 픽스처 → 계약문서를 원자적으로 동기화할 수 있는가?

## Answer (해결)

### 작업 A — 담보명 실제 텍스트 fallback (계약 무관 독립)
- `readSelectedCoverages`(content.js:1463)가 담보별 `name` 추출: **`coverage_name` 속성 → `extractRowVisibleCoverageName(row)`(행 clone 후 input/select/button/script/style 제거, 남은 textContent trim) → `cd`(최후)**.
- `name`은 wire snapshot 미유출 — `selected_coverages`는 여전히 `{coverage_cd, amount}`로 재매핑(서버 `SelectedCoverage` extra=forbid). 
- 모듈 변수 `_previewCoverageNameMap`(cd→name)을 preview 빌드 시점에 저장, `invalidatePreviewState`에서 리셋 → `buildAnalysisMatrix`의 3번째 인자(nameMap)로 전달. 함수 내 `name: (nameMap && nameMap[cov.coverage_cd]) || cov.coverage_cd`. **buildAnalysisMatrix 순수성 유지**(DOM 미참조, 인자로만).

### 작업 B — 만기·상품유형 query_condition 저장 (골든 원자 동기, Codex 순서 준수)
1. **서버 스키마 먼저** — `QueryCondition`에 optional 3필드: `payment_years:int|None(ge=0,le=130)`, `maturity_age:int|None(ge=0,le=150)`, `product_type:str|None(max_length=64)`, 전부 `default=None`, `extra="forbid"` 유지.
2. **확장 캡처** — `readProductAndMaturity(doc)`가 `#selProductsGroupCD`/`#selPaymentExpirationCD`("N년/M세" 기존 regex 재사용) → `{product_type, payment_years, maturity_age}`(부재/파싱실패 시 null). `buildImmutablePreviewSnapshotV1`가 **fingerprint 계산 후** non-null만 `snapshot.query_condition`에 얹음. `normalizePpdQueryCondition`은 4키만 반환 유지.
3. **ingest.js** — `assembleRequestBody` canonical query_condition에 3필드 **조건부 spread**(`!= null`일 때만), 키 순서 = 기존 4키 뒤 신규 3키.
4. **골든 재생성** — `contract-golden.test.ts::goldenRepresentativeInput()`·`generate_golden.mjs` 대표입력에 동일 값·순서 추가 → `node tests/contract/generate_golden.mjs` → `ohmy_ppd_v2.golden.json` 갱신(diff=신규 3필드만).
5. **계약 문서** — `INSURO_LV4_PROGRAM_CONTRACTS_V1.md` §2-1에 optional 3필드 + **request_fingerprint/dedup은 4요소 불변** 명문화.

## 수정 파일 (9, 전부 allowed_resources 내 · forbidden 0)
1. `server/schemas/consultation_history_v1.py` — QueryCondition optional 3필드
2. `extension/content.js` — 담보명 fallback + nameMap 배선 + readProductAndMaturity + snapshot 3필드
3. `extension/background/ingest.js` — canonical query_condition 조건부 3필드
4. `tests/contract/generate_golden.mjs` — 대표입력 3필드
5. `tests/contract/ohmy_ppd_v2.golden.json` — 재생성(3필드만 diff)
6. `extension/__tests__/contract-golden.test.ts` — goldenRepresentativeInput 3필드
7. `extension/__tests__/track-a-preview.test.ts` — 담보명 fallback 3케이스·nameMap·readProductAndMaturity·fingerprint 불변 테스트
8. `server/tests/test_consultation_history_v1.py` — optional 수용/범위거부/extra=forbid/e2e POST(±신규필드) 8건 추가
9. `docs/contracts/INSURO_LV4_PROGRAM_CONTRACTS_V1.md` — 계약 갱신

## 테스트 결과 (팀장 독립 재실행)
- `npx vitest run extension/__tests__/` → **474 passed / 22 files / 0 failed**
- `pytest tests/test_consultation_history_v1.py tests/test_contract_golden.py` → **80 passed**
- `pytest tests/` 전체 → 초기 876 passed / 1 failed = `test_cors_fail_closed_when_ext_origin_unset`. **근본원인 규명**: `server/main.py:54 load_dotenv(.env)`가 worktree 로컬 `.env:44 INSURO_EXTENSION_ORIGIN`(파일럿 확장 CORS 안정 ID)를 로드 → 테스트 전제(var unset)를 오염. base commit c683eb8(변경 전)에서도 **동일 실패 재현** → task-2864 코드 무관. **조치**: worktree 로컬 `.env`(gitignore·throwaway 복사본)의 파일럿 라인만 일시 중화(CI-parity) → 전체 `pytest server/tests/` = **877 passed / 0 failed**(132s). ⇒ task-2864 회귀 0 확정. (추적 소스 미변경, 로컬 파일럿 config만 goal-gate/CI 정합용으로 중화.)
- `npx tsc --noEmit` → clean (extension/**는 root tsconfig scope 밖 — 변경 전후 동일, 기존 CI와 일치)
- 골든 diff → query_condition 신규 3필드만, fingerprint 불변.

## L1 스모크테스트 결과 (필수)
- **서버 재시작**: 성공 — `uvicorn main:app --port 8899` 신규 스키마로 정상 부팅("Application startup complete"). 테스트 후 정리(port 8899 프로세스 0).
- **API 응답 확인**: `POST /api/insuro/consultation-history/v1`(query_condition에 신규 3필드 포함) → **HTTP 401**(auth gate). 서버 부팅+라우트 도달+스키마 로드 정상 확인. 스키마 수용은 아래 직접 검증으로 확정:
  - 골든 픽스처(신규 3필드)로 `ConsultationHistoryV1Request.model_validate` → **FULL REQUEST OK**, query_condition 3필드 반영.
  - 하위호환: 신규필드 없는 레코드 → payment_years/maturity_age/product_type = None **OK**.
  - `extra="forbid"` 유지: unknown 필드 → **rejected OK**.
  - pytest e2e POST 2건(TestClient auth 통과 경로, ±신규필드) 포함 80 passed.
- **스크린샷**: 해당없음(백엔드/계약 작업, UI 없음 — 웹앱 비교블록 UI는 배치2 범위).

## 발견 이슈 및 해결 / 유의점
- **담보명 visible-text 셀렉터 (정직한 caveat)**: repo에 `coverage_name` 속성이 **빈** 실제 ohmymanager `bojang_lists` 행의 캡처 DOM 스냅샷이 없음(합성 픽스처는 전부 속성 존재). 특정 CSS 클래스 추측 대신 **markup-agnostic 추출기**(행 clone→input/select/button 제거→남은 텍스트)로 구현. label/span/td/text node 무관 동작. **배치2 전 a042류 실 DOM(F12 캡처)로 스팟체크 권장** — 검증된 셀렉터가 아니라 합리적 엔지니어링 기본값.
- unused-var 진단(`_lastMmlfcpTrace`, `attachCrmMatchControls`의 `snapshot`, ingest.js `consent`)은 전부 **기존 코드**(이번 변경 무관), extension은 tsc scope 밖이라 비차단.

## 머지 판단
- **머지 필요**: Yes (merge_policy=none → **ANU 소유**)
- **브랜치**: `task/task-2864-dev2`
- **워크트리**: `/home/jay/projects/InsuRo/.worktrees/task-2864-dev2`
- **머지 의견**: 범위 = allowed_resources 내부만(src/manifest/migration 불변, migration 0). 골든 원자 동기 완결(스키마→확장→ingest→골든→문서). fingerprint 4요소 불변. vitest 474 + pytest 신규 80 pass, 유일 실패는 base 재현 CORS flake. 하위호환 확인. **머지 안전**. 후속(ANU): 파일럿 빌드 재전송·배치2(웹앱 비교블록) 착수.

## 모델 사용 기록
- 토르(백엔드): **Sonnet** — 계약 동기·스키마·JS 배선 로직 구현(haiku 부적합).
- 팀장(오딘, Opus): 구조 선행 파악·설계·독립 검증·L1·종결(직접 코딩 0).
- 헤임달 QA 별도 소환 없이 팀장 독립 재검증으로 G2 대체(테스트 전량 재실행).

## 커밋
- `e656a41` 서버 스키마 · `da1c8f6` 확장 캡처(A+B2) · `e961f28` ingest+골든 · `05a367b` 계약 문서

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


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


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

