# task-2870 보고서 — InsuWiki 50MB 업로드 구현 (청크 서버리스 릴레이, A안)

**상태: ✅ 구현 완료 — 빌드 그린 + 단위 25 PASS + L1 스모크 통과. 순서 B(merge_policy=none): 실증 50MB E2E·머지는 ANU.**
**팀: dev2 (오딘)** · 레벨: Lv.3 · 작성: 2026-07-27 23:20 (server time)

---

## S (Situation)
- task-2869 스파이크에서 브라우저 직접 Drive resumable PUT = **CORS 차단 확정**(ACAO 부재, 실증). 대안 A(청크 서버리스 릴레이)가 5MB 실측 성공(308/308/200)으로 검증됨.
- 회장 A안 채택 → 본 태스크에서 **실제 50MB 업로드** 구현.

## C (Complication)
- Vercel Hobby 4.5MB body 하드한도 → 단일 요청 업로드는 4.5MB 초과 시 413(비-JSON)로 클라 `response.json()` 파손("Unexpected token 'R'…"). Google Drive 저장·기존 첨부 링크 구조는 유지해야 함(최소침습).

## Q (Question)
- 각 서버리스 요청 ≤4MB로 청크를 릴레이하여 Drive에 50MB 파일을 무손실 저장하고, 기존 <4.5MB 흐름·이미지 붙여넣기·Firestore 기록 회귀 0을 달성할 수 있는가?

## A (Answer) — 구현 완료
클라가 파일을 **4MB(256KB 배수) 청크로 분할** → `/api/upload/session`으로 Drive resumable 세션 개시 → 각 청크를 `/api/upload/chunk`로 순차 전송 → 서버가 세션 URI에 `Content-Range`로 서버측 PUT 릴레이(서버→Google은 CORS 무관) → 마지막 청크 200에서 webViewLink 회수 → 기존 Firestore attachments 로직 재사용. 각 요청 ≤4MB라 Vercel 한도 미저촉.

---

## 구현 내역 (팀원 위임)

### 백엔드 — 토르 (Sonnet)
- `nextapp/src/lib/googleDrive.ts`:
  - `getAuthClient()`(내부 추출·OAuth2 우선/GoogleAuth fallback, 기존 `getDriveService` 재사용·동작 불변), `getAuthHeaders()`(raw HTTP용 Bearer).
  - `initiateResumableUpload(folderId, filename, mimeType, size)→Promise<string>`: resumable POST(`X-Upload-Content-Type/Length`), `Location`(세션 URI) 반환.
  - `relayChunk(sessionUri, chunk, startByte, endByte, totalSize)`: 세션 URI에 `Content-Range: bytes {start}-{end}/{total}` PUT(**Authorization 미부착** — pre-authorized), 308→미완/200·201→완료(fileId). 에러 메시지에 sessionUri 미노출.
  - `getFileMetadata(fileId)`: webViewLink 등 조회. `setFilePermissions`는 **미구현**(기존 uploadFileToDrive가 per-file 권한 미설정·폴더 상속 → 회귀 0 위해 동일 유지).
- 신규 라우트 `api/upload/session/route.ts`(verifyIdToken + per-user 폴더 재사용 → sessionUri 반환, size>50MB 400 JSON, **sessionUri 로그 금지**), `api/upload/chunk/route.ts`(커스텀 헤더 `x-session-uri/x-chunk-start/x-chunk-end/x-total-size` + raw body, 4MB 상한 서버 방어, 308:`{complete:false,receivedTo}`/200:`{complete:true,fileId,name,webViewLink,webContentLink}`).
- 레거시 `api/upload/route.ts`: **유지**(DELETE=deleteFile 계속 사용, POST legacy 보존), `runtime='nodejs'` 명시. 모든 에러 JSON(raw 413 방지).

### 프론트 — 프레이야 (Sonnet)
- `useDocumentState.ts` `handleFileUpload`: 단일 FormData → 청크 릴레이 교체. guest 차단·50MB 클라 차단 유지, session→4MB 청크(`file.slice`)→chunk 순차(**x-chunk-end=end-1 inclusive**), **진행률 toast(%)**, 완료 시 기존 Firestore `arrayUnion` 재사용. JSON-safe 에러(`res.json().catch(()=>({}))`). `handleRecordingComplete`(4MB 캡·레거시)·`handleDeleteAttachment`(DELETE) 미변경.
- `EditorToolbar.tsx` `uploadFile`: 로컬 `uploadInChunks` 헬퍼로 동일 청크 흐름 전환(413/회귀 방지), 완료 후 `/api/drive-image/{fileId}` setImage 기존 유지.

### 테스트 — 헤임달 (Sonnet)
- `nextapp/src/lib/__tests__/googleDrive.chunk-upload.test.ts` — **25 케이스 전부 PASS**(relayChunk 10: 308/200·Content-Range·Authorization 미포함·sessionUri 미노출·비-JSON 안전 / initiateResumableUpload 4 / 청크분할 경계 8: 4MB·4MB+1·49MB(13청크)·off-by-one / JSON-safe 3).

## 검증

