From 675b4e3d8ffcb57a041db285417d81e61284d900 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Sun, 14 Sep 2025 05:28:01 +0000 Subject: (대표님) RFQ-last, tbe-last, 기본계약 템플릿 내 견적,입찰,계약 추가, env.dev NAS_PATH 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vendor-response/editor/attachments-upload.tsx | 466 +++++++++++++++++++++ 1 file changed, 466 insertions(+) create mode 100644 lib/rfq-last/vendor-response/editor/attachments-upload.tsx (limited to 'lib/rfq-last/vendor-response/editor/attachments-upload.tsx') diff --git a/lib/rfq-last/vendor-response/editor/attachments-upload.tsx b/lib/rfq-last/vendor-response/editor/attachments-upload.tsx new file mode 100644 index 00000000..a2967767 --- /dev/null +++ b/lib/rfq-last/vendor-response/editor/attachments-upload.tsx @@ -0,0 +1,466 @@ +"use client" + +import { useState, useRef } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Upload, + FileText, + File, + Trash2, + Download, + AlertCircle, + Paperclip, + FileCheck, + Calculator, + Wrench +} from "lucide-react" +import { formatBytes } from "@/lib/utils" +import { cn } from "@/lib/utils" + +interface FileWithType extends File { + attachmentType?: "구매" | "설계" + description?: string +} + +interface AttachmentsUploadProps { + attachments: FileWithType[] + onAttachmentsChange: (files: FileWithType[]) => void + existingAttachments?: any[] +} + +const acceptedFileTypes = { + documents: ".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx", + images: ".jpg,.jpeg,.png,.gif,.bmp", + compressed: ".zip,.rar,.7z", + all: ".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.bmp,.zip,.rar,.7z" +} + +export default function AttachmentsUpload({ + attachments, + onAttachmentsChange, + existingAttachments = [] +}: AttachmentsUploadProps) { + const purchaseInputRef = useRef(null) + const designInputRef = useRef(null) + const [purchaseDragActive, setPurchaseDragActive] = useState(false) + const [designDragActive, setDesignDragActive] = useState(false) + const [uploadErrors, setUploadErrors] = useState([]) + + // 파일 유효성 검사 + const validateFile = (file: File): string | null => { + const maxSize = 1024 * 1024 * 1024 // 10MB + const allowedExtensions = acceptedFileTypes.all.split(',') + const fileExtension = `.${file.name.split('.').pop()?.toLowerCase()}` + + if (file.size > maxSize) { + return `${file.name}: 파일 크기가 1GB를 초과합니다` + } + + if (!allowedExtensions.includes(fileExtension)) { + return `${file.name}: 허용되지 않은 파일 형식입니다` + } + + return null + } + + // 파일 추가 + const handleFileAdd = (files: FileList | null, type: "구매" | "설계") => { + if (!files) return + + const newFiles: FileWithType[] = [] + const errors: string[] = [] + + Array.from(files).forEach(file => { + const error = validateFile(file) + if (error) { + errors.push(error) + } else { + const fileWithType = Object.assign(file, { + attachmentType: type, + description: "" + }) + newFiles.push(fileWithType) + } + }) + + if (errors.length > 0) { + setUploadErrors(errors) + setTimeout(() => setUploadErrors([]), 5000) + } + + if (newFiles.length > 0) { + onAttachmentsChange([...attachments, ...newFiles]) + } + } + + // 구매 드래그 앤 드롭 핸들러 + const handlePurchaseDrag = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (e.type === "dragenter" || e.type === "dragover") { + setPurchaseDragActive(true) + } else if (e.type === "dragleave") { + setPurchaseDragActive(false) + } + } + + const handlePurchaseDrop = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setPurchaseDragActive(false) + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + handleFileAdd(e.dataTransfer.files, "구매") + } + } + + // 설계 드래그 앤 드롭 핸들러 + const handleDesignDrag = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (e.type === "dragenter" || e.type === "dragover") { + setDesignDragActive(true) + } else if (e.type === "dragleave") { + setDesignDragActive(false) + } + } + + const handleDesignDrop = (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDesignDragActive(false) + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + handleFileAdd(e.dataTransfer.files, "설계") + } + } + + // 파일 삭제 + const handleFileRemove = (index: number) => { + const newFiles = attachments.filter((_, i) => i !== index) + onAttachmentsChange(newFiles) + } + + // 파일 타입 변경 + const handleTypeChange = (index: number, newType: "구매" | "설계") => { + const newFiles = [...attachments] + newFiles[index].attachmentType = newType + onAttachmentsChange(newFiles) + } + + // 파일 아이콘 가져오기 + const getFileIcon = (fileName: string) => { + const extension = fileName.split('.').pop()?.toLowerCase() + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp'] + + if (imageExtensions.includes(extension || '')) { + return + } + return + } + + // 구매/설계 문서 개수 계산 + const purchaseCount = attachments.filter(f => f.attachmentType === "구매").length + + existingAttachments.filter(f => f.attachmentType === "구매").length + const designCount = attachments.filter(f => f.attachmentType === "설계").length + + existingAttachments.filter(f => f.attachmentType === "설계").length + + return ( +
+ {/* 필수 파일 안내 */} + + + + 문서 분류: 구매 문서(견적서, 상업조건 등)와 설계 문서(기술문서, 성적서, 인증서 등)를 구분하여 업로드하세요. +
+ 허용 파일: PDF, Word, Excel, PowerPoint, 이미지 파일, 압축 파일(ZIP, RAR, 7Z) (최대 1GB) +
+
+ + {/* 업로드 오류 표시 */} + {uploadErrors.length > 0 && ( + + + +
    + {uploadErrors.map((error, index) => ( +
  • {error}
  • + ))} +
+
+
+ )} + + {/* 두 개의 드래그존 */} +
+ {/* 구매 문서 업로드 영역 */} + + + + + 구매 문서 + + + 견적서, 금액, 상업조건 관련 문서 + + + +
+ +

+ 구매 문서를 드래그하여 업로드 +

+ + handleFileAdd(e.target.files, "구매")} + className="hidden" + /> + {purchaseCount > 0 && ( +
+ {purchaseCount}개 업로드됨 +
+ )} +
+
+
+ + {/* 설계 문서 업로드 영역 */} + + + + + 설계 문서 + + + 기술문서, 성적서, 인증서, 도면 등 + + + +
+ +

