From 0fddf148402fd6b99a1b3800d73679899bcb2ed3 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 13 Jun 2025 07:11:18 +0000 Subject: (대표님) 20250613 16시 10분 global css, b-rfq, document 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ship-vendor-document/add-attachment-dialog.tsx | 368 +++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 components/ship-vendor-document/add-attachment-dialog.tsx (limited to 'components/ship-vendor-document/add-attachment-dialog.tsx') diff --git a/components/ship-vendor-document/add-attachment-dialog.tsx b/components/ship-vendor-document/add-attachment-dialog.tsx new file mode 100644 index 00000000..2f2467a3 --- /dev/null +++ b/components/ship-vendor-document/add-attachment-dialog.tsx @@ -0,0 +1,368 @@ +"use client" + +import React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Button } from "@/components/ui/button" +import { Progress } from "@/components/ui/progress" +import { + Upload, + FileText, + X, + Loader2, + CheckCircle, + Paperclip +} from "lucide-react" +import { toast } from "sonner" +import { useSession } from "next-auth/react" + +/* ------------------------------------------------------------------------------------------------- + * Schema & Types + * -----------------------------------------------------------------------------------------------*/ + +// 파일 검증 스키마 +const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB +const ACCEPTED_FILE_TYPES = [ + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-excel', + 'image/jpeg', + 'image/png', + 'image/gif', + 'text/plain', + 'application/zip', + 'application/x-zip-compressed' +] + +const attachmentUploadSchema = z.object({ + attachments: z + .array(z.instanceof(File)) + .min(1, "최소 1개의 파일을 업로드해주세요") + .max(10, "최대 10개의 파일까지 업로드 가능합니다") + .refine( + (files) => files.every((file) => file.size <= MAX_FILE_SIZE), + "파일 크기는 50MB 이하여야 합니다" + ) + .refine( + (files) => files.every((file) => ACCEPTED_FILE_TYPES.includes(file.type)), + "지원하지 않는 파일 형식입니다" + ), +}) + +interface AddAttachmentDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + revisionId: number + revisionName: string + onSuccess?: (result?: any) => void +} + +/* ------------------------------------------------------------------------------------------------- + * File Upload Component + * -----------------------------------------------------------------------------------------------*/ +function FileUploadArea({ + files, + onFilesChange +}: { + files: File[] + onFilesChange: (files: File[]) => void +}) { + const fileInputRef = React.useRef(null) + + const handleFileSelect = (event: React.ChangeEvent) => { + const selectedFiles = Array.from(event.target.files || []) + if (selectedFiles.length > 0) { + onFilesChange([...files, ...selectedFiles]) + } + } + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault() + const droppedFiles = Array.from(event.dataTransfer.files) + if (droppedFiles.length > 0) { + onFilesChange([...files, ...droppedFiles]) + } + } + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault() + } + + const removeFile = (index: number) => { + onFilesChange(files.filter((_, i) => i !== index)) + } + + 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] + } + + return ( +
+
fileInputRef.current?.click()} + > + +

+ 추가할 파일을 드래그하여 놓거나 클릭하여 선택하세요 +

+

+ PDF, Word, Excel, 이미지, 텍스트, ZIP 파일 지원 (최대 50MB) +

+ +
+ + {files.length > 0 && ( +
+

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

+
+ {files.map((file, index) => ( +
+
+ +
+

+ {file.name} +

+

+ {formatFileSize(file.size)} +

+
+
+ +
+ ))} +
+
+ )} +
+ ) +} + +/* ------------------------------------------------------------------------------------------------- + * Main Dialog Component + * -----------------------------------------------------------------------------------------------*/ +export function AddAttachmentDialog({ + open, + onOpenChange, + revisionId, + revisionName, + onSuccess + }: AddAttachmentDialogProps) { + const [isUploading, setIsUploading] = React.useState(false) + const [uploadProgress, setUploadProgress] = React.useState(0) + const { data: session } = useSession() + + const userName = React.useMemo(() => { + return session?.user?.name ? session.user.name : null; + }, [session]); + + type AttachmentUploadSchema = z.infer + + const form = useForm({ + resolver: zodResolver(attachmentUploadSchema), + defaultValues: { + attachments: [], + }, + }) + + const watchedFiles = form.watch("attachments") + + const handleDialogClose = () => { + if (!isUploading) { + form.reset() + setUploadProgress(0) + onOpenChange(false) + } + } + + const onSubmit = async (data: AttachmentUploadSchema) => { + setIsUploading(true) + setUploadProgress(0) + + try { + const formData = new FormData() + formData.append("revisionId", String(revisionId)) + formData.append("uploaderName", userName || "evcp") + + // 파일들 추가 + data.attachments.forEach((file) => { + formData.append("attachments", file) + }) + + // 진행률 업데이트 시뮬레이션 + const totalSize = data.attachments.reduce((sum, file) => sum + file.size, 0) + let uploadedSize = 0 + + const progressInterval = setInterval(() => { + uploadedSize += totalSize * 0.1 + const progress = Math.min((uploadedSize / totalSize) * 100, 90) + setUploadProgress(progress) + }, 300) + + const response = await fetch('/api/revision-attachment', { + method: 'POST', + body: formData, + }) + + clearInterval(progressInterval) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || errorData.details || '첨부파일 업로드에 실패했습니다.') + } + + const result = await response.json() + setUploadProgress(100) + + toast.success( + result.message || + `${result.data?.uploadedFiles?.length || 0}개 첨부파일이 추가되었습니다.` + ) + + console.log('✅ 첨부파일 업로드 성공:', result) + + setTimeout(() => { + handleDialogClose() + onSuccess?.(result) + }, 1000) + + } catch (error) { + console.error('❌ 첨부파일 업로드 오류:', error) + toast.error(error instanceof Error ? error.message : "첨부파일 업로드 중 오류가 발생했습니다") + } finally { + setIsUploading(false) + setTimeout(() => setUploadProgress(0), 2000) + } + } + + return ( + + + {/* 고정 헤더 */} + + + + 첨부파일 추가 + + + 리비전 {revisionName}에 추가 첨부파일을 업로드합니다 + + + +
+ + {/* 스크롤 가능한 중간 영역 */} +
+ {/* 파일 업로드 */} + ( + + 첨부파일 + + + + + + )} + /> + + {/* 업로드 진행률 */} + {isUploading && ( +
+
+ 업로드 진행률 + {uploadProgress.toFixed(0)}% +
+ + {uploadProgress === 100 && ( +
+ + 업로드 완료 +
+ )} +
+ )} +
+ + {/* 고정 버튼 영역 */} + + + + +
+ +
+
+ ) + } \ No newline at end of file -- cgit v1.2.3