'use client'; import { useState } from 'react'; import { FoxFileUpload } from '@fox/core/components/fox-file-upload'; import { FoxHelperText } from '@fox/core/components/fox-helper-text'; import { FILE_MODULE, MAX_STREAM_UPLOAD_BYTES, allowedExtensions, fileModuleAccept, formatMaxUploadSize, isAllowedFileExtension, } from '@/lib/domain/file-module'; const UPLOAD_ENDPOINT = `/contents/upload?module=${FILE_MODULE.vodContent.id}`; const MAX_SIZE_TEXT = formatMaxUploadSize(MAX_STREAM_UPLOAD_BYTES); const UPLOAD_HELP_TEXT = `${allowedExtensions(FILE_MODULE.vodContent).join(', ')} · ${MAX_SIZE_TEXT} 이하`; interface ContentVideoFieldProps { defaultFileId?: string | null; /** 이미 올라가 있는 영상의 원본 파일명. 없으면 대체 문구로 그린다. */ defaultFileName?: string | null; } /** * 영상 콘텐츠 — 고르는 즉시 스트리밍 라우트로 올리고 파일 ID만 폼에 싣는다. * * 영상은 Server Action의 multipart 한도(10MB)를 넘기 때문에 폼 제출에 파일을 실을 수 없다. * 업로드가 끝나기 전에 저장하면 영상 없이 저장되므로 그동안은 `videoUploading`을 실어 보내 * Server Action이 막게 한다. */ export function ContentVideoField({ defaultFileId, defaultFileName, }: ContentVideoFieldProps) { 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]; if (!file) { return; } setError(''); setHasFailedPick(false); if (!isAllowedFileExtension(FILE_MODULE.vodContent, file.name)) { setHasFailedPick(true); setError( `${allowedExtensions(FILE_MODULE.vodContent).join(', ')} 파일만 올릴 수 있습니다.` ); return; } // 넘겨 보내면 앞단이 사유 없는 413으로 끊는다 — 고르는 순간 이유를 말해 준다. if (file.size > MAX_STREAM_UPLOAD_BYTES) { setHasFailedPick(true); setError(`영상은 ${MAX_SIZE_TEXT} 이하만 올릴 수 있습니다.`); return; } const body = new FormData(); body.set('file', file); setIsUploading(true); try { 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); } } return ( <> { setFileId(''); setError(''); setHasFailedPick(false); }} /> {isUploading && } {hasFailedPick && } {isUploading && ( )} {error && } ); }