From b67e36df49f067cbd5ba899f9fbcc755f38d4b4f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 4 Sep 2025 08:31:31 +0000 Subject: (대표님, 최겸, 임수민) 작업사항 커밋 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rfq-last/attachment/add-attachment-dialog.tsx | 365 ++++++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 lib/rfq-last/attachment/add-attachment-dialog.tsx (limited to 'lib/rfq-last/attachment/add-attachment-dialog.tsx') diff --git a/lib/rfq-last/attachment/add-attachment-dialog.tsx b/lib/rfq-last/attachment/add-attachment-dialog.tsx new file mode 100644 index 00000000..14baf7c7 --- /dev/null +++ b/lib/rfq-last/attachment/add-attachment-dialog.tsx @@ -0,0 +1,365 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { Plus, X, FileIcon } from "lucide-react" +import { toast } from "sonner" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Progress } from "@/components/ui/progress" +import { + Dropzone, + DropzoneDescription, + DropzoneInput, + DropzoneTitle, + DropzoneUploadIcon, + DropzoneZone, +} from "@/components/ui/dropzone" +import { + FileList, + FileListAction, + FileListDescription, + FileListHeader, + FileListIcon, + FileListInfo, + FileListItem, + FileListName, + FileListSize, +} from "@/components/ui/file-list" +import { useRouter } from "next/navigation"; + +const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB +const MAX_FILES = 10; // 최대 10개 파일 + +const addAttachmentSchema = z.object({ + description: z.string().optional(), + files: z + .array(z.instanceof(File)) + .min(1, "최소 1개 이상의 파일을 선택해주세요.") + .max(MAX_FILES, `최대 ${MAX_FILES}개까지 업로드 가능합니다.`) + .refine( + (files) => files.every((file) => file.size <= MAX_FILE_SIZE), + `각 파일 크기는 100MB를 초과할 수 없습니다.` + ), +}) + +type AddAttachmentFormData = z.infer + +interface AddAttachmentDialogProps { + rfqId: number; + attachmentType: "구매" | "설계"; + onSuccess?: () => void; +} + +export function AddAttachmentDialog({ + rfqId, + attachmentType, + onSuccess +}: AddAttachmentDialogProps) { + const [open, setOpen] = React.useState(false) + const [isSubmitting, setIsSubmitting] = React.useState(false) + const [uploadProgress, setUploadProgress] = React.useState(0) + const [selectedFiles, setSelectedFiles] = React.useState([]) + const router = useRouter(); + + const form = useForm({ + resolver: zodResolver(addAttachmentSchema), + defaultValues: { + description: "", + files: [], + }, + }) + + // 파일 크기 포맷팅 함수 + const formatFileSize = (bytes: number) => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; + }; + + // 파일 확장자 가져오기 + const getFileExtension = (fileName: string) => { + return fileName.split('.').pop()?.toUpperCase() || 'FILE'; + }; + + // 파일 추가 처리 + const handleFilesAdd = (newFiles: FileList | null) => { + if (!newFiles) return; + + const filesArray = Array.from(newFiles); + const totalFiles = selectedFiles.length + filesArray.length; + + if (totalFiles > MAX_FILES) { + toast.error(`최대 ${MAX_FILES}개까지만 업로드할 수 있습니다.`); + return; + } + + // 파일 크기 체크 + const oversizedFiles = filesArray.filter(file => file.size > MAX_FILE_SIZE); + if (oversizedFiles.length > 0) { + toast.error(`다음 파일들이 100MB를 초과합니다: ${oversizedFiles.map(f => f.name).join(", ")}`); + return; + } + + const updatedFiles = [...selectedFiles, ...filesArray]; + setSelectedFiles(updatedFiles); + form.setValue("files", updatedFiles, { shouldValidate: true }); + }; + + + const onSubmit = async (data: AddAttachmentFormData) => { + setIsSubmitting(true); + setUploadProgress(0); + + try { + const formData = new FormData(); + formData.append("rfqId", rfqId.toString()); + formData.append("attachmentType", attachmentType); + formData.append("description", data.description || ""); + + // 모든 파일 추가 + data.files.forEach((file) => { + formData.append("files", file); + }); + + // 진행률 시뮬레이션 + const progressInterval = setInterval(() => { + setUploadProgress((prev) => { + if (prev >= 90) { + clearInterval(progressInterval); + return 90; + } + return prev + 10; + }); + }, 200); + + const response = await fetch("/api/rfq-attachments/upload", { + method: "POST", + body: formData, + }); + + clearInterval(progressInterval); + setUploadProgress(100); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "파일 업로드 실패"); + } + + const result = await response.json(); + + if (result.success) { + toast.success(`${result.uploadedCount}개 파일이 성공적으로 업로드되었습니다`); + form.reset(); + setSelectedFiles([]); + setOpen(false); + + router.refresh() + onSuccess?.(); + } else { + toast.error(result.message); + } + } catch (error) { + toast.error(error instanceof Error ? error.message : "파일 업로드 중 오류가 발생했습니다"); + } finally { + setIsSubmitting(false); + setUploadProgress(0); + } + }; + + // 다이얼로그 닫을 때 상태 초기화 + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen) { + form.reset(); + setSelectedFiles([]); + setUploadProgress(0); + } + setOpen(newOpen); + }; + + const handleDropAccepted = (acceptedFiles: File[]) => { + const newFiles = [...selectedFiles, ...acceptedFiles] + setSelectedFiles(newFiles) + form.setValue('files', newFiles, { shouldValidate: true }) + } + + const removeFile = (index: number) => { + const updatedFiles = [...selectedFiles] + updatedFiles.splice(index, 1) + setSelectedFiles(updatedFiles) + form.setValue('files', updatedFiles, { shouldValidate: true }) + } + + return ( + + + + + + + + 새 첨부파일 추가 + + {attachmentType} 문서를 업로드합니다. (파일당 최대 100MB, 최대 {MAX_FILES}개) + + + +
+ +
+
+ ( + + 파일 첨부 + + + + + +
+ +
+ 파일을 여기에 드롭하세요 + + 또는 클릭하여 파일을 선택하세요 + +
+
+
+
+ +
+ )} + /> + + {/* 선택된 파일 목록 */} + {selectedFiles.length > 0 && ( +
+ + +
+ + 선택된 파일 ({selectedFiles.length}/{MAX_FILES}) + + + 총 {formatFileSize(selectedFiles.reduce((acc, file) => acc + file.size, 0))} + +
+
+ {selectedFiles.map((file, index) => ( + + + + + + {file.name} + + {getFileExtension(file.name)} • {formatFileSize(file.size)} + + + + + + + ))} +
+
+ )} + + ( + + 설명 (선택) + +