# task-2930 — [InsuRo P0-1] Drive 프록시 Phase 2: 프론트 링크 프록시 배선 + E2E

- 팀: dev1-team (헤르메스)
- 팀원: 이리스(프론트), 아르고스(테스터)
- 브랜치: `task/task-2930-dev1` (base=origin/main `f2b282e`)
- 워크트리: `/home/jay/projects/InsuRo/.worktrees/task-2930-dev1`
- 완료 정책: **finalize-only (머지=ANU)**

---

## Situation
P0-1 Drive 프록시 백엔드(Phase 1+1.5)는 **라이브 배포 완료**. Drive `/view` 직접 URL은 아직 공개(anyone) 상태이나, 프론트가 실제로 **서버 프록시(file-ticket → 스트리밍)** 를 통해 파일을 열도록 배선하는 단계.

## Complication
- 프론트 7~8개 지점이 Drive `file_url`을 `window.open`/`<a href>`로 직접 열고 있어 접근통제 우회.
- async ticket 발급 후 `window.open`은 **브라우저 팝업차단**에 걸림(Codex 지적).
- 이미지 프리뷰(`<img src>`)는 view 모드(다회+Range) ticket URL이 필요.

## Question
백엔드 계약을 건드리지 않고, 프론트만으로 (1) 프록시 배선 (2) 팝업차단 회피 (3) view/download 구분 (4) 이미지 프리뷰를 무중단으로 구현할 수 있는가?

## Answer
신규 헬퍼 `src/lib/driveFile.ts` 도입 + 링크 8개 지점 교체 + 팝업차단 회피 패턴으로 구현 완료. build PASS·회귀 0·헬퍼 유닛 6건(팝업 순서 실검증)·앱 런타임 부팅 스모크 통과.

---

## 생성/수정 파일 (base 대비 9파일, +298/-26)
| 파일 | 변경 |
|---|---|
| `src/lib/driveFile.ts` (신규) | 헬퍼 2종: `requestFileTicket(fileUrl, mode)`, `openFileViaProxy(fileUrl, mode)` |
| `src/lib/__tests__/driveFile.test.ts` (신규) | 헬퍼 유닛 테스트 6건 |
| `src/pages/PolicyAnalysis.tsx` | 증권 "원문 PDF 보기" `window.open` → `openFileViaProxy(...,"download")` |
| `src/pages/AdminNewsletters.tsx` | 소식지 "원본 파일 열기" 앵커 → 버튼 `openFileViaProxy(...,"view")` |
| `src/pages/AdminPremiumData.tsx` | 보험료 "원본 파일 열기" 앵커 → 버튼 `...,"view"` |
| `src/components/newsletter/ReviewDetailPanel.tsx` | 이미지 프리뷰 view-ticket 배선 + "원본 파일 보기"/"파일 링크" 앵커 → 버튼 view |
| `src/components/newsletter/ReviewDetailPanel.test.tsx` | 앵커→버튼 role 검증·img src=프록시 URL 검증·driveFile mock |
| `src/pages/AdminFcpa.tsx` | 금소법 현재 파일명 링크 → 버튼 `...,"view"` |
| `src/pages/FcpaGuide.tsx` | "법률 전문 다운로드" 버튼 → `openFileViaProxy(...,"download")` |

## 헬퍼 설계 (`driveFile.ts`)
- `requestFileTicket`: `supabase.auth.getSession()` → 세션 없으면 throw. `POST ${INSURO_API_BASE}/api/insuro/file-ticket` (Bearer + `{file_url, mode}`) → `${INSURO_API_BASE}/api/insuro/file/{ticket_id}` 반환. `!res.ok` throw.
- `openFileViaProxy` (**팝업차단 회피**): 클릭 핸들러 진입 즉시 **동기적으로** `const w = window.open("", "_blank")` → 이후 `await requestFileTicket` → 성공 시 `w.location.href = url`. 창 null(차단)이면 throw, 예외 시 `w.close()` 후 rethrow.

## view/download 결정 (라벨-동작 일치)
- **download**: 증권 PDF(PolicyAnalysis, 민감 개인정보 저장 성격), FcpaGuide "법률 전문 다운로드"(라벨 일치)
- **view**: 소식지·보험료 원본 열기, 검토 이미지 프리뷰/원본 보기/파일 링크, 금소법 현재 파일명(법령 열람)

## 이미지 프리뷰 (ReviewDetailPanel)
- `useEffect`로 isImage일 때만 `requestFileTicket(fileUrl,"view")` 비동기 발급 → `previewSrc` state → `<img src={previewSrc}>`. view는 다회+Range 지원되어 `<img>` 재요청 정상. 기존 `onError`(imgFailed) 폴백 유지, ticket 실패 시에도 imgFailed 처리.

