diff options
Diffstat (limited to 'lib/dolce/dialogs/upload-files-to-detail-dialog.tsx')
| -rw-r--r-- | lib/dolce/dialogs/upload-files-to-detail-dialog.tsx | 242 |
1 files changed, 86 insertions, 156 deletions
diff --git a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx index 1d8ac582..af73aea6 100644 --- a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx +++ b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx @@ -14,7 +14,9 @@ import { Button } from "@/components/ui/button"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Upload, FolderOpen, Loader2, X, FileText, AlertCircle } from "lucide-react"; import { toast } from "sonner"; -import { uploadFilesToDetailDrawing, type UploadFilesResult } from "../actions"; +import { useFileUploadWithProgress } from "../hooks/use-file-upload-with-progress"; +import { uploadFilesWithProgress, type UploadResult } from "../utils/upload-with-progress"; +import { FileUploadProgressList } from "../components/file-upload-progress-list"; interface UploadFilesToDetailDialogProps { open: boolean; @@ -35,101 +37,26 @@ export function UploadFilesToDetailDialog({ userId, onUploadComplete, }: UploadFilesToDetailDialogProps) { - const [selectedFiles, setSelectedFiles] = useState<File[]>([]); const [isUploading, setIsUploading] = useState(false); - const [isDragging, setIsDragging] = useState(false); + + // 파일 업로드 훅 사용 (진행도 추적) + const { + fileProgresses, + files: selectedFiles, + removeFile, + clearFiles, + updateFileProgress, + getRootProps, + getInputProps, + isDragActive, + } = useFileUploadWithProgress(); // 다이얼로그 닫을 때 초기화 React.useEffect(() => { if (!open) { - setSelectedFiles([]); - setIsDragging(false); - } - }, [open]); - - // 파일 선택 핸들러 - const handleFilesChange = (files: File[]) => { - if (files.length === 0) return; - - // 파일 크기 및 확장자 검증 - 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[] = []; - - files.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); - }); - - if (invalidFiles.length > 0) { - invalidFiles.forEach((msg) => toast.error(msg)); - } - - if (validFiles.length > 0) { - // 중복 제거 - const existingNames = new Set(selectedFiles.map((f) => f.name)); - const newFiles = validFiles.filter((f) => !existingNames.has(f.name)); - - if (newFiles.length === 0) { - toast.error("이미 선택된 파일입니다"); - return; - } - - setSelectedFiles((prev) => [...prev, ...newFiles]); - toast.success(`${newFiles.length}개 파일이 선택되었습니다`); - } - }; - - // Drag & Drop 핸들러 - const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(true); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (e.currentTarget === e.target) { - setIsDragging(false); - } - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - e.dataTransfer.dropEffect = "copy"; - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - - const droppedFiles = Array.from(e.dataTransfer.files); - if (droppedFiles.length > 0) { - handleFilesChange(droppedFiles); + clearFiles(); } - }; - - // 파일 제거 - const handleRemoveFile = (index: number) => { - setSelectedFiles((prev) => prev.filter((_, i) => i !== index)); - }; + }, [open, clearFiles]); // 업로드 처리 const handleUpload = async () => { @@ -141,18 +68,28 @@ export function UploadFilesToDetailDialog({ setIsUploading(true); try { - // FormData 생성 - const formData = new FormData(); - formData.append("uploadId", uploadId); - formData.append("userId", userId); - formData.append("fileCount", String(selectedFiles.length)); - - selectedFiles.forEach((file, index) => { - formData.append(`file_${index}`, file); + // 모든 파일 상태를 uploading으로 변경 + selectedFiles.forEach((_, index) => { + updateFileProgress(index, 0, "uploading"); }); - // 서버 액션 호출 - const result: UploadFilesResult = await uploadFilesToDetailDrawing(formData); + // 진행도 추적 업로드 호출 + const result: UploadResult = await uploadFilesWithProgress({ + uploadId, + userId, + files: selectedFiles, + callbacks: { + onProgress: (fileIndex, progress) => { + updateFileProgress(fileIndex, progress, "uploading"); + }, + onFileComplete: (fileIndex) => { + updateFileProgress(fileIndex, 100, "completed"); + }, + onFileError: (fileIndex, error) => { + updateFileProgress(fileIndex, 0, "error", error); + }, + }, + }); if (result.success) { toast.success(`${result.uploadedCount}개 파일 업로드 완료`); @@ -192,92 +129,85 @@ export function UploadFilesToDetailDialog({ {/* 파일 선택 영역 */} <div - className={`border-2 border-dashed rounded-lg p-8 transition-all duration-200 ${ - isDragging + {...getRootProps()} + className={`border-2 border-dashed rounded-lg p-8 transition-all duration-200 cursor-pointer ${ + isDragActive ? "border-primary bg-primary/5 scale-[1.02]" : "border-muted-foreground/30 hover:border-muted-foreground/50" }`} - onDragEnter={handleDragEnter} - onDragLeave={handleDragLeave} - onDragOver={handleDragOver} - onDrop={handleDrop} > - <input - type="file" - multiple - accept=".pdf,.doc,.docx,.xls,.xlsx,.dwg,.dxf,.zip" - onChange={(e) => handleFilesChange(Array.from(e.target.files || []))} - className="hidden" - id="detail-file-upload" - /> - <label - htmlFor="detail-file-upload" - className="flex flex-col items-center justify-center cursor-pointer" - > + <input {...getInputProps()} /> + <div className="flex flex-col items-center justify-center"> <FolderOpen className={`h-12 w-12 mb-3 transition-colors ${ - isDragging ? "text-primary" : "text-muted-foreground" + isDragActive ? "text-primary" : "text-muted-foreground" }`} /> <p className={`text-sm transition-colors ${ - isDragging + isDragActive ? "text-primary font-medium" : "text-muted-foreground" }`} > - {isDragging + {isDragActive ? "파일을 여기에 놓으세요" : "클릭하거나 파일을 드래그하여 선택"} </p> <p className="text-xs text-muted-foreground mt-1"> - PDF, DOC, DOCX, XLS, XLSX, DWG, DXF, ZIP (max 1GB per file) + 여러 파일을 한 번에 업로드할 수 있습니다 (최대 1GB/파일) </p> - </label> + </div> </div> {/* 선택된 파일 목록 */} {selectedFiles.length > 0 && ( <div className="border rounded-lg p-4"> - <div className="flex items-center justify-between mb-3"> - <h4 className="text-sm font-medium"> - 선택된 파일 ({selectedFiles.length}개) - </h4> - <Button - variant="ghost" - size="sm" - onClick={() => setSelectedFiles([])} - disabled={isUploading} - > - 전체 제거 - </Button> - </div> - <div className="max-h-48 overflow-auto space-y-2"> - {selectedFiles.map((file, index) => ( - <div - key={index} - className="flex items-center justify-between p-2 rounded bg-muted/50" - > - <div className="flex items-center gap-2 flex-1 min-w-0"> - <FileText className="h-4 w-4 text-muted-foreground shrink-0" /> - <div className="flex-1 min-w-0"> - <p className="text-sm truncate">{file.name}</p> - <p className="text-xs text-muted-foreground"> - {(file.size / 1024 / 1024).toFixed(2)} MB - </p> - </div> - </div> + {isUploading ? ( + // 업로드 중: 진행도 표시 + <FileUploadProgressList fileProgresses={fileProgresses} /> + ) : ( + // 대기 중: 삭제 버튼 표시 + <> + <div className="flex items-center justify-between mb-3"> + <h4 className="text-sm font-medium"> + 선택된 파일 ({selectedFiles.length}개) + </h4> <Button variant="ghost" size="sm" - onClick={() => handleRemoveFile(index)} - disabled={isUploading} + onClick={clearFiles} > - <X className="h-4 w-4" /> + 전체 제거 </Button> </div> - ))} - </div> + <div className="max-h-48 overflow-auto space-y-2"> + {selectedFiles.map((file, index) => ( + <div + key={index} + className="flex items-center justify-between p-2 rounded bg-muted/50" + > + <div className="flex items-center gap-2 flex-1 min-w-0"> + <FileText className="h-4 w-4 text-muted-foreground shrink-0" /> + <div className="flex-1 min-w-0"> + <p className="text-sm truncate">{file.name}</p> + <p className="text-xs text-muted-foreground"> + {(file.size / 1024 / 1024).toFixed(2)} MB + </p> + </div> + </div> + <Button + variant="ghost" + size="sm" + onClick={() => removeFile(index)} + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + </> + )} </div> )} </div> |
