# [task-2928] InsuRo 개인정보 P0-1 Drive 프록시 Phase 1.5 — 백엔드 보완 (Codex 조율 반영)

- 팀: dev1-team (헤르메스/불칸/아르고스)
- 브랜치: `task/task-2928-dev1` (worktree) · HEAD `22c3f3c`
- base: **로컬 main `f81085f`(Phase 1 task-2927 포함)** — 아래 "머지 판단" 참조
- 작업일: 2026-08-09

---

## Situation
Phase 1(백엔드 코어: file-ticket 발급·무인증 스트리밍 프록시·drive_file_id 정규화 컬럼+과거 백필·ES256 전용)이 완료됐다. 그러나 Codex 교차검증에서 **프론트 배선(Phase 2) 진입 전 반드시 고쳐야 할 백엔드 갭 4건**이 도출됐다.

## Complication
1. 마이그레이션은 **과거 데이터 백필만** 했다 → 신규 업로드/분석이 `drive_file_id(s)`를 저장하지 않으면 새 레코드는 ticket IDOR 조회에서 누락 → **새 파일 열람 불가**.
2. `GET /file/{ticket}`가 **진입 즉시 1회 소비(pop) + 항상 attachment** → `<img>`·PDF뷰어·HTTP Range 재요청이 2번째부터 404 → "보기"가 다운로드로 귀결.
3. 다중계정 탐지 미들웨어가 `["ES256","HS256"]` 허용 → verify_jwt(ES256 전용)와 불일치(알고리즘 컨퓨전 여지).
4. ticket 인메모리 저장소가 단일 워커 전제인데 명시가 없음.

## Question
프론트 배선 전, 위 4건을 회귀 0으로 보완할 수 있는가?

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

### 수정 1. 신규 쓰기 경로 `drive_file_id` 배선 (불칸)
- `upload_to_drive` record_data(main.py L6216): `"drive_file_id": gdrive.extract_drive_file_id(drive_url)` 추가 → premium/newsletter 공통 경유.
- `analyze_policy` policy_analyses insert(main.py L6991~): results 각 `file_url`→id 추출해 `"drive_file_ids": _drive_file_ids`(text[]) 저장.
- **전수 점검 결과(추가 경로 없음)**: `upload_pdf` 호출부 2곳 중 L942(`upload_fcpa_pdf`)는 `fcpa_config.pdf_url`에 저장 → ticket 공개등급 인가가 그 URL을 직접 파싱·대조하므로 별도 컬럼 불필요(무변경). `source_file_ref`(L5765)는 감사용 JSON 필드로 ticket 인가 참조 컬럼과 무관.

### 수정 2. view(inline) 모드 + 짧은TTL 다회 토큰 + Range (불칸)
- `file_ticket_store.py`: `_Ticket.mode` 추가, `VIEW_TTL_SECONDS=120`, `create_ticket(..., ttl_seconds=, mode=)`, 신규 `peek_ticket()`(비소비 조회), `consume_ticket()` 반환에 `mode` 포함.
- `create_file_ticket`: `FileTicketRequest.mode`(기본 download). view→TTL 120·mode=view, download→TTL 300. 잘못된 mode **400**. 등급별 IDOR 인가는 **mode 무관 동일**. 응답 `{ticket_id, expires_in, mode}`.
- `get_file_by_ticket(ticket_id, request)`: **peek 우선** → download는 `consume`(1회소비 확정, replay 방지 유지), view는 미소비(TTL로만 무효). Disposition view=inline/download=attachment. `Accept-Ranges: bytes`. view+Range→`fetch_drive_file_range`로 부분바이트 **206**(`Content-Range`,`Content-Length`), 범위초과/형식오류→**416**(`Content-Range: bytes */{size}`). download는 Range 무시·전체 스트리밍. `nosniff`/`no-referrer` 유지.
- `gdrive.fetch_drive_file_range(file_id,start,end)` 신설: get_media + `Range` 헤더로 부분만 메모리 적재(OOM 회피).

### 수정 3. 미들웨어 ES256 통일 (불칸)
- main.py L383 `algorithms=["ES256","HS256"]` → `["ES256"]` (grep 확인 완료).

### 수정 4. ticket store 외부화 TODO (범위 밖, 주석만) (불칸)
- `file_ticket_store.py` 상단 `TODO(멀티워커)`: 다중 워커 전환 시 워커간 미공유→404, Redis 등 외부화 필요 명시.

---

## L1 스모크테스트 결과 (실서버 기동)
- **서버 재시작**: 성공 — worktree 코드로 `uvicorn main:app :8099` 기동, `Application startup complete`.
- **API 응답 확인(curl)**:
  - `GET /api/status` → **200** (앱 부팅 정상)
  - `GET /api/insuro/file/bogus-ticket-xyz` → **404** `{"detail":"유효하지 않거나 만료된 티켓입니다"}` — **변경된 get_file_by_ticket의 peek 경로가 라이브임을 실증**(무인증 capability 경로).
  - `POST /api/insuro/file-ticket`(무인증) → **401** `Missing or invalid authorization` — verify_jwt 인가 게이트 정상(mode 검증은 인가 통과 후 단계, TestClient 통합테스트로 400 검증됨).