---

## L1 스모크테스트 결과
- **서버 재시작**: 해당없음(프론트 dev 서버 신규 기동)
- **dev 서버 기동**: `vite --port 5199` → `curl localhost:5199/` **HTTP 200** (앱 런타임 부팅 OK)
- **Playwright 렌더 스모크**: `http://localhost:5199/` 접속 → `/login` 리다이렉트(로그인 벽 정상), **콘솔 에러 0 / warning 3(기존 Radix a11y, 무관)**. driveFile.ts를 import하는 6개 페이지 번들 포함 상태로 런타임 에러 없이 부팅 확인.
- **스크린샷**: `/home/jay/workspace/memory/reports/task-2930-smoke-login.png`
- **헬퍼 실동작 유닛 검증 (핵심 L1)**: `driveFile.test.ts` **6/6 PASS** — 팝업차단 회피 순서(`window.open`이 `fetch`보다 먼저 호출)를 `invocationCallOrder`로 **실제 검증**, ticket URL 조립·세션없음·발급실패·창정리(`w.close()`) 검증.
- **브라우저 정리**: `browser_close` 호출 완료, dev 서버(vite 5199) 종료 확인.

### L1 부분 미통과 (사유 명시)
- **로그인 세션 필요 관리자 페이지의 실링크 클릭 E2E는 미실행**: 7곳 중 다수(소식지/보험료/금소법/증권/검토)가 관리자·로그인 세션 뒤에 있어 표준 봇 환경에서 실계정 로그인·실파일 file-ticket 200 왕복까지는 확인 불가. → **대체 검증**: (1) 헬퍼 실동작 유닛 6건(팝업 순서 포함) (2) 앱 런타임 부팅 스모크(콘솔 0) (3) build PASS + tsc 0 + 전체 회귀 0. 실브라우저 링크 클릭 왕복은 **ANU/회장 로그인 세션에서 배포 후 최종 확인 권장**.

## 빌드/회귀
- `npm run build`: **성공** (`✓ built in ~12s`, PWA precache 176 entries). 청크 크기 경고는 기존(pdf-libs 등) 무관.
- `npx tsc --noEmit`: **에러 0**
- 전체 vitest 회귀: **78 files / 1168 tests 전부 PASS** (신규 driveFile 6 + ReviewDetailPanel 12 포함, 회귀 0)

## 발견 이슈 및 해결
- **ReviewDetailPanel 테스트 2건 회귀**: 앵커→버튼 전환 + img src=비동기 ticket 로 인해 구 동작(`getByRole("link")`+href, 직접 src) 검증 실패. → 새 동작에 맞춰 테스트 갱신(role=button, `not.toHaveAttribute("href")`, img src=프록시 URL, `@/lib/driveFile` mock 추가). 프로덕션 코드 무수정. 재실행 1168/1168 PASS.

## 게이트
- **G1 설계**: affected_files = `src/**`, `tests/**`만. 백엔드(`server/**`)·`.env`·`.github/**` 무수정 — 다른 팀/백엔드 겹침 0.
- **G2 구현**: 아르고스 테스터 검증 — 헬퍼 유닛 6건 + 회귀 1168 PASS + 런타임 부팅 스모크.
- **G3 머지**: **finalize-only (머지=ANU)**. Gemini PR OFF 방침(리뷰=Codex+ANU)에 따라 자동 머지 없음.

## 모델 사용 기록
- 이리스(프론트 구현): **sonnet** — 헬퍼+링크 8곳 배선(로직 구현)
- 아르고스(테스트 갱신+유닛): **sonnet** — 테스트 코드
- 팀장(헤르메스, Opus): 설계/분배/diff 직접 검증/통합/커밋 (직접 코딩 없음)
- haiku 미사용

## 머지 판단
- **머지 필요**: Yes (ANU가 수행 — finalize-only)
- **브랜치**: `task/task-2930-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2930-dev1`
- **머지 의견**: 프론트 전용(src/ + tests/), 백엔드/스키마 겹침 0, 회귀 0, build PASS. 라이브 백엔드 계약(file-ticket/file)만 호출. **Phase 2는 무중단**(파일 아직 공개라 프록시·직접링크 둘 다 열림). ANU 독립검증 → Codex 조율 → 합의 시 **CF Pages 배포(프론트만)** 로 프록시 실동작. Phase 3(anyone 제거+소급 회수)는 별도.

## 비고 (다음 단계)
- 배포: **CF Pages**(`wrangler pages deploy dist --project-name insuro --branch main`, `source .env.keys`). 백엔드 이미 라이브 → 프론트만 배포하면 프록시 동작.
- 실브라우저 링크 클릭 왕복 최종 확인은 로그인 세션에서 배포 후 권장.

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

