# task-2828 보고서 — [Lv.4] Integration Phase2 PR-D0: V 레코드 GET 조회 + 확장 CORS (서버 선행)

- 팀: dev1-team (헤르메스 팀장 · 불칸 백엔드)
- 리포: InsuRo · base=origin/main `452ed14` (Phase1 PR-C 완료분)
- worktree: `/home/jay/projects/InsuRo/.worktrees/task-2828-dev1` · branch `task/task-2828-dev1`
- 머지 정책: **머지 금지**(FINALIZE_ONLY) — PR open 상태로 종결, ANU 독립검증 대기

## Situation
Integration Phase1(서버 V ingest·CRM 후보검색)이 origin/main 에 완료됐다. 그러나 후속 PR-D(확장)·PR-E(웹앱)가 막혀 있다.

## Complication
1. V 레코드는 `POST /consultation-history/v1`(ingest)만 있고 **읽을 GET 엔드포인트가 없어** 웹앱이 나중 조회·재분석을 못 한다.
2. 확장이 insuro 서버를 호출하려면 CORS allowlist 에 chrome-extension origin 이 필요한데 현재 없다(전체개방 없이 정확 origin 만 허용해야 함).

## Question
읽기 전용으로 GET 조회(목록+단건)를 추가하고, 확장 origin 을 **정확 매칭·fail-closed** 로 CORS 에 허용하되, ingest·스키마·멱등·계약·migration 회귀를 0 으로 유지하려면?

## Answer (구현)

### 1. V 레코드 GET 조회 엔드포인트 (`server/routes/consultation_history_v1.py`, +131줄)
- `GET /api/insuro/consultation-history/v1` (목록) — `{"records":[...], "count":N}`, `created_at` 최신순, `limit(100)`.
- `GET /api/insuro/consultation-history/v1/{record_id}` (단건) — 0건이면 404 `CONSULTATION_HISTORY_NOT_FOUND`.
- **게이트**: ingest 와 **동일 Depends 순서** — `Depends(_require_feature_enabled)` → `Depends(_require_pilot_allowlisted)`. flag OFF 유지(default-disable). 파일럿 allowlist 밖 계정 403.
- **fa_account_id 서버파생**: `_require_pilot_allowlisted` 반환값만 사용(payload/query param 미수용) → IDOR 원천 차단.
- **뷰 기반**: `OK_ONLY_VIEW = "consultation_history_v1_ok_only"` **뷰만** 조회(원본 테이블 직접 접근 금지). 뷰가 `record_status='OK' AND deleted_at IS NULL` 를 강제 → REQUIRES_REVIEW·삭제분 노출 0.
- **per-FA 스코프**: 모든 조회에 `.eq("fa_account_id", fa_account_id)`(단건은 `.eq("id", record_id)` 추가) → 타 FA 레코드 반환 0.
- **응답 화이트리스트 projection(금지필드 0)**: `_ALLOWED_RESPONSE_FIELDS`(18개) 로 select 컬럼 명시 + `_project_record()` 응답 구성 — **이중 방어**. `analysis_matrix` 포함(웹 재분석용). 제외: `fa_account_id, body_sha256, idempotency_key, operation_scope, deleted_at`.
- supabase 예외 → `_generic_error(500, "CONSULTATION_HISTORY_QUERY_FAILED")`(계약 5종 enum 밖 신규 enum 신설 없음).

### 2. 확장 origin CORS 허용 (`server/main.py`, CORS 블록)
- 기존 origin 10종을 `_cors_allow_origins` 리스트로 이동(값·순서 무변경).
- `INSURO_EXTENSION_ORIGIN` env 가 truthy 일 때만 정확 origin append. **미설정/빈값=fail-closed**(확장 origin 미허용). 와일드카드(`*`)·정규식 전체개방 없음.
- `allow_methods/allow_headers/allow_credentials` 무변경. `os` 는 기존 import 재사용.

### 3. 테스트 (`server/tests/test_consultation_history_get.py`, 신규 15케이스)
tenant 경계/IDOR·404, ok-only 뷰 사용 증명(원본 테이블 접근 시 AssertionError), analysis_matrix 포함 + 금지필드 5종 부재, select 화이트리스트 이중방어, 파일럿 게이트 403(allowlist 밖·flag OFF), 목록 형태, CORS(fail-closed/설정시 포함/기존 origin 회귀 0) 커버.
- CORS 테스트는 `sys.modules["main"]` 오염 방지를 위해 **격리 서브프로세스**에서 지정 env 로 main 재import → origins 목록 + 실제 preflight ACAO 회수 방식으로 구현(후속 테스트 오염 0).

