summaryrefslogtreecommitdiff
path: root/lib/dolce/hooks/use-file-upload.ts
blob: 38556cb94c59ffcaaa2e1d98bc63f0f43ae24c3d (plain)
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,
  };
}