import { useState, useCallback } from "react"; import { useDropzone, FileRejection } from "react-dropzone"; import { toast } from "sonner"; interface UseFileUploadOptions { onFilesAdded?: (files: File[]) => void; } export function useFileUpload(options: UseFileUploadOptions = {}) { const [files, setFiles] = useState([]); // 파일 검증 const validateFiles = useCallback((filesToValidate: File[]): { valid: File[]; invalid: string[] } => { const MAX_FILE_SIZE = 1024 * 1024 * 1024; // 1GB const FORBIDDEN_EXTENSIONS = ['exe', 'com', 'dll', 'vbs', 'js', 'asp', 'aspx', 'bat', 'cmd']; const validFiles: File[] = []; const invalidFiles: string[] = []; filesToValidate.forEach((file) => { // 크기 검증 if (file.size > MAX_FILE_SIZE) { invalidFiles.push(`${file.name}: 파일 크기가 1GB를 초과합니다`); return; } // 확장자 검증 (블랙리스트) const extension = file.name.split('.').pop()?.toLowerCase(); if (extension && FORBIDDEN_EXTENSIONS.includes(extension)) { invalidFiles.push(`${file.name}: 금지된 파일 형식입니다 (.${extension})`); return; } validFiles.push(file); }); return { valid: validFiles, invalid: invalidFiles }; }, []); // 파일 드롭 핸들러 const onDrop = useCallback((acceptedFiles: File[], rejectedFiles: FileRejection[]) => { const { valid: validFiles, invalid: invalidMessages } = validateFiles(acceptedFiles); // 거부된 파일 처리 if (rejectedFiles.length > 0) { rejectedFiles.forEach((rejected) => { const errorMsg = rejected.errors?.[0]?.message || "파일이 거부되었습니다"; toast.error(`${rejected.file.name}: ${errorMsg}`); }); } // 유효하지 않은 파일 메시지 표시 if (invalidMessages.length > 0) { invalidMessages.forEach((msg) => toast.error(msg)); } if (validFiles.length > 0) { // 중복 제거 const existingNames = new Set(files.map((f) => f.name)); const newFiles = validFiles.filter((f) => !existingNames.has(f.name)); if (newFiles.length === 0) { toast.error("이미 선택된 파일입니다"); return; } setFiles((prev) => { const updated = [...prev, ...newFiles]; options.onFilesAdded?.(updated); return updated; }); toast.success(`${newFiles.length}개 파일이 선택되었습니다`); } }, [files, validateFiles, options]); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, multiple: true, maxSize: 1024 * 1024 * 1024, // 1GB }); // 파일 제거 const removeFile = useCallback((index: number) => { setFiles((prev) => prev.filter((_, i) => i !== index)); }, []); // 전체 파일 제거 const clearFiles = useCallback(() => { setFiles([]); }, []); // 파일 배열 직접 설정 const setFileList = useCallback((newFiles: File[]) => { setFiles(newFiles); }, []); return { files, setFiles: setFileList, removeFile, clearFiles, getRootProps, getInputProps, isDragActive, }; }