### 빌드 결과: **성공**
`✓ Compiled successfully in 14.7s / Finished TypeScript / Generating static pages (43/43)`. `.next/BUILD_ID` 생성, `/api/upload/chunk`·`/api/upload/session` 산출물 포함. sonner 타입 스텁(`module-declarations.d.ts`)은 빌드 무영향(TypeScript 완전 통과).

### 회귀
- 전체 스위트 1331 passed / 26 failed. 실패 26건(`api/ai/feedback`, `useReviewQueueListener`)은 **base commit 6516dbd에서 동일 재현 → 선재(pre-existing) 실패, 회귀 아님**. task-2870 변경 파일과 무관. **신규 실패 0.**

## L1 스모크테스트 결과
- **서버 재시작: 성공** — worktree에서 `npm run dev`(포트 3971) 실제 기동, Ready 1.2s. `npm run build` 프로덕션 빌드 exit 0(43/43 static pages).
- **API 응답 확인 (curl, 서버 실기동 상태)**:
  - `POST /api/upload/session` (무인증) → `HTTP/1.1 401` + `content-type: application/json` + `{"error":"Unauthorized"}`
  - `POST /api/upload/chunk` (무인증) → `HTTP/1.1 401` JSON
  - `POST /api/upload` (레거시, 무인증) → `HTTP/1.1 401` JSON
  - → 3개 신규/레거시 라우트 모두 라우팅 정상(404 아님)·JSON 에러 계약 준수(**원래 버그였던 raw 413/500 텍스트 아님**)·인증 게이트 작동 실증 확인. 401은 무인증 요청의 기대 응답(정상 동작).
- **단위 테스트: 25 passed**(relayChunk Content-Range/Authorization-미포함, 청크분할 경계 49MB→13청크, JSON-safe) — 릴레이 로직 실동작 검증.
- **스크린샷**: API 라우트 계층 스모크(curl 기반)라 브라우저 스크린샷 비대상. 실 UI 업로드(Firebase 토큰+Drive OAuth 필요)의 5MB/30MB/49MB 왕복 실증은 순서 B 지시대로 ANU 담당(task ★완료: "ANU 검증(실증 업로드 포함)·머지는 ANU"). 릴레이 방식 자체는 Step0(task-2869)에서 5MB 실측 성공(308/308/200)으로 이미 검증됨.

## 발견 이슈 및 해결
- **worktree_manager origin/main 하드코딩** → insuwiki(master) fail 우려. base=origin/master(6516dbd) **수동 worktree** 생성 + `task-2870.worktree-base.json` 마커 기록으로 우회(task-2869 검증 방식).
- **메인 repo node_modules stale**(`react-swipeable` 미설치 — task-2334가 package.json 추가 후 `npm install` 미실행). task-2870 코드 무관, API 라우트는 해당 패키지 미의존. 빌드/스모크는 API 라우트 온디맨드 컴파일로 영향 없음. **별도 조치 필요: 메인 repo `npm install`**(ANU/운영).
- **sonner 타입 스텁 협소**(`module-declarations.d.ts`가 `.loading`/`{id}` 미선언) → 프론트 2파일 내 로컬 타입 별칭으로 우회(런타임=실제 sonner 2.0.7 정상). 근본 해결(스텁 확장)은 스코프 밖·별도 태스크 권장.

## 변경 파일 (7건, 전부 allowed_resources 내부)
- `nextapp/src/lib/googleDrive.ts`
- `nextapp/src/app/api/upload/session/route.ts` (신규)
- `nextapp/src/app/api/upload/chunk/route.ts` (신규)
- `nextapp/src/app/api/upload/route.ts`
- `nextapp/src/app/docs/[id]/useDocumentState.ts`
- `nextapp/src/components/EditorToolbar.tsx`
- `nextapp/src/lib/__tests__/googleDrive.chunk-upload.test.ts` (신규)
- forbidden_paths(api/admin, .github) 미접촉.

## 머지 판단
- **머지 필요: Yes → 단, merge_policy=none·순서 B로 ANU가 수행**(→ Vercel 자동배포).
- **PR**: https://github.com/Jeon-Jonghyuk/InsuWiki/pull/4 (base=master, 검토·머지 대기)
- **브랜치**: `task/task-2870-dev2`
- **워크트리 경로**: `/home/jay/projects/insuwiki/.worktrees/task-2870-dev2`
- **base**: origin/master 6516dbd (worktree-base.json 마커 기록)
- **머지 의견**: 빌드 그린·단위 25 PASS·회귀 0·L1 라우팅/JSON 계약 통과. ANU 실증(5MB/30MB/49MB 실제 Drive 업로드 성공·413 0·webViewLink·Firestore 기록, <4.5MB 하위호환, EditorToolbar 이미지, 50MB 초과 차단 메시지) 후 머지 권장.

## 모델 사용 기록
- 토르(백엔드)·프레이야(프론트)·헤임달(테스트) 전부 **Sonnet**. haiku 미사용. 팀장(오딘, Opus)은 설계/분배/검토/통합만, 직접 코딩 0.

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


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


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


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


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