- **서버 정리**: 포트 8099 clear 확인(잔존 프로세스 0).
- 스크린샷: 해당없음(백엔드 API 작업).

## 테스트 (아르고스)
- 신규 41케이스 + 기존 store 테스트 2건 mode 계약 갱신.
  - `test_file_ticket_store.py`(+): view peek 다회 비소비 / consume mode무관 pop / mode·TTL 배선 / TTL 만료·`VIEW_TTL_SECONDS==120`.
  - `test_file_ticket_view_endpoint.py`(신규): view 2·4회 GET 모두 **200**(inline+Accept-Ranges) / Range **206**(Content-Range·부분바이트)·재조회 성공 / 범위·형식오류 **416** / download 1회 후 **404**·Range 무시 200 / view IDOR **403** / 잘못된 mode **400**.
  - `test_drive_file_id_wiring.py`(신규): upload_to_drive record_data `drive_file_id`(premium/newsletter) / analyze_policy `drive_file_ids` 배열(실 클로저 직접 await 관찰) / extract 파서.
- **회귀 전체**: `1 failed, 1377 passed`. 유일 실패 = `test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset` — **base 브랜치 기존 결함(팀 메모리 기록), 본 작업 무관·재현 확인**. → **회귀 0**.
- py_compile: main.py/file_ticket_store.py/gdrive.py OK.

## 발견 이슈 및 해결
- **Worktree stale base(중요)**: worktree_manager가 `origin/main`(f2b282e=task-2925)을 base로 잡았으나 **Phase 1(task-2927)은 origin에 미푸시(배포 HOLD)** 상태로 로컬 main(f81085f)에만 존재 → worktree에 Phase 1 코드 부재. 해결: worktree 브랜치를 로컬 main으로 `reset --hard` 하여 Phase 1 위에 쌓고, base-marker JSON을 f81085f로 갱신(scope-guard가 Phase 1.5 diff만 측정하도록). 관련 메모리: `finish_task_worktree_stale_base`.
- Pyright 임포트 경고(file_ticket_store/gdrive/main 등)는 PYTHONPATH=server 미설정에 따른 **기존 패턴**이며 런타임 무관(pytest는 PYTHONPATH=server로 전부 통과).

## 수정/생성 파일
- 수정: `server/main.py`(+93/-21), `server/file_ticket_store.py`, `server/gdrive.py`
- 신규 테스트: `server/tests/test_file_ticket_view_endpoint.py`, `server/tests/test_drive_file_id_wiring.py`, `test_file_ticket_store.py`(케이스 추가)
- 총 6파일, +734/-21

## 모델 사용 기록
- 불칸(백엔드 구현): sonnet — 로직 구현
- 아르고스(테스트): sonnet — 테스트 설계/작성
- 헤르메스(팀장): 직접 코딩 없음(설계/검토/통합/L1 스모크/worktree base 교정만)

---

## 머지 판단
- **머지 필요**: Yes
- **브랜치**: `task/task-2928-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2928-dev1`
- **머지 의견**: 코드만 변경(마이그레이션 없음 — 컬럼 기존 존재). 회귀 0, L1 실서버 스모크 통과. **단 base 주의**: 본 브랜치는 **로컬 main(f81085f, Phase 1 미푸시)** 위에 쌓였다. Phase 1이 origin/main에 아직 없으므로, 머지·배포는 **Phase 1(task-2927)과 함께 origin push + systemd `insuro-api` 재기동을 ANU가 조율**해야 한다(Phase 1 배포 HOLD 해제와 동시 진행 권장). finalize-only. finish-task 후 원격 head 확인 필요.

## 완료 보고 8항목
1. 수정 파일/함수 diff: 위 "수정 1~4" + diffstat.
2. drive_file_id 배선 지점: upload_to_drive record_data / analyze_policy insert / 전수점검(추가 없음).
3. view 모드·다회토큰·Range: peek/consume 분기, 206/416, fetch_drive_file_range.
4. 미들웨어 ES256: main.py L383 `["ES256"]`.
5. ticket store TODO: file_ticket_store.py 상단.
6. 회귀/스모크: 1377 passed(회귀0, CORS 기존1 무관) + L1 실서버 curl 404/401/200.
7. PR/머지 상태: worktree PR 생성 예정(G3), 머지=ANU(Phase 1과 동시 배포 조율).
8. callback: executor self-발사 0, ANU normal collector(독립 key) 등록 — OS-level pickup 계약 준수.

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

