# [task-2929] InsuRo P0-1 Drive 프록시 — Gap 1 보완: FCPA drive_file_id 일관성

- 팀: dev1-team (헤르메스/불칸/아르고스)
- 브랜치: `task/task-2929-dev1` (worktree) · HEAD `a87b77e`
- base: **`task/task-2928-dev1` `22c3f3c`** (Phase 1.5 위에 이어서 — task-2928+2929 함께 머지 예정)
- 작업일: 2026-08-09
- 레벨: Lv.2 · 검증레벨: normal

---

## Situation
P0-1 Drive 프록시 Phase 1(task-2927 머지·배포 완료) + Phase 1.5(task-2928 검증완료·머지대기). Codex 교차검증에서 **Gap 1** 도출: FCPA 가이드 업로드 경로가 `fcpa_config.pdf_url`만 저장하고 `drive_file_id`를 안 채움. 다른 3개 테이블(newsletters/premium_data/policy_analyses)은 이미 `drive_file_id`로 통일된 상태.

## Complication
FCPA 인가(`server/main.py:5915` 부근)는 `pdf_url`에서 **실시간 file_id 추출**해 매칭하므로 기능상 무해하나, 다른 테이블과 **일관성 부재** → Codex 합의 완성을 위해 FCPA도 `drive_file_id`로 통일 필요. 단 기존 `pdf_url` 추출 매칭은 하위호환 폴백으로 유지해야 함.

## Question
회귀 0으로 FCPA를 `drive_file_id` 우선 매칭으로 통일하되 `pdf_url` 폴백을 유지할 수 있는가?

## Answer — 3건 전부 구현·검증 완료

### 수정 1. 마이그레이션 (불칸)
신규 파일 `supabase/migrations/20260809120000_fcpa_drive_file_id.sql`:
- `ALTER TABLE fcpa_config ADD COLUMN IF NOT EXISTS drive_file_id text;`
- 백필: `UPDATE ... SET drive_file_id = substring(pdf_url from '/file/d/([^/]+)') WHERE drive_file_id IS NULL AND pdf_url ~ '/file/d/';`
- 인덱스: `CREATE INDEX IF NOT EXISTS idx_fcpa_config_drive_file_id ON fcpa_config(drive_file_id);`
- 기존 `20260808120000_drive_file_id_normalization.sql` 패턴과 동일. `ADD COLUMN IF NOT EXISTS`·`IF NOT EXISTS` 인덱스로 멱등.

### 수정 2. FCPA 업로드 배선 (불칸) — `server/main.py:959`
`upload_fcpa_pdf`의 `upsert_payload`에 추가:
```python
"drive_file_id": gdrive.extract_drive_file_id(drive_url),
```
`drive_url = gdrive.upload_pdf(...)`(L942)로 이미 생성된 값 사용. insert/update 양쪽 경로 공통 경유.

### 수정 3. 인가 로직 통일 (불칸) — `server/main.py:5915~5926`
```python
fr = sb.table("fcpa_config").select("drive_file_id,pdf_url").execute()
for row in (getattr(fr, "data", None) or []):
    # drive_file_id 우선(다른 테이블과 일관), 없으면 pdf_url 실시간 추출 폴백(하위호환)
    row_fid = row.get("drive_file_id") or gdrive.extract_drive_file_id(row.get("pdf_url") or "")
    if row_fid and row_fid == file_id:
        grade = "public"
        break
```
- `drive_file_id` 우선 → 없으면 `pdf_url` 추출 폴백(하위호환 유지).
- **file_id None 방어(이중화)**: `create_file_ticket`(L5857~) 진입 시 `if not file_id: raise 400`으로 이미 file_id truthy 보장. 추가로 `if row_fid and ...` 가드로 양쪽 컬럼 모두 빈 fcpa_config row의 우연매칭을 명시적 차단(defense-in-depth).

