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 --- .../dialogs/upload-files-to-detail-dialog.tsx | 242 ++++++++------------- 1 file changed, 86 insertions(+), 156 deletions(-) (limited to 'lib/dolce/dialogs/upload-files-to-detail-dialog.tsx') 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([]); 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({ {/* 파일 선택 영역 */}
- handleFilesChange(Array.from(e.target.files || []))} - className="hidden" - id="detail-file-upload" - /> -
{/* 선택된 파일 목록 */} {selectedFiles.length > 0 && (
-
-

- 선택된 파일 ({selectedFiles.length}개) -

- -
-
- {selectedFiles.map((file, index) => ( -
-
- -
-

{file.name}

-

- {(file.size / 1024 / 1024).toFixed(2)} MB -

-
-
+ {isUploading ? ( + // 업로드 중: 진행도 표시 + + ) : ( + // 대기 중: 삭제 버튼 표시 + <> +
+

+ 선택된 파일 ({selectedFiles.length}개) +

- ))} -
+
+ {selectedFiles.map((file, index) => ( +
+
+ +
+

{file.name}

+

+ {(file.size / 1024 / 1024).toFixed(2)} MB +

+
+
+ +
+ ))} +
+ + )}
)}
-- cgit v1.2.3