+ 설계 문서를 드래그하여 업로드 +

+ + handleFileAdd(e.target.files, "설계")} + className="hidden" + /> + {designCount > 0 && ( +
+ {designCount}개 업로드됨 +
+ )} + {/*

+ 최대 1GB, 여러 파일 선택 가능 +

*/} +
+
+
+
+ + {/* 첨부파일 목록 */} + {(attachments.length > 0 || existingAttachments.length > 0) && ( + + +
+ 첨부파일 목록 +
+ + + 구매 {purchaseCount} + + + + 설계 {designCount} + + + 총 {attachments.length + existingAttachments.length}개 + +
+
+
+ + + + + 유형 + 파일명 + 크기 + 문서 구분 + 상태 + 작업 + + + + {/* 기존 첨부파일 */} + {existingAttachments.map((file, index) => ( + + + {getFileIcon(file.originalFileName)} + + +
+

{file.originalFileName}

+ {file.description && ( +

+ {file.description} +

+ )} +
+
+ + {formatBytes(file.fileSize || 0)} + + + + {file.attachmentType === "구매" ? + : + + } + {file.attachmentType} + + + + + + 기존 + + + + + +
+ ))} + + {/* 새로 추가된 파일 */} + {attachments.map((file, index) => ( + + + {getFileIcon(file.name)} + + +
+

{file.name}

+
+
+ + {formatBytes(file.size)} + + +
+ + +
+
+ + + + 신규 + + + + + +
+ ))} +
+
+
+
+ )} +
+ ) +} \ No newline at end of file -- cgit v1.2.3