# [Lv.3] InsuWiki 50MB 업로드 구현 — 청크 서버리스 릴레이 (A안, Step0 검증완료)

## repo / 배포
- repo: `/home/jay/projects/insuwiki` (git root), 앱=`nextapp/`, branch=**master**(origin/master, 현 HEAD 6516dbd). github `JonghyukJeon/InsuWiki`. Vercel Hobby 자동배포.
- ⚠️ worktree_manager `create`가 origin/main 하드코딩 → insuwiki(master)에서 fail. **base=origin/master 로 수동 worktree** 생성(task-2869 스파이크 때 검증된 방식), worktree-base.json에 base_sha=6516dbd 기록.

## 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. Firestore attachments 기록 = **클라 유지**(최소침습). ANU 검증·머지(→Vercel 자동배포).

## 배경 (task-2869 Step0 실증 = A안 확정)
- 브라우저 직접 Drive PUT = **CORS 차단 확정**(ACAO 부재, 실증). → 폐기.
- **A안 = 청크 서버리스 릴레이 검증 성공**: 클라 → `/api/upload/chunk`(≤4MB) → 서버가 Drive 세션 URI에 `Content-Range`로 서버측 PUT 릴레이(서버→Google은 CORS 무관). 실증: 5MB를 2MB청크 3개 → `308/308/200` `UPLOAD_COMPLETE hasLink=true`, cleanup `DELETE 204`. Drive 유지·4.5MB 회피·토큰 미노출.

## 목표
Google Drive에 **실제 50MB 파일 업로드**(각 서버리스 요청 ≤4MB라 Vercel 4.5MB 안 걸림).

## 작업

### 서버 — 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()`(OAuth2 우선 유지). 응답 **`Location`(세션 URI) 반환**.
- `relayChunk(sessionUri, chunkBuffer, startByte, endByte, totalSize)`: 세션 URI에 **`PUT` + `Content-Range: bytes {start}-{end}/{total}`** 릴레이. 중간청크 → 308(범위 진행), 마지막 → 200/201(+file id). 응답 상태·완료여부·fileId 반환.
- `getFileMetadata(fileId)`(id·name·webViewLink·webContentLink), `setFilePermissions(fileId)`(기존 공유정책 동일 범위). 기존 `findFolder`/`createFolder`/`deleteFile` 재사용.

### 서버 — 라우트
- `nextapp/src/app/api/upload/session/route.ts`(신규): Firebase verifyIdToken → 사용자폴더 find/create(재사용) → initiateResumableUpload → `{ sessionUri, folderId }`. **세션URI 로그 금지(민감)**.
- `nextapp/src/app/api/upload/chunk/route.ts`(신규): verifyIdToken → body(청크 바이트 + sessionUri + Content-Range 정보) → `relayChunk` → 진행상태/완료시 fileId 반환. **각 요청 ≤4MB**(청크는 256KB 배수, 마지막 제외; 예 4,194,304 = 4MB).
- `nextapp/src/app/api/upload/finalize/route.ts`(신규 or chunk 최종응답에 포함): setFilePermissions + getFileMetadata → `{ fileId, webViewLink, name }`.
- 레거시 `route.ts`: 삭제 말고 유지(deleteFile 등), 에러 응답 **항상 JSON**(raw 413 방지).

### 클라이언트
- `useDocumentState.ts` 업로드 핸들러(733~) 교체:
  1. size ≤ **50MB** 유지(실지원).
  2. `/api/upload/session` → sessionUri.
  3. 파일을 **≤4MB 청크(256KB 배수)로 분할**, 순차적으로 `/api/upload/chunk`에 전송(sessionUri + Content-Range). **진행률 표시**(청크 누적).
  4. 마지막 청크 200 → finalize(또는 응답) → webViewLink → **기존 Firestore attachments 기록 재사용**.
  - 에러 **JSON-safe**(`.json().catch(()=>({}))`, useDocumentState.ts:928 패턴). 실패/네트워크 시 명확 메시지.
- `EditorToolbar.tsx:18`(이미지 붙여넣기, 동일 `/api/upload` 사용): **같은 청크 릴레이 흐름으로 전환**(회귀·413 방지).

## 제약·유의
- 각 서버리스 요청 ≤4MB 절대 준수(파일 바이트가 4.5MB 넘게 서버리스 한 번에 들어가면 413 재발).
- Vercel Hobby **함수 실행시간 ~10초/호출** — 청크당 ≤4MB 릴레이는 여유(청크 수만큼 호출). 청크 순차 처리.
- sessionURI 민감정보(로그·노출 최소, HTTPS·auth 릴레이만). 공유권한 범위 = 기존과 동일.
- 기존 <4.5MB 업로드·다른 카드·Firestore 기록 회귀 0.

## 검증
- `npm run build`(nextapp) 클린, tsc/lint 통과. 단위: initiateResumableUpload(Location)·relayChunk(Content-Range·308/200)·session/chunk/finalize 라우트·클라 청크분할·진행률·JSON-safe 에러.
- **실증 E2E**: 5MB·30MB·**49MB** 실제 업로드 성공(413 0·webViewLink·Firestore 기록), <4.5MB 하위호환, EditorToolbar 이미지, 50MB 초과 client 차단+명확메시지.

## 완료 (★ 순서 B)
- 변경 = allowed_resources 내부만.
- **dev6 금지** — dev2(task-2869 스파이크 연속 컨텍스트). base=origin/master 수동 worktree. 커밋 → push → finish-task **foreground 1회** → .done 즉시 종료. **background+wait 금지.** ANU 검증(실증 업로드 포함)·Codex 검토·머지(→Vercel 배포)는 ANU.
