임동욱 임동욱 08-21
fix: 영상 업로드가 실패한 채로 저장되지 않게 막는다
"등록은 잘 된다"는 것이 문제였다. 영상 업로드가 413으로 실패해도 화면은 오류만 띄우고 저장
버튼은 그대로 눌렸다. 영상은 필수 항목이 아니라 검증도 통과하고, `videoFileId`가 빈 값인 채로
저장돼 **영상 없는 콘텐츠가 조용히 만들어졌다.** 사용자에게는 정상 등록으로 보인다.

고른 영상이 끝내 올라가지 않으면 그 사실을 `videoUploadFailed`로 실어 보내고 Server Action이
저장을 막는다 — 업로드 중(`videoUploading`)을 막던 것과 같은 방식이다. 파일을 지우면 플래그와
오류가 함께 풀려 영상 없이 저장하려는 의도는 그대로 통한다.

413 메시지 자체는 정확하다(재실측: 2MB·6MB 모두 nginx 413). 인프라 상향 전까지는 이 경로로
영상을 올릴 수 없고, 그 사실이 저장 시점에 드러나는 편이 낫다.

확인: 100MB 초과 선택 → 오류 + `videoUploadFailed=1` + `videoFileId` 빈 값,
삭제 → 플래그·오류 모두 해제.

Co-Authored-By: Claude Opus 5 
@557130ca20e03a49d3733a1273a7f29cee1c123b
app/(protected)/(basic)/contents/_actions.ts
--- app/(protected)/(basic)/contents/_actions.ts
+++ app/(protected)/(basic)/contents/_actions.ts
@@ -48,6 +48,8 @@
 const SAVE_FAILED_MESSAGE = '저장하지 못했습니다. 잠시 후 다시 시도해 주세요.';
 const DELETE_FAILED_MESSAGE = '삭제하지 못했습니다. 잠시 후 다시 시도해 주세요.';
 const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.';
+const VIDEO_FAILED_MESSAGE =
+  '영상을 올리지 못했습니다. 파일을 지우거나 다시 골라 주세요 — 이대로 저장하면 영상 없이 저장됩니다.';
 const VIDEO_UPLOADING_MESSAGE =
   '영상을 올리는 중입니다. 업로드가 끝난 뒤 저장해 주세요.';
 const OVERSIZE_MESSAGE = `첨부파일은 ${Math.floor(MAX_ATTACHMENT_BYTES / (1024 * 1024))}MB까지 올릴 수 있습니다.`;
@@ -228,6 +230,9 @@
   if (readString(formData, 'videoUploading') === '1') {
     return { status: 'error', message: VIDEO_UPLOADING_MESSAGE };
   }
+  if (readString(formData, 'videoUploadFailed') === '1') {
+    return { status: 'error', message: VIDEO_FAILED_MESSAGE };
+  }
 
   const failed = await runWrite(async () => {
     const validation = validateShortsForm(
@@ -269,6 +274,9 @@
   if (readString(formData, 'videoUploading') === '1') {
     return { status: 'error', message: VIDEO_UPLOADING_MESSAGE };
   }
+  if (readString(formData, 'videoUploadFailed') === '1') {
+    return { status: 'error', message: VIDEO_FAILED_MESSAGE };
+  }
 
   const failed = await runWrite(async () => {
     const validation = validateShortsForm(
app/(protected)/(basic)/contents/_components/content-video-field.tsx
--- app/(protected)/(basic)/contents/_components/content-video-field.tsx
+++ app/(protected)/(basic)/contents/_components/content-video-field.tsx
@@ -33,6 +33,9 @@
   const [fileId, setFileId] = useState(defaultFileId ?? '');
   const [isUploading, setIsUploading] = useState(false);
   const [error, setError] = useState('');
+  // 고른 영상이 끝내 올라가지 않은 상태. 이대로 저장하면 영상 없이 저장되므로 Server Action이
+  // 막게 한다 — 화면에 오류가 떠 있어도 저장 버튼은 눌리기 때문이다.
+  const [hasFailedPick, setHasFailedPick] = useState(false);
 
   async function upload(picked: File[]) {
     const file = picked[0];
@@ -41,8 +44,10 @@
     }
 
     setError('');
+    setHasFailedPick(false);
 
     if (!isAllowedFileExtension(FILE_MODULE.vodContent, file.name)) {
+      setHasFailedPick(true);
       setError(
         `${allowedExtensions(FILE_MODULE.vodContent).join(', ')} 파일만 올릴 수 있습니다.`
       );
@@ -50,6 +55,7 @@
     }
     // 넘겨 보내면 앞단이 사유 없는 413으로 끊는다 — 고르는 순간 이유를 말해 준다.
     if (file.size > MAX_STREAM_UPLOAD_BYTES) {
+      setHasFailedPick(true);
       setError(`영상은 ${MAX_SIZE_TEXT} 이하만 올릴 수 있습니다.`);
       return;
     }
@@ -62,11 +68,13 @@
       const response = await fetch(UPLOAD_ENDPOINT, { method: 'POST', body });
       const payload = (await response.json()) as { id?: string; message?: string };
       if (!response.ok || !payload.id) {
+        setHasFailedPick(true);
         setError(payload.message ?? '영상을 업로드하지 못했습니다.');
         return;
       }
       setFileId(payload.id);
     } catch {
+      setHasFailedPick(true);
       setError('영상을 업로드하지 못했습니다.');
     } finally {
       setIsUploading(false);
@@ -83,10 +91,15 @@
         description={UPLOAD_HELP_TEXT}
         disabled={isUploading}
         onSelect={upload}
-        onRemove={() => setFileId('')}
+        onRemove={() => {
+          setFileId('');
+          setError('');
+          setHasFailedPick(false);
+        }}
       />
       <input type="hidden" name="videoFileId" value={fileId} />
       {isUploading && <input type="hidden" name="videoUploading" value="1" />}
+      {hasFailedPick && <input type="hidden" name="videoUploadFailed" value="1" />}
 
       {isUploading && (
         <FoxHelperText type="information" message="영상을 올리는 중입니다…" />
Add a comment
List