# task-2787+4 보고서 — 진단빌드 한정 "현재선택 DOM 스냅샷" 스파이크 (읽기전용 관찰)

- **작업 ID**: task-2787+4
- **팀**: dev5-team (마르둑 팀장 / 엔키 백엔드 / 닌기르수 테스터)
- **레벨**: Lv.3 (진단 관찰 로직 + 테스트, 구현 아님)
- **브랜치**: `task/task-2787+2-dev5` (same-branch 계속 커밋, **PR·merge·배포 금지 준수**)
- **워크트리**: `/home/jay/projects/InsuRo/.worktrees/task-2787+2-dev5`
- **작성 시각**: 2026-07-18 KST

---

## S — Situation (상황)

self-smoke #1에서 캡처 경로는 VERIFIED됐으나, `selected_coverage_count=null`(user_coverages 2개·활성 식별 불가)로 어댑터가 **전체 담보를 합산** → 화면 총액과 불일치(메리츠 화면 343,414 vs adapter 481,359). GPT·Codex·ANU 3자 합의로 데이터 계약이 확정됨:
- **DOM = 현재 선택 담보집합 + 담보별 현재 가입금액** (source of truth 후보)
- **API `/api/ProductPremiums` = 보험료 원천**
- **화면 표시 총액 = 검증 oracle** (계산 입력 아님)

## C — Complication (문제)

기존 `user_coverages` 경로는 저장플랜(GUID) 기준이라 **현재 화면 선택 상태를 반영하지 못함**. 그러나 전면 구현은 금지 — DOM 보험료 셀을 주 계산원으로 승격하면 과거 스크래핑 취약성이 회귀함. 담보명 단독 매칭도 혼동 위험.

## Q — Question (질문)

기존 경로를 파괴하지 않고, DOM 현재선택을 **병렬 관찰 필드로만** 추가해 실데이터 증거(MATCH/GAP/UNKNOWN)를 얻을 수 있는가? 코드가 라이브-DOM 미지수를 단정하지 않고 정직하게(UNKNOWN/fail_closed) 기록하는가?

## A — Answer (답변)

**inject.js 진단 게이트 내부에 순수함수 4개 + sink 부착으로 병렬 관찰 필드를 추가했다. 기존 경로는 무변경.** 실 DOM fixture end-to-end 실행에서 DOM 병렬 경로가 checked 담보를 정확히 추출하고 화면총액과 EXACT 매칭됨을 관찰했다. 단위테스트 19건 신규 + 전체 451건 PASS.

---

## 구현 상세 (S1~S6 매핑)

모든 신규 로직은 **inject.js 내부**(진단 게이트 `__INSURO_DIAG_ACTIVE__===true` → runRealInjection 안, 또는 `isTestEnvironment` 시 export만). 프로덕션 무동작. `extension/lib/ohmy_premium_adapter.js` 참조 없음(존재하지 않음).

| 요구 | 구현 함수 (inject.js) | 내용 |
|---|---|---|
| S1 checked 담보 추출 | `extractDomCheckedSelection(rootEl)` | `#bojang_lists input[type=checkbox][data-cd]` 중 `el.checked===true`만 → `dom_selected_cds[]`, `dom_checked_count` |
| S2 가입금액 병렬 실사 | 〃 | 각 checked 담보 행: `(a) getAttribute("guide_coverage_amount")` + `(b) 조상 행(li 우선) 편집 input.value` 둘 다 기록. input 미발견 시 `amount_input:"UNKNOWN_NO_INPUT"`(추측·0 대입 금지) |
| S3 coverage_cd join 검증 | `joinDomSelectionToApi(cds, coveragePremiums)` | 회사별 `join_matched`/`join_gap_missing`/`join_gap_premium_null`/`join_duplicate`(회사별 중복). 1 DOM담보→1 API코드 확정 실패(중복) 시 `fail_closed:true` + 해당 cd 합산 제외(임의 채택 금지) |
| S4 DOM선택 기반 재계산 | 〃 + `buildDomSelectionDiagnostic()` (sibling 어셈블러) | `dom_selected_adapter_sum`을 **신규 필드**로 산출. buildDiagnosticSummary의 user_coverages 내부생성 경로는 그대로, DOM 집합을 전달하는 별도 경로 추가. premium 유한수만 합산(0/null 미변환) |
| S5 검증 대조 분류 | `classifyScreenTotalMatch(sum, screenTotal, count)` | `EXACT`(diff==0) / `WITHIN_TOLERANCE`(\|diff\| ≤ 담보수×1원) / `OUT_OF_TOLERANCE`(초과) / `UNKNOWN`(sum·screen null) |
| S6 race 증거 | 어셈블러 인자 | `api_captured_at`(ppBuffer.capturedAt) + `dom_snapshotted_at`(Date.now()) 병렬 기록 |