### 검증 (아르고스)
신규 테스트 4건:
- `test_file_ticket_endpoint.py::test_public_fcpa_allowed_via_drive_file_id_field` — `drive_file_id`만 있고 `pdf_url=None` → **200** (우선 매칭 경로).
- `test_file_ticket_endpoint.py::test_public_fcpa_fallback_via_pdf_url_when_no_drive_file_id` — `drive_file_id=None, pdf_url` 존재 → **200** (폴백 경로).
- `test_drive_file_id_wiring.py::...::test_insert_path_includes_drive_file_id` / `test_update_path_includes_drive_file_id` — FCPA 업로드 upsert_payload에 `drive_file_id` 채워짐(insert/update 양경로).
- `_sb_mock` docstring `fcpa_config: .select(...)`을 `drive_file_id,pdf_url`로 코드 계약 일치 갱신.

---

## L1 스모크테스트 결과 (실서버 기동)
worktree 코드로 `uvicorn main:app 127.0.0.1:8097` 실기동(실 `.env` 임시 심볼릭 링크, gitignore 확인·스모크 후 제거).

- **서버 재시작**: 성공 — `Application startup complete`.
- **API 응답 확인(curl)**:
  - `GET /api/status` → **HTTP 200**
  - `GET /api/insuro/file/bogus-ticket-xyz` → **404** `{"detail":"유효하지 않거나 만료된 티켓입니다"}` — 티켓 프록시 경로 라이브.
  - `POST /api/insuro/file-ticket`(무인증) → **401** `Missing or invalid authorization` — 인가 게이트 정상(변경된 FCPA 등급판정은 인가 통과 후 단계로, TestClient 통합테스트 `test_public_fcpa_*`로 200 실증).
- **스크린샷**: 해당없음(백엔드 API 작업).
- **서버 정리**: 포트 8097 clear, 잔존 uvicorn 0 확인.

---

## 테스트 결과 (전체 회귀)
```
1381 passed, 1 skipped, 38 warnings in 160.14s
```
- **회귀 0건.** 신규 4테스트 전부 PASS.
- pre-existing CORS 테스트(`test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset`)는 이 환경(worktree clean)에서 단독/전체 모두 **PASS**로 별도 예외 불필요.
- `python3 -m py_compile server/main.py` → OK. 마이그레이션 SQL sanity(컬럼/인덱스/DROP 부재) OK.

## 발견 이슈 및 해결
- **worktree 루트 `.env` 부재로 실서버 부팅 시 env 누락**: main.py가 `parents[1]/.env` 로드 → worktree에는 없음. 실 `.env`(gitignore 확인)를 임시 심볼릭 링크 → 스모크 후 제거. 커밋 오염 0.
- **Pyright import 경고**(`file_ticket_store`/`gdrive`/`main` 미해석): server/ 상대 import를 루트 cwd Pyright가 미해석하는 **기존 구조 특성**(pytest는 conftest로 정상 해석 — 1381 passed 실증). 본 변경 무관.

---

## 머지 판단
- **머지 필요**: Yes
- **브랜치**: `task/task-2929-dev1` (base `task/task-2928-dev1` `22c3f3c`)
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2929-dev1`
- **HEAD**: `a87b77e`
- **머지 의견**: task-2929는 task-2928 위에 이어진 소규모 보완(4파일·+135/-4). 회귀 0·L1 통과. **task-2928 + task-2929를 함께 머지**해야 함(2929는 2928 코드 위에 커밋됨). Gap 1 해소로 Codex 합의 완성 → 회장님 지시("codex 합의 시 go")에 따라 **ANU 독립검증 → Codex 재조율 → 합의 시 함께 머지 + 마이그레이션 적용(fcpa_config drive_file_id 백필 포함) + systemd insuro-api 재배포 + L1 스모크** 흐름.

## 배포 (finalize-only)
- 마이그레이션 있음 → **ANU가 Management API로 적용**(`20260808120000` + `20260809120000` 순서). 코드 머지=ANU, systemd 재배포=ANU.
- `callback_schedule_created: false` — OS-level pickup runner 소유.

## 모델 사용 기록
- 불칸(백엔드 구현): sonnet — 로직/마이그레이션 구현.
- 아르고스(테스트): sonnet — 테스트 작성/회귀. (haiku 미사용)
- 헤르메스(팀장): 설계/분배/검토/L1 스모크/통합만 수행(직접 코딩 없음).

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

