From 26365ef08588d53b8c5d9c7cfaefb244536e6743 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 24 Nov 2025 10:41:46 +0900 Subject: (김준회) 돌체 재개발 - 2차: 업로드 개선, 다운로드 오류 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/dolce/hooks/use-file-upload.ts | 107 +++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 lib/dolce/hooks/use-file-upload.ts (limited to 'lib/dolce/hooks/use-file-upload.ts') diff --git a/lib/dolce/hooks/use-file-upload.ts b/lib/dolce/hooks/use-file-upload.ts new file mode 100644 index 00000000..38556cb9 --- /dev/null +++ b/lib/dolce/hooks/use-file-upload.ts @@ -0,0 +1,107 @@ +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, + }; +} + -- cgit v1.2.3