**sink 부착**: `attachScreenTotalDiff` 직후 → `console.log("[InsuRo Diagnostic]")` 직전에 try/catch로 신규 필드(`dom_checked_count · dom_selected_cds · dom_amounts · per_insurer_dom · fail_closed · api_captured_at · dom_snapshotted_at`) 부착. 기존 필드 전부 유지.

### 수정/생성 파일
- **수정**: `extension/inject.js` (471줄 → 716줄, +245줄). 커밋 `357025b`. 기존 `buildDiagnosticSummary`/`user_coverages` 로직 **무변경**.
- **신규(new_file_limit=1 준수)**: `extension/__tests__/dom_selection_snapshot.test.ts` (359줄, it 19건). 커밋 `7d14b82`.

---

## 신규 필드 예시 출력 (실 DOM fixture end-to-end 관찰)

`extension/__tests__/fixtures/mmlfcp-real-dom.html`(DEATH·CANCER 둘 다 checked, 편집 input 없음) + API fixture(meritz/hanwha/db):

```
dom_checked_count: 2
dom_selected_cds: ["DEATH","CANCER"]
dom_amounts: [
  {"cd":"DEATH","amount_attr":"100000000","amount_input":"UNKNOWN_NO_INPUT"},
  {"cd":"CANCER","amount_attr":"50000000","amount_input":"UNKNOWN_NO_INPUT"}
]
fail_closed: false
api/dom 타임스탬프 gap: 500ms
per_insurer_dom:
  {"company_name":"메리츠화재","join_matched":2,"join_gap_missing":0,"join_gap_premium_null":0,"join_duplicate":0,"dom_selected_adapter_sum":20000,"screen_total":20000,"diff":0,"classification":"EXACT"}
  {"company_name":"한화손해보험",...,"dom_selected_adapter_sum":22500,"screen_total":22500,"diff":0,"classification":"EXACT"}
  {"company_name":"DB손해보험",...,"dom_selected_adapter_sum":19500,"screen_total":19500,"diff":0,"classification":"EXACT"}

[기존 필드 무변경 확인]
selected_coverage_count: null | validation_flags: ["USER_COVERAGES_NOT_UNIQUE"] | per_insurer: 3사 유지
```

### MATCH / GAP / UNKNOWN 판정 (이번 fixture 기준)
- **MATCH**: fixture는 전체 담보 checked → DOM 선택 합계 == 화면총액 3사 모두 `EXACT`. 즉 DOM 스냅샷이 현재선택 source of truth로 작동함을 관찰.
- **UNKNOWN**: `amount_input`이 3건 모두 `UNKNOWN_NO_INPUT` — fixture DOM엔 담보 행 편집 input이 없어 **추측 없이 정직하게 UNKNOWN**. 라이브에 실제 편집 input이 있는지는 self-smoke #2로만 판정.
- **GAP(대조 증거)**: 기존 경로는 `selected_coverage_count=null`+`USER_COVERAGES_NOT_UNIQUE`로 여전히 실패하는데, **동일 시점 DOM 병렬 경로는 선택 2개를 정확 추출** → 계약이 예측한 "저장플랜 ≠ 현재선택" GAP을 코드로 재현.

---

## 테스트 결과 (goal_assertions)

- `npx vitest run extension/__tests__/dom_selection_snapshot.test.ts` → **19 passed / 0 failed** (신규)
- `npx vitest run` (전체) → **451 passed / 0 failed (36 test files)** — 회귀 0
- 기존 `diagnostic_build.test.ts` → **20 passed** (무영향)

신규 테스트 커버(단위): checked 추출/UNKNOWN_NO_INPUT · join 4상황(matched/missing/premium_null/duplicate) · fail_closed true/false · tolerance 4분류(EXACT/WITHIN/OUT/UNKNOWN) · 어셈블러 end-to-end.

## L1 스모크테스트 결과

