영상 id는 조회에서 채워 hidden input에 실려 있었지만 FoxFileUpload에
defaultFiles를 넘기지 않아 칸이 늘 비어 보였다. 어떤 영상이 붙어 있는지
알 수 없어 매번 다시 올려야 하는 것처럼 보였다.
조회 응답의 fileList[].orgnlFileNm을 함께 실어 와 그 이름을 그린다.
이름이 없으면 "등록된 영상"으로 대신한다.
Co-Authored-By: Claude Opus 5
"등록은 잘 된다"는 것이 문제였다. 영상 업로드가 413으로 실패해도 화면은 오류만 띄우고 저장
버튼은 그대로 눌렸다. 영상은 필수 항목이 아니라 검증도 통과하고, `videoFileId`가 빈 값인 채로
저장돼 **영상 없는 콘텐츠가 조용히 만들어졌다.** 사용자에게는 정상 등록으로 보인다.
고른 영상이 끝내 올라가지 않으면 그 사실을 `videoUploadFailed`로 실어 보내고 Server Action이
저장을 막는다 — 업로드 중(`videoUploading`)을 막던 것과 같은 방식이다. 파일을 지우면 플래그와
오류가 함께 풀려 영상 없이 저장하려는 의도는 그대로 통한다.
413 메시지 자체는 정확하다(재실측: 2MB·6MB 모두 nginx 413). 인프라 상향 전까지는 이 경로로
영상을 올릴 수 없고, 그 사실이 저장 시점에 드러나는 편이 낫다.
확인: 100MB 초과 선택 → 오류 + `videoUploadFailed=1` + `videoFileId` 빈 값,
삭제 → 플래그·오류 모두 해제.
Co-Authored-By: Claude Opus 5
"저장할 수 없는 크기"의 출처는 dev API 앞단 nginx다. `client_max_body_size`가 기본값 1MiB라
그보다 큰 본문을 봉투 없이 413으로 끊는다(실측: 1,000KB→401 통과 / 1,050KB→413 nginx HTML).
영상은 사실상 항상 이 선을 넘어 브라우저 경로로는 올릴 수 없었다.
인프라를 백엔드 `maxUploadSize`와 같은 100m로 올리기로 해(사용자 확정) 그 값을 기준으로
`MAX_STREAM_UPLOAD_BYTES`를 둔다.
화면 쪽 문제도 함께 고친다 — 지금까지 영상 칸은 확장자만 안내하고 크기는 말하지 않았다.
사용자는 한도를 모른 채 고르고, 업로드가 끝나서야 사유 없는 실패를 봤다. 이제 「mp4 · 100MB
이하」를 입력칸 밑에 적고, 넘는 파일은 고르는 순간 "영상은 100MB 이하만 올릴 수 있습니다."로
막는다. 확장자 검사가 먼저라 mov를 고르면 확장자 사유가 뜬다.
한도와 안내 문구는 `formatMaxUploadSize`로 한 값에서 만든다 — 둘이 어긋나면 안내가 거짓말이 된다.
⚠️ 인프라 반영 전에는 이 상한을 통과한 파일도 413으로 끊긴다. 반영 여부는 업로드 엔드포인트에
1MiB 넘는 본문을 보내 401(통과)인지 413(차단)인지로 확인한다.
확인: 안내 문구 "mp4 · 100MB 이하", 150MB → "영상은 100MB 이하만 올릴 수 있습니다.",
mov → "mp4 파일만 올릴 수 있습니다.", 두 경우 모두 업로드가 시작되지 않고 videoFileId가 빈 값.
Co-Authored-By: Claude Opus 5
화면과 스트리밍 라우트의 100MB 사전 검사를 걷어낸다. 라우트는 본문을 버퍼에 담지
않고 그대로 흘려보내므로 자체 상한이 필요 없고, 실제 한도는 앞단 프록시와 백엔드가
정한다 — 넘으면 413이 사유와 함께 화면까지 올라간다(직전 커밋).
쓰이지 않게 된 MAX_STREAM_UPLOAD_BYTES를 지운다. Server Action 경로의
MAX_ATTACHMENT_BYTES(10MB)는 미들웨어가 본문을 자르는 실제 제약이라 그대로 둔다.
Co-Authored-By: Claude Opus 5
10MB 벽의 원인이 Server Action이 아니었다. **미들웨어(`proxy.ts`)를 지나는 요청은 본문이
10MiB에서 잘린다** — 30MB를 보내면 라우트에 10,485,472바이트만 도달하고, 같은 경로를 matcher에서
빼면 31,457,478바이트가 전부 도달한다(curl 측정). 잘려도 오류가 아니라 조용히 끝나서 업로드가
성공한 것처럼 실패한다. 그동안 `bodySizeLimit`을 올려도 낫지 않던 `Unexpected end of form`이
이것이었다.
그래서 영상은 Server Action(페이지 URL로 POST → 반드시 미들웨어 통과)을 쓰지 않고, matcher에서
제외한 `/contents/upload`로 브라우저가 직접 올린다. 라우트는 본문을 파싱하지 않고 백엔드로 그대로
흘려보낸다 — 여기서 버퍼링하면 같은 문제를 다시 만드는 셈이다. 토큰이 httpOnly 세션 안에만 있어
브라우저가 백엔드를 직접 부를 수 없으므로 이 중계가 필요하다.
인증은 느슨해지지 않는다. 미들웨어의 쿠키 확인은 원래 낙관적 체크이고 최종 판단은 DAL의
`verifySession()`이며, 이 라우트도 그것으로 시작한다(확인: 세션 없이 POST → 303 /login,
GET → 405로 라우트까지는 도달, 다른 경로는 그대로 307 /login).
상한은 백엔드 `multipartResolver.maxUploadSize`와 같은 100MB로 잡았다. 썸네일·게시판 첨부는
Server Action 경로 그대로라 10MB 가드를 유지한다.
확장자는 저장소 설정만으로 부족했다 — `Globals.fileUpload.Extensions`가 먼저 거른다.
`MODULE_VOD_CONTENT`가 mov·avi·mkv·webm·wmv를 허용해도 실제로 통과하는 것은 mp4뿐이라
두 목록의 교집합을 화면 accept와 사전 검증이 함께 본다(확인: 영상 칸 accept=".mp4").
Co-Authored-By: Claude Opus 5