# task-2336 보고서 — InsuRo 복합설계 Chrome Extension 피벗

**팀**: dev5-team (마르둑 팀장, 엔키 백엔드, 이쉬타르 프론트, 나부 UX/UI)
**작업 레벨**: Lv.4 (한정승인)
**작업 시각**: 2026-05-01 19:24 ~ 19:51 KST (약 27분)
**워크트리**: `/home/jay/projects/InsuRo/.worktrees/task-2336-dev5` (브랜치: `task/task-2336-dev5`)
**PR**: https://github.com/JonghyukJeon/InsuRo/pull/72

---

## S - Situation (배경)

기존 Phase 1(task-2333/2335)에서 백그라운드 수집기(`ohmy_premium_collector.py`) + Tailscale exit node 분산 구조를 완성했지만, MVP 부적합 사유 5건이 누적됐다 — 3일 수집 시간, Tailscale 인프라 비용/복잡도, JWT 23h 만료 사이클 차단 위험, IP 차단 탐지 가능성, 74 plan_id 시드 부담.

## C - Complication (문제)

자동화 트래픽은 항상 탐지 위험을 안고 있다. 또한 ohmymanager에 plan list endpoint가 없어 plan_id 74개를 별도로 시드해야 했다 — 즉, 데이터 신선도/커버리지 모두 운영 부담이 컸다.

## Q - Question (요구사항)

탐지 위험 0 + 인프라 0 + JWT 자동 갱신 + 신선도 100% 데이터 동기화 경로를 제공하라. 외부 노출 없이 인카금융서비스 FA에 한정하라.

## A - Answer (해결)

### 새 아키텍처

```
[사용자 브라우저]
  ohmymanager.com (사용자 평소 사용)
    └─ Chrome Extension (사이드로드)
        ├─ ohmymanager API 응답 자동 캡처 (fetch + XHR override)
        ├─ ohmymanager JWT 보존 (확장 내부에서만)
        └─ InsuRo 백엔드로 POST (Supabase JWT 인증)

  insuro.biz/composite-design
    ├─ 확장 설치 감지 (chrome.runtime.sendMessage PING)
    ├─ "최근 본 조건" 카드 (recent-views API)
    └─ 미설치 시 /composite-design/setup 가이드 안내
```

### 산출물 — 신규 파일

| 파일 | 설명 |
|---|---|
| `extension/manifest.json` | Manifest V3 (storage, scripting, activeTab, alarms 권한, externally_connectable) |
| `extension/inject.js` | page world fetch+XHR override → /api/ProductPremiums 응답 캡처 → window.postMessage |
| `extension/content.js` | inject.js 주입 + page→background 메시지 릴레이 + ohmymanager JWT 보존 |
| `extension/background.js` | InsuRo POST + 5회 재시도 큐 + chrome.alarms (30초 주기) + onMessageExternal (PING/SET_INSURO_JWT) |
| `extension/popup.html`, `popup.js` | 상태 패널 (연결/푸시/큐/최근 시각) |
| `extension/options.html`, `options.js` | 설정 페이지 (계정 연결/베이스 URL/활성화/로그) |
| `extension/icons/icon{16,48,128}.png` | placeholder PNG (인디고 배경 + IR 텍스트, 나부) |
| `extension/README.md` | 사이드로드 절차 + 권한 사용 이유 + 응답 구조 + 보안 |
| `server/migrations/010_ohmy_user_views.sql` | `ohmy_user_views` 테이블 + 2 인덱스 |
| `server/config/extension_version.json` | 확장 최신 버전 메타데이터 (v0.1.0) |
| `server/tests/test_composite_ingest.py` | pytest 3건 (ingest/version/recent-views) |
| `src/lib/extensionBridge.ts` | pingExtension / setInsuroJwt 헬퍼 |
| `src/pages/CompositeExtensionGuide.tsx` | 5단계 설치 가이드 + FAQ + 면책 + 확장 감지/연결 버튼 |
| `src/components/ExtensionVersionToast.tsx` | 1일 1회 버전 체크 토스트 (localStorage 캐시) |
| `public/downloads/insuro-helper-0.1.0.zip` | 확장 빌드 산출물 (11,434 bytes, 12 파일) |
| `public/downloads/extension-guide/step{1..5}.png` | 가이드 placeholder 스크린샷 (800x500, 나부) |

### 산출물 — 수정 파일