## 검증 결과

### 회귀/단위 (팀장 독립 재실행)
- `py_compile routes/consultation_history_v1.py main.py` → OK
- 전체 pytest: **824 passed** (baseline 809 → +15 신규, 감소 0·회귀 0)

### L1 스모크테스트 (실제 서버 기동 · curl)
- **서버 재시작**: 성공 (uvicorn `main:app` 127.0.0.1:8791, feature flag OFF 로 기동)
- **API 응답 확인 (curl)**:
  - `GET /api/insuro/consultation-history/v1` (feature OFF) → **HTTP 403 `FEATURE_DISABLED`** (게이트 실동작·default-OFF 유지)
  - `GET .../v1/{id}` (feature OFF) → **HTTP 403 `FEATURE_DISABLED`**
  - `/openapi.json` → 두 라우트 마운트 확인: `/api/insuro/consultation-history/v1`, `/api/insuro/consultation-history/v1/{record_id}`
  - CORS preflight `Origin: https://insuro.biz` → `access-control-allow-origin: https://insuro.biz` (**기존 origin 회귀 0**)
  - CORS preflight `Origin: chrome-extension://...` (env 미설정) → **ACAO 없음** (**fail-closed 실동작**)
- **스크린샷**: 해당없음(백엔드 API 작업)

## 발견 이슈 및 해결
- (불칸) CORS 테스트 초안이 `importlib.reload(main)` 로 공유 `sys.modules` 를 오염시켜 후속 `test_get_user_plan.py` 2건을 깨뜨림 → 격리 서브프로세스 방식으로 전환해 해결(전체 824 그린 재확인).

## 파일 목록
- 수정: `server/routes/consultation_history_v1.py`(+131), `server/main.py`(CORS 블록)
- 신규: `server/tests/test_consultation_history_get.py`(15 케이스)
- diff scope: 허용 3경로 외 변경 0 (`git diff --stat` 검증). ingest/스키마/멱등/계약/migration 무변경.

## 머지 판단
- **머지 필요**: No (task 지시 **머지 금지** — PR open 종결, ANU 독립검증 대기)
- **브랜치**: `task/task-2828-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2828-dev1`
- **머지 의견**: 읽기 전용 additive. 회귀 0, per-FA·ok-only·화이트리스트·CORS fail-closed 전부 실동작 검증. Phase1 계약 무변경. Gemini High 0 시 머지 후보이나, task 지시상 자동머지 금지 → ANU/회장 승인 대기.
- **PR**: [Jeon-Jonghyuk/InsuRo#117](https://github.com/Jeon-Jonghyuk/InsuRo/pull/117) (OPEN, 미머지)

## 종결 경위 (closeout)
- finish-task **FINALIZE_ONLY=1**(머지 금지) + **FINISH_TASK_WORKTREE_STRICT=1** + worktree cwd 실행.
- QC=WARN(FAIL 아님·통과), gates 전부 PASS(impact_scanner/ci_preflight/l1_smoketest/goal_assertions/unresolved). merge 스킵(`.finalize-only`), `.done` status=done 생성.
- 1차 포그라운드 실행이 백그라운드 자식 fd 보유로 하네스 최대 타임아웃(SIGTERM)까지 대기 → 완전 detach(setsid+nohup+fd 분리) 재실행으로 멱등 종결(qc-done/finalize-only 스킵, .done+통지만 완료).
- ANU closeout: executor result.json(**owner_pickup_required=true, authoritative=false, raw ANU key 0** — self-key 자가발사 0) → OS-level pickup runner 가 owner-proof 후 결정론 closeout 소유. `anu-notified`·`done.notified` 마커 생성 확인.

## 모델 사용 기록
- 불칸(백엔드): sonnet(general-purpose) — GET 엔드포인트·CORS·테스트 구현. haiku 미사용.
- 헤르메스(팀장, Opus): 설계·분배·diff/코드 검증·L1 스모크테스트·독립 회귀 재실행.

## 게이트
- G1(설계): affected_files = routes/consultation_history_v1.py · main.py · tests. 타 팀·타 파일 겹침 0(forbidden_paths 전부 미변경).
- G2(구현): 팀 테스터 케이스 15 + 팀장 독립 회귀 824 PASS + L1 실동작.
- G3(머지): PR open → Gemini 리뷰 → **자동머지 금지**(task 지시) → ANU 독립검증.

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

