---
task_id: "2859"
team: 2팀
level: 4
priority: P1
scope: task
status: completed
---

# task-2859 — 저장=조회가능(API_MISSING 완화) + 보험사 체크해제 완전제외

## Situation (상황)
InsuRo 파일럿에서 회장 피드백 2건:
1. **저장했는데 인슈로에서 안 보임** — 저장은 성공(id 3, 서버 200·DB 기록)하지만 웹앱 ok-only 뷰에서 숨겨짐.
2. **체크 해제한 보험사(롯데)가 저장 매트릭스에 그대로 포함** — 10사 전부 저장됨.

## Complication (문제)
1. `classifyOverallStatus`가 `failureCounts`에 값이 하나라도 있으면 **무조건 `REQUIRES_REVIEW`** 반환 → ok-only 뷰가 숨김. 하지만 per_insurer 합계는 전부 `WITHIN_TOLERANCE`(실제 금액은 정상, 과도 격리). 원인은 선택 담보 일부가 ProductPremiums API 응답에 없어 `API_MISSING`이 카운트된 것.
2. `readSelectedInsurerCodes`가 실 DOM에서 `#companyInfo input[type="checkbox"][company_code]` 셀렉터를 못 찾아 `null` 반환 → 필터 미적용(전부 포함). ohmymanager는 미선택 보험사도 API에 stale non-zero 값을 유지하지만 화면 셀은 0원으로 렌더링됨.

## Question (질문)
저장 성공 = 조회 가능하게 하되(실제 금액 불일치만 격리), 체크 해제 보험사는 확실히 제외하려면?

## Answer (해결)

### 작업 A — P1: API_MISSING 완화 (저장=조회가능)
- `content.js`에 `SOFT_DOWNGRADE_FAILURE_CODES` Set 신설: `API_MISSING`, `API_DUPLICATE`, `SCREEN_CELL_MISSING`, `DOM_DUPLICATE`, `PARSE_FAIL` (전부 `PPD_FAILURE_CODE` 상수 참조, 하드코딩 문자열 없음).
- `classifyOverallStatus`: failureCounts에 **HARD 코드(SOFT_DOWNGRADE 밖 전부)가 하나라도 있으면** `REQUIRES_REVIEW`, HARD 없고 SOFT만/없음이면 `OK`.
  - **HARD 유지**: `OUT_OF_TOLERANCE`(실제 금액 불일치), `NEGATIVE_INVALID`, `ZERO_BASELINE`, `DIFFERS_ACROSS_COMPANIES`, `API_PREMIUM_MISSING_OR_INVALID`, `ZERO_PREMIUM_UNVERIFIED` (fail-closed 안전).
- **disclosure 유지**: `buildImmutablePreviewSnapshotV1`의 `failure_codes` 조립부는 **불변** → "OK지만 일부 담보 API 미매칭" 정보는 snapshot에 그대로 기록됨(숨기지 않음). 통합 테스트로 검증.

