1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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<File[]>([]);
// 파일 검증
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,
};
}
|