summaryrefslogtreecommitdiff
path: root/lib/dolce/dialogs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dolce/dialogs')
-rw-r--r--lib/dolce/dialogs/add-detail-drawing-dialog.tsx129
-rw-r--r--lib/dolce/dialogs/upload-files-to-detail-dialog.tsx242
2 files changed, 168 insertions, 203 deletions
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<string>("");
const [registerKind, setRegisterKind] = useState<string>("");
const [revision, setRevision] = useState<string>("");
- const [files, setFiles] = useState<File[]>([]);
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({
파일을 드래그하거나 클릭하여 선택
</p>
<p className="text-xs text-muted-foreground">
- 여러 파일을 한 번에 업로드할 수 있습니다
+ 여러 파일을 한 번에 업로드할 수 있습니다 (최대 1GB/파일)
</p>
</div>
</div>
@@ -337,25 +348,49 @@ export function AddDetailDrawingDialog({
{/* 선택된 파일 목록 */}
{files.length > 0 && (
<div className="space-y-2 mt-4">
- {files.map((file, index) => (
- <div
- key={index}
- className="flex items-center gap-2 p-2 border rounded-lg"
- >
- <FileIcon className="h-4 w-4 text-muted-foreground" />
- <span className="flex-1 text-sm truncate">{file.name}</span>
- <span className="text-xs text-muted-foreground">
- {(file.size / 1024).toFixed(2)} KB
- </span>
- <Button
- variant="ghost"
- size="icon"
- onClick={() => removeFile(index)}
- >
- <X className="h-4 w-4" />
- </Button>
- </div>
- ))}
+ {isSubmitting ? (
+ // 업로드 중: 진행도 표시
+ <FileUploadProgressList fileProgresses={fileProgresses} />
+ ) : (
+ // 대기 중: 삭제 버튼 표시
+ <>
+ <div className="flex items-center justify-between mb-2">
+ <h4 className="text-sm font-medium">
+ 선택된 파일 ({files.length}개)
+ </h4>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={clearFiles}
+ >
+ 전체 제거
+ </Button>
+ </div>
+ <div className="max-h-48 overflow-auto space-y-2">
+ {files.map((file, index) => (
+ <div
+ key={index}
+ className="flex items-center gap-2 p-2 border rounded-lg bg-muted/50"
+ >
+ <FileIcon 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>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => removeFile(index)}
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ </div>
+ ))}
+ </div>
+ </>
+ )}
</div>
)}
</div>
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>