- **서버 재시작**: 해당없음 (브라우저 확장 진단빌드 — 서버 프로세스 아님)
- **API 응답 확인**: 해당없음 (읽기전용 관찰 — 신규 API 없음). 대신 실 DOM fixture + API fixture로 **순수함수 end-to-end 실행**해 신규 7개 필드가 실제로 산출됨을 관찰(위 예시 출력). node CJS 강제 실행(루트 package.json `type:module` 회피) — exports 12개 확인.
- **스크린샷**: 해당없음 (라이브 브라우저 self-smoke는 계약상 **회장 self-smoke #2**로만 판정 — 코드로 단정 금지). L1 미통과 아님: fixture end-to-end 관찰로 실동작 1건 이상 실행·통과.

## 라이브-DOM 미지수 (self-smoke #2로만 판정)

1. 담보 행에 편집가능 `<input>`(현재 가입금액)이 실제 존재하는가 — fixture엔 없어 `UNKNOWN_NO_INPUT`. 실 DOM 확인 필요.
2. `checked`가 진짜 제출상태를 반영하는가.
3. DOM `data-cd` == API detailList `coverage_cd` 코드체계 동일한가.
4. 화면 회사 표시총액이 선택담보만 반영하는가(전체 렌더 시 GAP 발생).

## 발견 이슈 및 해결

- **TypeScript 진단 경고 3건**: (a) inject.js:165/171 — 기존 코드(patchedSend body 미사용, XHR constructor)로 이번 변경과 무관. (b) inject.js:663 `per_insurer_dom may not exist on type` — summary 객체 동적 프로퍼티 할당(병렬 관찰 설계상 의도). `.js` 파일이라 런타임 무해, vitest(swc/babel 트랜스파일)에서 451건 전부 PASS로 실증. 조치: 병렬 관찰 계약상 동적 할당이 의도이므로 유지.
- **standalone node 실행 실패**: 루트 `package.json`이 `type:module`이라 plain node가 inject.js를 ESM으로 해석해 `module.exports` 무시. 조치: L1 관찰은 `.cjs` 강제 또는 vitest(vite 트랜스폼) 경로로 실행 — 실사용 경로(vitest)는 정상 동작 확인.

---

## 게이트 통과 (X팀 한정승인 → 팀장 자체 통과)

- **G1 설계**: affected_files = inject.js + 신규 test 1개. allowed_resources 범위 내, forbidden_paths(src/server/supabase/content.js/background.js/manifest/build-diagnostic.mjs) 무접촉. 다른 팀 worktree 무접촉.
- **G2 구현**: 팀 테스터(닌기르수) 단위테스트 19건 + 전체 451건 PASS. 기능 테스트(fixture end-to-end 관찰) 완료.
- **G3 머지**: **PR·merge 금지(task 명시)** — same-branch 커밋만 유지. G3 머지 게이트는 이번 스파이크에서 **미적용**(계약: 증거 확보 후 별도 승인).

## 머지 판단

- **머지 필요**: **No** (task 명시: PR·merge·배포·운영활성화 금지. 이번은 병렬 관찰 스파이크)
- **브랜치**: `task/task-2787+2-dev5`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2787+2-dev5`
- **머지 의견**: 커밋 2건(`357025b`, `7d14b82`) same-branch 유지. dist-diagnostic 재빌드+hash 재고정은 **ANU가 수행 예정**(build-diagnostic.mjs 자체 수정 금지 준수). 회장 self-smoke #2 실데이터 증거 확보 후 구현·fail-closed 전환·scrapeScreenTotals 필터화를 **별도 승인**으로 진행 권장.

## 모델 사용 기록

- 엔키(백엔드): sonnet — inject.js 순수함수 구현(로직 정확성 필요, haiku 미사용)
- 닌기르수(테스터): sonnet — jsdom 단위테스트 작성(계약 검증 정밀성 필요, haiku 미사용)
- 마르둑(팀장): 설계/분배/통합/L1 관찰 검증

## 이번 스파이크 금지 준수 확인

- USER_COVERAGES_NOT_UNIQUE "전체행 합산" fallback **미변경**(병렬 관찰만) ✓
- scrapeScreenTotals 필터화 안 함 ✓ · DOM 보험료 셀 주 계산원 안 씀 ✓ · 담보명 단독매칭 안 함(코드 join만) ✓ · 실패시 DOM 자동 fallback 없음 ✓
- PR·merge·배포·운영활성화·서버/DB/CRM/UI 수정·자동전송·실고객 저장 **없음** ✓