| 파일 | 변경 |
|---|---|
| `server/main.py` | `CompositeIngestRequest` Pydantic 모델 + 엔드포인트 3개 추가 (라인 6991~7186) |
| `src/pages/CompositeDesign.tsx` | extInstalled 감지 + recent-views 카드 + applyRecentView (확장 미설치 노란 배너 + 최근 5건 grid) |
| `src/config/routes.ts` | `CompositeExtensionGuide` lazy import + `/composite-design/setup` 라우트 |
| `src/components/navigation/navigationConfig.ts` | "Helper 확장 설치" 메뉴 항목 추가 (분석&도구) |
| `src/components/DashboardLayout.tsx` | `<ExtensionVersionToast />` 마운트 |

### 신규 백엔드 엔드포인트

1. **POST `/api/insuro/composite-design/ingest`** (verify_jwt + _verify_incar_member)
   - body: `{plan_id, plan_name, plan_type_name?, insurance_type, age, gender, raw_data}`
   - 처리: ohmy_plans upsert → ohmy_raw_responses INSERT → 32개 target coverage 필터링 → ohmy_premiums 비활성화 후 INSERT → ohmy_user_views INSERT
   - 응답: `{status: ok, ingested: {plan, premiums, view}}`

2. **GET `/api/insuro/composite-design/extension-version`** (verify_jwt)
   - `server/config/extension_version.json` 반환

3. **GET `/api/insuro/composite-design/recent-views`** (verify_jwt + _verify_incar_member)
   - 본인 ohmy_user_views 최근 10건 + ohmy_plans 별도 조회로 plan_name 매핑
   - 응답: `{items: [{plan_id, plan_name, age, gender, viewed_at}]}`

### 발견 이슈 및 해결

#### 이슈 1: PostgREST embed가 FK 미정의로 PGRST200 (Critical)
- **재현**: recent-views 호출 시 `Could not find a relationship between 'ohmy_user_views' and 'ohmy_plans' in the schema cache`
- **원인**: 010 마이그레이션에 plan_id 외래키 미정의 → PostgREST `select(... ohmy_plans(plan_name))` embed 불가
- **해결**: FK 추가 대신 두 단계 분리 조회로 변경 (1) ohmy_user_views select → (2) ohmy_plans `.in_("plan_id", ...)` 매핑. 외래키는 데이터 정합성 위험 없이 코드 레벨 join.
- **회귀 테스트**: `test_recent_views_returns_items` 의 mock을 새 패턴에 맞춰 갱신.

#### 이슈 2: vite/uvicorn 포트 충돌
- **재현**: 8000 포트는 dashboard server 점유, 5173 대신 vite는 8080에서 기동.
- **해결**: 백엔드를 8002로, 프론트는 vite 기본 8080 사용. `.env.local` (gitignored) 에 `VITE_INSURO_API_BASE=http://127.0.0.1:8002` 설정.

#### 이슈 3: 인증 검증이 old project JWKS 참조
- **재현**: 새 Supabase 프로젝트(zayhf...) JWT를 가져왔으나 `Invalid token` 401.
- **원인**: env에 `INSURO_SUPABASE_URL`(old=dmyjp..)이 INSURO_NEW_SUPABASE_URL보다 우선 적용 → JWKS 검증 실패.
- **해결**: 서버 기동 시 `unset INSURO_SUPABASE_URL` 으로 INSURO_NEW_SUPABASE_URL 만 사용.

#### 이슈 4: extension icons placeholder
- **결정**: 디자인팀 호출 없이 PIL로 인디고 배경 + 흰색 "IR" 텍스트 placeholder 생성. 정식 아이콘은 Phase 2 디자인 작업으로 명시.

#### 이슈 5: 가이드 스크린샷 placeholder
- **결정**: 동일하게 PIL로 단계 번호+제목 800x500 placeholder. 실제 사이드로드 스크린샷은 사이드로드 후 캡처 작업으로 별도 dispatch 필요.

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