### 작업 B — P2: 보험사 체크해제 완전 제외 (2신호 견고화)
- `queryInsurerCheckboxes(d)` 신설: 체크박스 셀렉터 3후보 순차 시도(① `#companyInfo [company_code]` ② 문서 전역 `[company_code]` ③ `#companyInfo` checkbox 중 `closest('[company_code]')`). 첫 매칭(개수>0) 채택, 전부 실패 시 `[]`(→ null 신호).
- `extractCompanyCode(el)`: 요소에 없으면 `closest('[company_code]')`로 견고 추출.
- `companyScreenPremiumsAllZeroOrEmpty(...)` 신설: 해당 보험사의 선택 담보 화면 셀(#premium_lists)이 전부 0/null/undefined면 미선택 판정.
- `isSelected` 2신호 통합: (1) 체크박스 신호(non-null인데 미포함 → 제외) OR (2) 0원 fallback(`screenSignalPresent`일 때 전 담보 0원 → 제외). 둘 중 하나라도 미선택이면 제거.
- **★ 회귀 방지**: `selectedCodes===null` AND `screenSignalPresent===false`(신호 둘 다 부재) → 전부 포함(기존 동작 유지, task-2858 fallback·prd 테스트 보존).
- 선택 0개 → perInsurer 빈 배열 → `classifyOverallStatus`가 `REQUIRES_REVIEW`(별도 sentinel 불필요). matrix/premiums 격자는 per_insurer 파생이라 자동 정합.

## 수정 파일
- `extension/content.js` (+103행): P1 SOFT/HARD 분리, P2 다중 셀렉터 + 0원 fallback + 2신호 isSelected.
- `extension/__tests__/task-2859-classify-and-filter.test.ts` (신규, 353행, 23 테스트).
- `extension/__tests__/track-a-preview.test.ts` (기존 1개 assertion 갱신): "일부 실패(API_MISSING)→REQUIRES_REVIEW"는 **구 동작을 인코딩**한 것 → task 지시대로 "OUT_OF_TOLERANCE→REQUIRES_REVIEW" + "API_MISSING→OK" 2케이스로 교체(의도적·필수 갱신, 우회 아님).

## 테스트 결과
- **`npx vitest run extension/__tests__/` — 22 files, 462 tests PASS (팀장 독립 재실행 확인).**
  - 신규 23 (task-2859) + 기존 439, 회귀 0.
  - 기존 `task-2858-per-insurer-filter`, `contract-golden*`, `prd1-*`/`prd2-*`, `track-a-*` 전부 통과.
- P1 커버: API_MISSING만→OK / SOFT 다종만→OK / OUT_OF_TOLERANCE→REQUIRES_REVIEW / 하드+소프트 혼재→REQUIRES_REVIEW / NEGATIVE_INVALID만→REQUIRES_REVIEW(HARD 회귀) / 빈 실패→OK / perInsurer 빈→REQUIRES_REVIEW / 통합(disclosure failure_codes에 API_MISSING 포함).
- P2 커버: 체크박스 2사 checked→2사만 / 체크박스 부재+1사 0원→2사(fallback 작동) / 체크박스 부재+셀 0개→3사 전부(include-all 회귀) / 체크박스+화면 0원 합의→1사.

## L1 스모크테스트 결과
- 서버 재시작: **해당없음** (크롬 확장 content script — 상시 기동 서버 없음).
- API 응답 확인: **해당없음** (외부 인증 사이트 ohmymanager DOM 의존, curl 불가).
- 스크린샷: **해당없음** (실 브라우저 E2E는 ohmymanager 로그인 세션 필요 → ANU 라이브 파일럿 왕복에서 수행 예정, 순서 B).
- **실동작 검증 대체**: `buildImmutablePreviewSnapshotV1` 통합 테스트를 JSDOM으로 실제 시나리오(API_MISSING + 합계 정상 → overall_status OK + failure_codes 유지) end-to-end 구동. 순수 로직 content.js 변경이라 vitest 통합이 L1 실동작에 해당. pytest 아닌 vitest 462 PASS 실행 확인.

## 게이트
- **G1 설계**: affected_files = `extension/content.js` + `extension/__tests__/**`만. forbidden(src/server/.github/manifest/background) 0건 변경(diff 스코프 검증). 다른 팀 겹침 없음.
- **G2 구현**: 팀장 독립 diff 리뷰 + 전체 vitest 재실행(462 PASS). consent/egress/taint/전송 로직 diff 변경 0건 확인.
- **G3 머지**: merge_policy=none → **ANU가 독립검증·Codex 검토·머지·파일럿 재빌드 담당**(순서 B). 봇=push+PR+.done까지.

## 머지 판단
- **머지 필요**: Yes (단, merge_policy=none → **ANU 소유**, 팀장 머지 안 함)
- **브랜치**: `task/task-2859-dev2`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2859-dev2`
- **base**: `origin/main` (base_sha 3c41b05)
- **머지 의견**: 스코프 최소(content.js + 테스트), 462 PASS 회귀 0, consent/egress/taint 불변. 안전하게 머지 가능.

## ANU/파일럿 라이브 검증 시 유의점 (watch-item)
- **P2 0원 fallback의 partial-screen-signal 경계**: `findScreenCellPremiumMap`이 실 DOM에서 일부 보험사 셀만 잡고 정상 선택 보험사 셀을 못 잡으면(셀렉터 `em[company_code][coverage_cd][premium]`가 실 마크업과 불일치) 해당 보험사가 오제외될 수 있음. task 지시대로 "화면 0원=미선택"을 authoritative 2차 신호로 채택한 결과이며, 셀이 아예 없으면(screenSignalPresent=false) fallback 비활성 → 안전 강등. **라이브 왕복에서 실제 #premium_lists 셀 구조가 셀렉터와 일치하는지, 선택 보험사가 오제외되지 않는지 확인 권장.**

## 모델 사용 기록
- 토르(백엔드, sonnet): content.js P1/P2 구현 + 테스트 작성. (haiku 미사용 — 로직/계약 판정이라 sonnet 정당)
- 팀장(Opus): 설계 확정, diff 독립 리뷰, 전체 vitest 재실행, 스코프/consent 검증.

## 커밋
- `b55fded` P1 classifyOverallStatus SOFT_DOWNGRADE
- `2ed6cc8` P2 filter robustness (multi-selector + 0원 fallback)
- `024f87f` MT-3 tests (23 new + track-a-preview 갱신), 462 PASS

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

