# [Lv.3] InsuWiki 파일 업로드 50MB 실지원 — Google Drive 직접(resumable) 업로드로 Vercel 4.5MB 우회

## repo / 배포
- repo: `/home/jay/projects/insuwiki` (git root), 앱=`nextapp/` (Next.js), branch=**master**, github `JonghyukJeon/InsuWiki`.
- 배포: **Vercel Hobby**(서버리스 요청 body 4.5MB 하드한도가 근본원인). master push/PR 머지 시 Vercel 자동배포.

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "nextapp/src/lib/googleDrive.ts"
    - "nextapp/src/app/api/upload/**"
    - "nextapp/src/app/docs/[id]/useDocumentState.ts"
    - "nextapp/src/components/EditorToolbar.tsx"
    - "nextapp/src/**/__tests__/**"
  forbidden_paths:
    - "nextapp/src/app/api/admin/**"
    - ".github/**"
  commands: ["npm","npx","node","git"]
  merge_policy: "none"
  ttl_hours: 20
```
> ★ merge_policy=none. base=현 origin/master. ANU 검증·머지(→Vercel 자동배포). Firestore 문서 attachments 기록은 **클라이언트 유지**(최소침습).

## 문제 (확정 진단)
- 업로드가 브라우저 → `/api/upload`(Vercel 서버리스) → `file.arrayBuffer()` 버퍼 → Drive `files.create`. **Vercel Hobby 4.5MB body 한도**로 4.5MB 초과 파일은 함수 실행 전 **413(비-JSON)** → client `useDocumentState.ts:755` `await response.json()`가 "Unexpected token 'R', Request En..."로 터짐. UI "Max 50MB" 허위.

## 목표
실제 50MB 업로드 지원 = **브라우저에서 Google Drive로 직접 resumable 업로드**(서버리스 body 우회).

## ★ Step 0 — CORS 실증 스파이크 (착수 최우선, 가정 금지)
- 브라우저가 Drive resumable **세션 URI로 cross-origin PUT** 가능한지 **실제 검증**(preflight 포함). 세션 URI는 pre-authorized라 브라우저에 OAuth 토큰 노출 없이 PUT 되는지 확인.
- **CORS 불가로 판명되면 강행 금지 → ANU 에스컬레이션**(대안: Firebase Storage 브라우저 SDK 등 별도 논의). 가능 확인 후에만 본 구현.

## 작업 (Codex 검증 반영)
### 서버 — 세션 개시 + finalize (작은 요청, body 한도 무관)
- `nextapp/src/lib/googleDrive.ts`에 추가:
  - `initiateResumableUpload(folderId, filename, mimeType, size)`: **raw HTTP로** `POST https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable` (메타데이터 name/parents, 헤더 `X-Upload-Content-Type`/`X-Upload-Content-Length`) — 기존 auth client의 `getRequestHeaders()`로 Authorization 발급. 응답 **`Location`(세션 URI) 반환**. (googleapis `files.create`는 media 있으면 multipart 강제라 세션URI 못 얻음 → raw fetch.)
  - `getFileMetadata(fileId)` (id·name·webViewLink·webContentLink), `setFilePermissions(fileId)`(기존 공유정책과 동일 범위).
  - **인증 = 기존 OAuth2 우선**(서비스계정 0-quota는 소유권 문제라 유지). SCOPES 재사용.
- `nextapp/src/app/api/upload/session/route.ts` (신규): Firebase `verifyIdToken` → 기존 `findFolder`/`createFolder`(사용자폴더) **재사용** → `initiateResumableUpload` → `{ sessionUri, folderId }` 반환. **세션URI 로그 금지(민감)**.
- `nextapp/src/app/api/upload/finalize/route.ts` (신규): 토큰검증 → `setFilePermissions` + `getFileMetadata` → `{ fileId, webViewLink, name }` 반환.
- 레거시 `nextapp/src/app/api/upload/route.ts`: **삭제하지 말고**(deleteFile 등 타 용도) 유지하되, 에러 응답을 **항상 JSON**으로(raw 413 방지). 업로드 POST 경로는 신규 흐름으로 대체.

### 클라이언트 — 3단계 직접 업로드
- `nextapp/src/app/docs/[id]/useDocumentState.ts` 업로드 핸들러(733~) 교체:
  1. size 체크 실제 **50MB** 유지(이제 실지원).
  2. `/api/upload/session` POST(filename·mimeType·size·auth) → sessionUri.
  3. **XHR/fetch로 sessionUri에 파일 바이트 직접 PUT** (진행률 표시). 단일 PUT(≤50MB) 또는 청크(Content-Range) — 단일 우선, 대용량 안정 위해 청크 고려.
  4. 완료 → `/api/upload/finalize` → webViewLink → **기존 Firestore attachments 기록 로직 재사용**.
  - **에러 처리 JSON-safe**: `response.json().catch(()=>({}))` 패턴(useDocumentState.ts:928 기존 패턴 참조). 413/네트워크 실패 시 명확한 메시지.
- `nextapp/src/components/EditorToolbar.tsx:18` (이미지 붙여넣기도 `/api/upload` 사용 → 동일 413 위험): **같은 직접 업로드 흐름으로 전환**(또는 최소한 JSON-safe 에러+대용량 안내). 회귀 방지.

## 제약
- 기존 <4.5MB 업로드·다른 카드 회귀 0. Firestore 기록 클라 유지(서버 이전 금지 — 최소침습). 세션URI·토큰 로그 금지. 공유 권한 범위는 **기존 정책과 동일**하게.
- 서버리스 함수는 세션 개시/finalize(작은 요청)만 — 파일 바이트는 절대 서버리스 경유 금지(그럼 4.5MB 재발).

## 검증
- **Step0 CORS 실증 결과 보고**(가능/불가).
- `npm run build`(nextapp) 클린, `npx tsc`/lint 통과. 단위: initiateResumableUpload(raw HTTP·Location 파싱)·session/finalize 라우트·클라 3단계·JSON-safe 에러. 
- **실증 E2E**: 5MB·30MB·49MB 파일 실제 업로드 성공(413 없음), <4.5MB 하위호환, EditorToolbar 이미지, webViewLink·Firestore 기록 정상. 50MB 초과는 client 차단+명확 메시지.

## 완료 (★ 순서 B)
- 변경 = allowed_resources 내부만.
- **dev6 금지** — dev2. 커밋 → push → finish-task **foreground 1회** → .done 즉시 종료. **background+wait 금지.** ANU 검증·Codex 재검토·머지(→Vercel 배포)는 ANU. **CORS 불가 시 구현 중단·ANU 에스컬레이션.**