| 파일 | 검증 방법 | 결과 |
|---|---|---|
| `server/main.py` (3 endpoints + 모델) | py_compile + pytest 3 + L1 curl 4종 | PASS |
| `server/migrations/010_ohmy_user_views.sql` | Supabase Management API 적용 + 컬럼/인덱스 검증 | PASS (HTTP 201) |
| `server/config/extension_version.json` | GET endpoint 200 + body 매칭 | PASS |
| `server/tests/test_composite_ingest.py` | pytest 3건 모두 PASS | PASS |
| `extension/manifest.json` | JSON 파싱 + 권한/배경/콘텐츠스크립트 키 존재 | PASS |
| `extension/inject.js` | grep `__insuroListenerAdded` (Gemini fix) + 빌드 zip 포함 | PASS |
| `extension/content.js` | zip 포함 확인 (12 files) | PASS |
| `extension/background.js` | zip 포함 확인 + grep `chrome.alarms` | PASS |
| `extension/popup.{html,js}`, `options.{html,js}` | zip 포함 확인 | PASS |
| `extension/icons/icon{16,48,128}.png` | PIL Image.open 로드 (16/48/128) | PASS |
| `extension/README.md` | 사이드로드 절차/권한/보안 섹션 | PASS |
| `public/downloads/insuro-helper-0.1.0.zip` | 11,453 bytes / 12 파일 (재빌드 후) | PASS |
| `public/downloads/extension-guide/step{1..5}.png` | PIL 로드 (800x500) + Playwright에서 가이드 페이지 표시 확인 | PASS |
| `src/lib/extensionBridge.ts` | npm run build 통과 + 페이지에서 호출 (`pingExtension`) | PASS |
| `src/components/ExtensionVersionToast.tsx` | grep navigate 제거 (Gemini fix) + DashboardLayout 마운트 확인 | PASS |
| `src/pages/CompositeExtensionGuide.tsx` | Playwright `/composite-design/setup` 렌더 확인 (스크린샷) | PASS |
| `src/pages/CompositeDesign.tsx` | Playwright "InsuRo Helper 확장이 필요합니다" 노란 배너 표시 확인 | PASS |
| `src/config/routes.ts` | Playwright 라우팅 정상 (Setup 페이지 접근) | PASS |
| `src/components/navigation/navigationConfig.ts` | grep "Helper 확장 설치" 매칭 | PASS |
| `src/components/DashboardLayout.tsx` | grep `<ExtensionVersionToast />` 매칭 | PASS |

### pytest 결과

```
server/tests/test_composite_ingest.py::test_ingest_returns_ok PASSED
server/tests/test_composite_ingest.py::test_extension_version_returns_json PASSED
server/tests/test_composite_ingest.py::test_recent_views_returns_items PASSED
server/tests/test_composite_calculator.py (11 tests) PASSED
====== 14 passed in 2.97s ======
```

### Pyright 진단

✘ 표시된 import 에러(`sb_helpers`, `composite_calculator`, `main`)는 모두 **pre-existing pyright pythonpath 미설정 이슈**로 본 작업과 무관. pytest는 `sys.path.insert(0, ...)` 로 정상 동작 확인.
★ 표시 항목은 모두 deprecation/style warning이며 기능 영향 없음.

## L1 스모크테스트 결과 (필수)

- **서버 재시작**: 성공 (uvicorn main:app port 8002, INSURO_NEW_SUPABASE_URL 사용)
- **API 응답 확인**:
  - `GET /api/insuro/composite-design/extension-version` (인카 JWT) → **HTTP 200**, `{"latest_version":"0.1.0","download_url":"/downloads/insuro-helper-0.1.0.zip","release_notes":"..."}`
  - `GET /api/insuro/composite-design/recent-views` (인카, empty) → **HTTP 200**, `{"items":[]}`
  - `POST /api/insuro/composite-design/ingest` (인카, sample raw_data 2 insurer × 2 coverage) → **HTTP 200**, `{"status":"ok","ingested":{"plan":1,"premiums":4,"view":1}}`
  - `GET /api/insuro/composite-design/recent-views` (인카, after ingest) → **HTTP 200**, plan_name 매핑 포함 1건
  - `GET /recent-views` 비인카 사용자 → **HTTP 403** "인카 소속만 접근 가능"
  - `POST /ingest` 비인카 사용자 → **HTTP 403** "인카 소속만 접근 가능"
- **DB 검증** (Supabase Management API):
  - `ohmy_user_views` 테이블 생성 + 2 인덱스 확인
  - 컬럼: `id bigint, user_id uuid, plan_id text, age integer, gender text, viewed_at timestamptz`
  - L1 ingest 후 row 1건 정상 적재 확인 (plan_id=L1_TEST_2336)
- **빌드 결과**: `npm run build` **PASS** (16.0s)
  - dist/sw.js, workbox-5d66ad07.js 등 PWA precache 168 entries 생성
- **Playwright 스크린샷**:
  - `./insuro-2336-extension-guide.png` — `/composite-design/setup` (5단계 + FAQ + 다운로드 버튼)
  - `./insuro-2336-composite-design.png` — `/composite-design` ("InsuRo Helper 확장이 필요합니다" 노란 안내 카드 + "설치 가이드" 버튼)
- **콘솔 에러**: 0건 (JWKS issue 해결 후, Supabase env 설정 후 재로드)
- **Playwright 정리**: `browser_close` 호출 완료, dev 서버(8002, 8080) 모두 종료

## 모델 사용 기록

