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/dialogs/add-detail-drawing-dialog.tsx | 129 +++++++----
.../dialogs/upload-files-to-detail-dialog.tsx | 242 ++++++++-------------
2 files changed, 168 insertions(+), 203 deletions(-)
(limited to 'lib/dolce/dialogs')
diff --git a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx b/lib/dolce/dialogs/add-detail-drawing-dialog.tsx
index 290a226b..34d06368 100644
--- a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx
+++ b/lib/dolce/dialogs/add-detail-drawing-dialog.tsx
@@ -1,7 +1,6 @@
"use client";
-import { useState, useCallback } from "react";
-import { useDropzone } from "react-dropzone";
+import { useState } from "react";
import {
Dialog,
DialogContent,
@@ -22,8 +21,11 @@ import {
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Upload, X, FileIcon, Info } from "lucide-react";
import { toast } from "sonner";
-import { UnifiedDwgReceiptItem, editDetailDwgReceipt, uploadFilesToDetailDrawing } from "../actions";
+import { UnifiedDwgReceiptItem, editDetailDwgReceipt } from "../actions";
import { v4 as uuidv4 } from "uuid";
+import { useFileUploadWithProgress } from "../hooks/use-file-upload-with-progress";
+import { uploadFilesWithProgress } from "../utils/upload-with-progress";
+import { FileUploadProgressList } from "../components/file-upload-progress-list";
interface AddDetailDrawingDialogProps {
open: boolean;
@@ -80,30 +82,26 @@ export function AddDetailDrawingDialog({
const [drawingUsage, setDrawingUsage] = useState("");
const [registerKind, setRegisterKind] = useState("");
const [revision, setRevision] = useState("");
- const [files, setFiles] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
- // 파일 드롭 핸들러
- const onDrop = useCallback((acceptedFiles: File[]) => {
- setFiles((prev) => [...prev, ...acceptedFiles]);
- }, []);
-
- const { getRootProps, getInputProps, isDragActive } = useDropzone({
- onDrop,
- multiple: true,
- });
-
- // 파일 제거
- const removeFile = (index: number) => {
- setFiles((prev) => prev.filter((_, i) => i !== index));
- };
+ // 파일 업로드 훅 사용 (진행도 추적)
+ const {
+ fileProgresses,
+ files,
+ removeFile,
+ clearFiles,
+ updateFileProgress,
+ getRootProps,
+ getInputProps,
+ isDragActive,
+ } = useFileUploadWithProgress();
// 폼 초기화
const resetForm = () => {
setDrawingUsage("");
setRegisterKind("");
setRevision("");
- setFiles([]);
+ clearFiles();
};
// 제출
@@ -169,16 +167,27 @@ export function AddDetailDrawingDialog({
if (files.length > 0) {
toast.info(`${files.length}개 파일 업로드를 진행합니다...`);
- const formData = new FormData();
- formData.append("uploadId", uploadId);
- formData.append("userId", userId);
- formData.append("fileCount", String(files.length));
-
- files.forEach((file, index) => {
- formData.append(`file_${index}`, file);
+ // 모든 파일 상태를 uploading으로 변경
+ files.forEach((_, index) => {
+ updateFileProgress(index, 0, "uploading");
});
- const uploadResult = await uploadFilesToDetailDrawing(formData);
+ const uploadResult = await uploadFilesWithProgress({
+ uploadId,
+ userId,
+ files,
+ callbacks: {
+ onProgress: (fileIndex, progress) => {
+ updateFileProgress(fileIndex, progress, "uploading");
+ },
+ onFileComplete: (fileIndex) => {
+ updateFileProgress(fileIndex, 100, "completed");
+ },
+ onFileError: (fileIndex, error) => {
+ updateFileProgress(fileIndex, 0, "error", error);
+ },
+ },
+ });
if (uploadResult.success) {
toast.success(`상세도면 추가 및 ${uploadResult.uploadedCount}개 파일 업로드 완료`);
@@ -189,8 +198,10 @@ export function AddDetailDrawingDialog({
toast.success("상세도면이 추가되었습니다");
}
+ // API 호출 성공 시 무조건 다이얼로그 닫기 (파일 업로드 성공 여부와 무관)
resetForm();
onComplete();
+ onOpenChange(false);
} else {
toast.error("상세도면 추가에 실패했습니다");
}
@@ -318,7 +329,7 @@ export function AddDetailDrawingDialog({
파일을 드래그하거나 클릭하여 선택
- 여러 파일을 한 번에 업로드할 수 있습니다
+ 여러 파일을 한 번에 업로드할 수 있습니다 (최대 1GB/파일)
@@ -337,25 +348,49 @@ export function AddDetailDrawingDialog({
{/* 선택된 파일 목록 */}
{files.length > 0 && (
- {files.map((file, index) => (
-
-
- {file.name}
-
- {(file.size / 1024).toFixed(2)} KB
-
-
-
- ))}
+ {isSubmitting ? (
+ // 업로드 중: 진행도 표시
+
+ ) : (
+ // 대기 중: 삭제 버튼 표시
+ <>
+
+
+ 선택된 파일 ({files.length}개)
+
+
+
+
+ {files.map((file, index) => (
+
+
+
+
{file.name}
+
+ {(file.size / 1024 / 1024).toFixed(2)} MB
+
+
+
+
+ ))}
+
+ >
+ )}
)}
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({
{/* 파일 선택 영역 */}
{/* 선택된 파일 목록 */}
{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