summaryrefslogtreecommitdiff
path: root/lib/rfq-last/vendor-response/editor/attachments-upload.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfq-last/vendor-response/editor/attachments-upload.tsx')
-rw-r--r--lib/rfq-last/vendor-response/editor/attachments-upload.tsx466
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