- 마르둑 (팀장, opus): 설계, 위임, 통합, L1 검증, 이슈 수정 (recent-views FK 회피, JWKS env 정리)
- 엔키 (백엔드, sonnet): MT-1~6 (마이그레이션 + 3 엔드포인트 + 테스트 3건 + 마이크로커밋)
- 이쉬타르 (프론트, sonnet): MT-A1~A8 (확장 9파일) + MT-B1~B7 (프론트 5파일) + MT-C1, MT-D1
- 나부 (UX/UI, haiku): icons 3 + screenshots 5 placeholder (Pillow 기반 단순 렌더, 정식 디자인은 Phase 2)
- haiku 사용 정당성: PIL을 써서 단순 placeholder PNG 생성하는 작업이라 sonnet 불필요.

## 머지 판단

- **머지 필요**: Yes — 완료
- **브랜치**: `task/task-2336-dev5` → main 머지 완료 (커밋 `ed26d26`)
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2336-dev5`
- **머지 의견**: pytest 14/14 PASS, npm run build PASS (worktree + main 양쪽), L1 API 4종 모두 200/200/200/403 정상. 마이그레이션도 운영 DB에 적용 완료. 기존 composite_calculator 11개 테스트 회귀 없음.

## QC 결과 (에스컬레이션)

`finish-task.sh` 실행 시 `git_evidence` 검증의 `NO_UNCOMMITTED` 항목 단 1건만 FAIL → 3회 재시도 초과로 에스컬레이션 상태. 사유:

- 검증기가 `/home/jay/workspace` git tree를 검사. 14개 워크스페이스-레벨 파일이 uncommitted 상태로 잡힘:
  - `config/constants.json`, `dashboard/data/naver-sa-stats.json`
  - `memory/backups/system-spec/2026-04-{06..23}/*.md` (다른 시스템의 자동 백업 파일 삭제 9건)
  - `memory/specs/.spec-state-cache.json`, `memory/specs/anu-system-spec.md`, `memory/specs/anu-system-spec-changelog.md`
- 이 14개 파일은 모두 **다른 워크플로우의 산출물**(시스템 spec 업데이트, 대시보드 데이터 캐시) 로 task-2336과 무관.
- task-2336 자체 산출물(InsuRo 워크트리/main, 보고서)은 모두 정상 커밋·머지·빌드 완료.

**TRUST 카테고리 결과**:
- Tested ✅ (14/14 pytest, npm run build, L1 4종 curl)
- Readable ✅ (style/pyright)
- Unified ✅ (scope_check)
- Secured ✅ (schema_contract — 인카 가드 403 검증)
- Trackable ✅ (file_check, data_integrity)
- _independent ✅ (api_health)

요약: `8 PASS, 1 FAIL, 11 SKIP, 3 WARN` — FAIL은 위 워크스페이스 데몬 파일 NO_UNCOMMITTED 1건뿐.

**.done 파일은 아누 판단 후 수동 생성** 필요.
에스컬레이션 파일: `/home/jay/workspace/memory/events/task-2336.escalate`

## Gemini PR 리뷰 결과

- 리뷰 도착: 60s
- 미수정 High: **0건** → PASS
- Medium 코멘트 2건 모두 수용:
  1. `extension/inject.js`: XHR `send()` 호출 시 `load` 리스너 중복 등록 방지 → `__insuroListenerAdded` 플래그 추가 (커밋 `f9a63f6`)
  2. `src/components/ExtensionVersionToast.tsx`: useEffect 의존성에서 미사용 `navigate` 제거 + `useNavigate` import 정리 (커밋 `f9a63f6`)
- 머지 시각: 2026-05-01T10:50:48Z (PR #72)
- Production main 빌드: PASS (16.18s, dist/ timestamp 19:51)

## 폐기 / 보존

- `server/scripts/ohmy_premium_collector.py` — 코드 보존 (Phase 2 fallback). cron/수동 실행 안 함.
- Tailscale exit node 계획 — Phase 2 fallback으로 표시. 본 PR에서 인프라 구성 없음.

## 후속 액션

1. **정식 아이콘/스크린샷 교체** — 디자인팀 dispatch (Phase 2). 현재 placeholder PNG는 기능 점검용.
2. **확장 ID env 설정** — 사이드로드 후 받은 확장 ID를 `VITE_INSURO_HELPER_EXT_ID` 로 frontend 빌드에 주입해야 `pingExtension()`이 작동. README의 "1차 사이드로드 → 확장 ID 환경변수 설정 → 재배포" 절차 필요.
3. **ohmymanager request body 구조 확정** — `extractIngestPayload()` 가 plan_id/age/gender 추출에 의존. 실제 사용 시 응답 캡처 → 첫 사용자 ingest 결과로 검증 필요.
4. **확장 권한 minimization 재검토** — `activeTab` 권한이 chrome.tabs.create 외에 사용 안 됨. 추후 정리 가능.

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


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


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


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


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


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


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


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

