"use client"; import React, { useCallback, useState } from "react"; import { useDropzone } from "react-dropzone"; import { CloudArrowUpIcon, CheckCircleIcon, ExclamationTriangleIcon, XMarkIcon, } from "@heroicons/react/24/outline"; interface UploadFile { id: string; file: File; progress: number; status: "uploading" | "completed" | "error"; error?: string; result?: { id: string; url: string; fileName: string; }; } interface MediaUploadZoneProps { onUploadComplete?: (files: UploadFile[]) => void; maxFiles?: number; maxFileSize?: number; acceptedFileTypes?: string[]; className?: string; } export function MediaUploadZone({ onUploadComplete, maxFiles = 10, maxFileSize = 50 * 1024 * 1024, // 50MB acceptedFileTypes = ["image/*", "video/*", "audio/*"], className = "", }: MediaUploadZoneProps) { const [uploadFiles, setUploadFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const uploadFile = async (file: File): Promise => { const uploadFile: UploadFile = { id: Math.random().toString(36).substring(2), file, progress: 0, status: "uploading", }; try { const formData = new FormData(); formData.append("file", file); const response = await fetch("/api/media/upload", { method: "POST", body: formData, }); if (!response.ok) { throw new Error("업로드 실패"); } const result = await response.json(); return { ...uploadFile, progress: 100, status: "completed", result: result.data, }; } catch (error) { return { ...uploadFile, status: "error", error: error instanceof Error ? error.message : "업로드 실패", }; } }; const onDrop = useCallback( async (acceptedFiles: File[]) => { if (acceptedFiles.length === 0) return; setIsUploading(true); const newUploadFiles: UploadFile[] = acceptedFiles.map((file) => ({ id: Math.random().toString(36).substring(2), file, progress: 0, status: "uploading" as const, })); setUploadFiles((prev) => [...prev, ...newUploadFiles]); // 파일들을 병렬로 업로드 const uploadPromises = acceptedFiles.map(uploadFile); const results = await Promise.all(uploadPromises); setUploadFiles((prev) => prev.map((prevFile) => { const result = results.find( (r) => r.file.name === prevFile.file.name ); return result || prevFile; }) ); setIsUploading(false); onUploadComplete?.(results); }, [onUploadComplete] ); const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({ onDrop, accept: acceptedFileTypes.reduce((acc, type) => { acc[type] = []; return acc; }, {} as Record), maxFiles, maxSize: maxFileSize, disabled: isUploading, }); const removeFile = (id: string) => { setUploadFiles((prev) => prev.filter((file) => file.id !== id)); }; const getStatusIcon = (status: UploadFile["status"]) => { switch (status) { case "completed": return ; case "error": return ; default: return (
); } }; return (
{/* 드롭존 */}

{isDragActive ? "파일을 놓아주세요" : "파일을 끌어다 놓거나 클릭하여 선택하세요"}

최대 {maxFiles}개 파일, 파일당 최대{" "} {Math.round(maxFileSize / 1024 / 1024)}MB

{/* 파일 거부 에러 */} {fileRejections.length > 0 && (

업로드할 수 없는 파일이 있습니다

    {fileRejections.map(({ file, errors }) => (
  • {file.name}: {errors.map((e) => e.message).join(", ")}
  • ))}
)} {/* 업로드 파일 목록 */} {uploadFiles.length > 0 && (

업로드 파일

{uploadFiles.map((uploadFile) => (
{getStatusIcon(uploadFile.status)}

{uploadFile.file.name}

{Math.round(uploadFile.file.size / 1024)} KB

{uploadFile.status === "uploading" && (
)} {uploadFile.status === "error" && ( {uploadFile.error} )}
))}
)}
); }