diff options
Diffstat (limited to 'lib/rfq-last/vendor-response/editor/attachments-upload.tsx')
| -rw-r--r-- | lib/rfq-last/vendor-response/editor/attachments-upload.tsx | 466 |
1 files changed, 466 insertions, 0 deletions
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<HTMLInputElement>(null) + const designInputRef = useRef<HTMLInputElement>(null) + const [purchaseDragActive, setPurchaseDragActive] = useState(false) + const [designDragActive, setDesignDragActive] = useState(false) + const [uploadErrors, setUploadErrors] = useState<string[]>([]) + + // 파일 유효성 검사 + 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 <File className="h-4 w-4 text-blue-500" /> + } + return <FileText className="h-4 w-4 text-gray-500" /> + } + + // 구매/설계 문서 개수 계산 + 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 ( + <div className="space-y-4"> + {/* 필수 파일 안내 */} + <Alert> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + <strong>문서 분류:</strong> 구매 문서(견적서, 상업조건 등)와 설계 문서(기술문서, 성적서, 인증서 등)를 구분하여 업로드하세요. + <br /> + <strong>허용 파일:</strong> PDF, Word, Excel, PowerPoint, 이미지 파일, 압축 파일(ZIP, RAR, 7Z) (최대 1GB) + </AlertDescription> + </Alert> + + {/* 업로드 오류 표시 */} + {uploadErrors.length > 0 && ( + <Alert variant="destructive"> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + <ul className="list-disc list-inside"> + {uploadErrors.map((error, index) => ( + <li key={index}>{error}</li> + ))} + </ul> + </AlertDescription> + </Alert> + )} + + {/* 두 개의 드래그존 */} + <div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> + {/* 구매 문서 업로드 영역 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Calculator className="h-5 w-5" /> + 구매 문서 + </CardTitle> + <CardDescription> + 견적서, 금액, 상업조건 관련 문서 + </CardDescription> + </CardHeader> + <CardContent> + <div + className={cn( + "border-2 border-dashed rounded-lg p-6 text-center transition-colors", + purchaseDragActive ? "border-blue-500 bg-blue-50" : "border-gray-300", + "hover:border-blue-400 hover:bg-blue-50/50" + )} + onDragEnter={handlePurchaseDrag} + onDragLeave={handlePurchaseDrag} + onDragOver={handlePurchaseDrag} + onDrop={handlePurchaseDrop} + > + <Calculator className="mx-auto h-10 w-10 text-blue-500 mb-3" /> + <p className="text-sm text-gray-600 mb-2"> + 구매 문서를 드래그하여 업로드 + </p> + <Button + type="button" + variant="outline" + size="sm" + onClick={() => purchaseInputRef.current?.click()} + className="border-blue-500 text-blue-600 hover:bg-blue-50" + > + <Paperclip className="h-4 w-4 mr-2" /> + 구매 문서 선택 + </Button> + <input + ref={purchaseInputRef} + type="file" + multiple + accept={acceptedFileTypes.all} + onChange={(e) => handleFileAdd(e.target.files, "구매")} + className="hidden" + /> + {purchaseCount > 0 && ( + <div className="mt-2"> + <Badge variant="secondary">{purchaseCount}개 업로드됨</Badge> + </div> + )} + </div> + </CardContent> + </Card> + + {/* 설계 문서 업로드 영역 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Wrench className="h-5 w-5" /> + 설계 문서 + </CardTitle> + <CardDescription> + 기술문서, 성적서, 인증서, 도면 등 + </CardDescription> + </CardHeader> + <CardContent> + <div + className={cn( + "border-2 border-dashed rounded-lg p-6 text-center transition-colors", + designDragActive ? "border-green-500 bg-green-50" : "border-gray-300", + "hover:border-green-400 hover:bg-green-50/50" + )} + onDragEnter={handleDesignDrag} + onDragLeave={handleDesignDrag} + onDragOver={handleDesignDrag} + onDrop={handleDesignDrop} + > + <Wrench className="mx-auto h-10 w-10 text-green-500 mb-3" /> + <p className="text-sm text-gray-600 mb-2"> + 설계 문서를 드래그하여 업로드 + </p> + <Button + type="button" + variant="outline" + size="sm" + onClick={() => designInputRef.current?.click()} + className="border-green-500 text-green-600 hover:bg-green-50" + > + <Paperclip className="h-4 w-4 mr-2" /> + 설계 문서 선택 + </Button> + <input + ref={designInputRef} + type="file" + multiple + accept={acceptedFileTypes.all} + onChange={(e) => handleFileAdd(e.target.files, "설계")} + className="hidden" + /> + {designCount > 0 && ( + <div className="mt-2"> + <Badge variant="secondary">{designCount}개 업로드됨</Badge> + </div> + )} + {/* <p className="text-xs text-gray-500 mt-2"> + 최대 1GB, 여러 파일 선택 가능 + </p> */} + </div> + </CardContent> + </Card> + </div> + + {/* 첨부파일 목록 */} + {(attachments.length > 0 || existingAttachments.length > 0) && ( + <Card> + <CardHeader> + <div className="flex items-center justify-between"> + <CardTitle>첨부파일 목록</CardTitle> + <div className="flex items-center gap-2"> + <Badge variant="outline" className="gap-1"> + <Calculator className="h-3 w-3" /> + 구매 {purchaseCount} + </Badge> + <Badge variant="outline" className="gap-1"> + <Wrench className="h-3 w-3" /> + 설계 {designCount} + </Badge> + <Badge variant="secondary"> + 총 {attachments.length + existingAttachments.length}개 + </Badge> + </div> + </div> + </CardHeader> + <CardContent> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[50px]">유형</TableHead> + <TableHead>파일명</TableHead> + <TableHead className="w-[100px]">크기</TableHead> + <TableHead className="w-[120px]">문서 구분</TableHead> + <TableHead className="w-[100px]">상태</TableHead> + <TableHead className="w-[80px]">작업</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {/* 기존 첨부파일 */} + {existingAttachments.map((file, index) => ( + <TableRow key={`existing-${index}`}> + <TableCell> + {getFileIcon(file.originalFileName)} + </TableCell> + <TableCell> + <div> + <p className="font-medium">{file.originalFileName}</p> + {file.description && ( + <p className="text-xs text-muted-foreground"> + {file.description} + </p> + )} + </div> + </TableCell> + <TableCell className="text-sm text-muted-foreground"> + {formatBytes(file.fileSize || 0)} + </TableCell> + <TableCell> + <Badge + variant={file.attachmentType === "구매" ? "default" : "secondary"} + className="gap-1" + > + {file.attachmentType === "구매" ? + <Calculator className="h-3 w-3" /> : + <Wrench className="h-3 w-3" /> + } + {file.attachmentType} + </Badge> + </TableCell> + <TableCell> + <Badge variant="secondary"> + <FileCheck className="h-3 w-3 mr-1" /> + 기존 + </Badge> + </TableCell> + <TableCell> + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => window.open(file.filePath, '_blank')} + > + <Download className="h-4 w-4" /> + </Button> + </TableCell> + </TableRow> + ))} + + {/* 새로 추가된 파일 */} + {attachments.map((file, index) => ( + <TableRow key={`new-${index}`}> + <TableCell> + {getFileIcon(file.name)} + </TableCell> + <TableCell> + <div> + <p className="font-medium">{file.name}</p> + </div> + </TableCell> + <TableCell className="text-sm text-muted-foreground"> + {formatBytes(file.size)} + </TableCell> + <TableCell> + <div className="flex items-center gap-1"> + <Button + type="button" + variant={file.attachmentType === "구매" ? "default" : "ghost"} + size="sm" + className="h-7 px-2 text-xs" + onClick={() => handleTypeChange(index, "구매")} + > + <Calculator className="h-3 w-3 mr-1" /> + 구매 + </Button> + <Button + type="button" + variant={file.attachmentType === "설계" ? "default" : "ghost"} + size="sm" + className="h-7 px-2 text-xs" + onClick={() => handleTypeChange(index, "설계")} + > + <Wrench className="h-3 w-3 mr-1" /> + 설계 + </Button> + </div> + </TableCell> + <TableCell> + <Badge variant="default"> + <Upload className="h-3 w-3 mr-1" /> + 신규 + </Badge> + </TableCell> + <TableCell> + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => handleFileRemove(index)} + > + <Trash2 className="h-4 w-4 text-red-500" /> + </Button> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </CardContent> + </Card> + )} + </div> + ) +}
\ No newline at end of file |
