summaryrefslogtreecommitdiff
path: root/lib/rfq-last/vendor-response/rfq-attachments-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfq-last/vendor-response/rfq-attachments-dialog.tsx')
-rw-r--r--lib/rfq-last/vendor-response/rfq-attachments-dialog.tsx407
1 files changed, 407 insertions, 0 deletions
diff --git a/lib/rfq-last/vendor-response/rfq-attachments-dialog.tsx b/lib/rfq-last/vendor-response/rfq-attachments-dialog.tsx
new file mode 100644
index 00000000..cfe24d73
--- /dev/null
+++ b/lib/rfq-last/vendor-response/rfq-attachments-dialog.tsx
@@ -0,0 +1,407 @@
+"use client"
+
+import * as React from "react"
+import { format } from "date-fns"
+import { Download, FileText, Eye, ExternalLink, Loader2 } from "lucide-react"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Skeleton } from "@/components/ui/skeleton"
+import { toast } from "sonner"
+import { RfqsLastView } from "@/db/schema"
+import { getRfqAttachmentsAction } from "../service"
+import { downloadFile, quickPreview, smartFileAction, formatFileSize, getFileInfo } from "@/lib/file-download"
+
+// 첨부파일 타입
+interface RfqAttachment {
+ attachmentId: number
+ attachmentType: string
+ serialNo: string
+ description: string | null
+ currentRevision: string
+ fileName: string
+ originalFileName: string
+ filePath: string
+ fileSize: number | null
+ fileType: string | null
+ createdByName: string | null
+ createdAt: Date | null
+ updatedAt: Date | null
+ revisionComment?: string | null
+}
+
+interface RfqAttachmentsDialogProps {
+ isOpen: boolean
+ onClose: () => void
+ rfqData: RfqsLastView
+}
+
+export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachmentsDialogProps) {
+ const [attachments, setAttachments] = React.useState<RfqAttachment[]>([])
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [downloadingFiles, setDownloadingFiles] = React.useState<Set<number>>(new Set())
+ const [isDownloadingAll, setIsDownloadingAll] = React.useState(false);
+
+
+ const handleDownloadAll = async () => {
+ setIsDownloadingAll(true);
+ try {
+
+ const rfqId = rfqData.id
+
+ const attachments = await getRfqAttachmentsAction(rfqId);
+
+ if (!attachments.success || attachments.data.length === 0) {
+ toast.error(result.error || "다운로드할 파일이 없습니다");
+ }
+
+
+ // 2. ZIP 파일 생성 (서버에서)
+ const response = await fetch(`/api/rfq/attachments/download-all`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ rfqId,
+ files: attachments.data.map(a => ({
+ path: a.filePath,
+ name: a.originalFileName
+ }))
+ })
+ });
+
+ if (!response.ok) throw new Error('ZIP 생성 실패');
+
+ // 3. ZIP 다운로드
+ const blob = await response.blob();
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+
+ // RFQ 코드를 포함한 파일명
+ const date = new Date().toISOString().slice(0, 10);
+ link.download = `RFQ_${rfqId}_attachments_${date}.zip`;
+ link.href = url;
+
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ } finally {
+ setIsDownloadingAll(false);
+ }
+ };
+
+ // 첨부파일 목록 로드
+ React.useEffect(() => {
+ if (!isOpen || !rfqData.id) return
+
+ const loadAttachments = async () => {
+ setIsLoading(true)
+ try {
+ const result = await getRfqAttachmentsAction(rfqData.id)
+
+ if (result.success) {
+ setAttachments(result.data)
+ } else {
+ toast.error(result.error || "첨부파일을 불러오는데 실패했습니다")
+ setAttachments([])
+ }
+ } catch (error) {
+ console.error("첨부파일 로드 오류:", error)
+ toast.error("첨부파일을 불러오는데 실패했습니다")
+ setAttachments([])
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ loadAttachments()
+ }, [isOpen, rfqData.id])
+
+ // 파일 다운로드 핸들러
+ const handleDownload = async (attachment: RfqAttachment) => {
+ const attachmentId = attachment.attachmentId
+ setDownloadingFiles(prev => new Set([...prev, attachmentId]))
+
+ try {
+ const result = await downloadFile(
+ attachment.filePath,
+ attachment.originalFileName,
+ {
+ action: 'download',
+ showToast: true,
+ showSuccessToast: true,
+ onSuccess: (fileName, fileSize) => {
+ console.log(`다운로드 완료: ${fileName} (${formatFileSize(fileSize || 0)})`)
+ },
+ onError: (error) => {
+ console.error(`다운로드 실패: ${error}`)
+ }
+ }
+ )
+
+ if (!result.success) {
+ console.error("다운로드 결과:", result)
+ }
+ } catch (error) {
+ console.error("파일 다운로드 오류:", error)
+ toast.error("파일 다운로드에 실패했습니다")
+ } finally {
+ setDownloadingFiles(prev => {
+ const newSet = new Set(prev)
+ newSet.delete(attachmentId)
+ return newSet
+ })
+ }
+ }
+
+ // 파일 미리보기 핸들러
+ const handlePreview = async (attachment: RfqAttachment) => {
+ const fileInfo = getFileInfo(attachment.originalFileName)
+
+ if (!fileInfo.canPreview) {
+ toast.info("이 파일 형식은 미리보기를 지원하지 않습니다. 다운로드를 진행합니다.")
+ return handleDownload(attachment)
+ }
+
+ try {
+ const result = await quickPreview(attachment.filePath, attachment.originalFileName)
+
+ if (!result.success) {
+ console.error("미리보기 결과:", result)
+ }
+ } catch (error) {
+ console.error("파일 미리보기 오류:", error)
+ toast.error("파일 미리보기에 실패했습니다")
+ }
+ }
+
+
+ // 첨부파일 타입별 색상
+ const getAttachmentTypeBadgeVariant = (type: string) => {
+ switch (type.toLowerCase()) {
+ case "견적요청서": return "default"
+ case "기술사양서": return "secondary"
+ case "도면": return "outline"
+ default: return "outline"
+ }
+ }
+
+ return (
+ <Dialog open={isOpen} onOpenChange={onClose}>
+ <DialogContent className="max-w-6xl h-[85vh] flex flex-col">
+ <DialogHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <DialogTitle>견적 첨부파일</DialogTitle>
+ <DialogDescription>
+ {rfqData.rfqCode} - {rfqData.rfqTitle || rfqData.itemName || "견적"}
+ {attachments.length > 0 && ` (${attachments.length}개 파일)`}
+ </DialogDescription>
+ </div>
+
+ </div>
+ </DialogHeader>
+
+ <ScrollArea className="flex-1">
+ {isLoading ? (
+ <div className="space-y-3">
+ {[...Array(3)].map((_, i) => (
+ <div key={i} className="flex items-center space-x-4 p-3 border rounded-lg">
+ <Skeleton className="h-8 w-8" />
+ <div className="space-y-2 flex-1">
+ <Skeleton className="h-4 w-[300px]" />
+ <Skeleton className="h-3 w-[200px]" />
+ </div>
+ <div className="flex gap-2">
+ <Skeleton className="h-8 w-20" />
+ <Skeleton className="h-8 w-20" />
+ </div>
+ </div>
+ ))}
+ </div>
+ ) : (
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-[120px]">타입</TableHead>
+ <TableHead>파일명</TableHead>
+ {/* <TableHead>설명</TableHead> */}
+ <TableHead className="w-[90px]">리비전</TableHead>
+ <TableHead className="w-[100px]">크기</TableHead>
+ <TableHead className="w-[120px]">생성자</TableHead>
+ <TableHead className="w-[120px]">생성일</TableHead>
+ <TableHead className="w-[140px]">액션</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {attachments.length === 0 ? (
+ <TableRow>
+ <TableCell colSpan={8} className="text-center text-muted-foreground py-12">
+ <div className="flex flex-col items-center gap-2">
+ <FileText className="h-8 w-8 text-muted-foreground" />
+ <span>첨부된 파일이 없습니다.</span>
+ </div>
+ </TableCell>
+ </TableRow>
+ ) : (
+ attachments.map((attachment) => {
+ const fileInfo = getFileInfo(attachment.originalFileName)
+ const isDownloading = downloadingFiles.has(attachment.attachmentId)
+
+ return (
+ <TableRow key={attachment.attachmentId}>
+ <TableCell>
+ <Badge
+ variant={getAttachmentTypeBadgeVariant(attachment.attachmentType)}
+ className="text-xs"
+ >
+ {attachment.attachmentType}
+ </Badge>
+ </TableCell>
+ <TableCell>
+ <div className="flex items-center gap-2">
+ <span className="text-lg">{fileInfo.icon}</span>
+ <div className="flex flex-col min-w-0">
+ <span className="text-sm font-medium truncate" title={attachment.originalFileName}>
+ {attachment.originalFileName}
+ </span>
+ </div>
+ </div>
+ </TableCell>
+ {/* <TableCell>
+ <span className="text-sm" title={attachment.description || ""}>
+ {attachment.description || "-"}
+ </span>
+ {attachment.revisionComment && (
+ <div className="text-xs text-muted-foreground mt-1">
+ {attachment.revisionComment}
+ </div>
+ )}
+ </TableCell> */}
+ <TableCell>
+ <Badge variant="secondary" className="font-mono text-xs">
+ {attachment.currentRevision}
+ </Badge>
+ </TableCell>
+ <TableCell className="text-xs text-muted-foreground">
+ {attachment.fileSize ? formatFileSize(attachment.fileSize) : "-"}
+ </TableCell>
+ <TableCell className="text-sm">
+ {attachment.createdByName || "-"}
+ </TableCell>
+ <TableCell className="text-xs text-muted-foreground">
+ {attachment.createdAt ? format(new Date(attachment.createdAt), "MM-dd HH:mm") : "-"}
+ </TableCell>
+ <TableCell>
+ <div className="flex items-center gap-1">
+ {/* 미리보기 버튼 (미리보기 가능한 파일만) */}
+ {fileInfo.canPreview && (
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => handlePreview(attachment)}
+ disabled={isDownloading}
+ title="미리보기"
+ >
+ <Eye className="h-4 w-4" />
+ </Button>
+ )}
+
+ {/* 다운로드 버튼 */}
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDownload(attachment)}
+ disabled={isDownloading}
+ title="다운로드"
+ >
+ {isDownloading ? (
+ <Loader2 className="h-4 w-4 animate-spin" />
+ ) : (
+ <Download className="h-4 w-4" />
+ )}
+ </Button>
+
+ {/* 스마트 액션 버튼 (메인 액션) */}
+ {/* <Button
+ variant="outline"
+ size="sm"
+ onClick={() => handleSmartAction(attachment)}
+ disabled={isDownloading}
+ className="ml-1"
+ >
+ {isDownloading ? (
+ <Loader2 className="h-4 w-4 animate-spin mr-1" />
+ ) : fileInfo.canPreview ? (
+ <Eye className="h-4 w-4 mr-1" />
+ ) : (
+ <Download className="h-4 w-4 mr-1" />
+ )}
+ {fileInfo.canPreview ? "보기" : "다운로드"}
+ </Button> */}
+ </div>
+ </TableCell>
+ </TableRow>
+ )
+ })
+ )}
+ </TableBody>
+ </Table>
+ )}
+ </ScrollArea>
+ <div className="flex items-center justify-between border-t pt-4 text-xs text-muted-foreground">
+ {/* 하단 정보 */}
+ {attachments.length > 0 && !isLoading && (
+ <div className="flex justify-between items-center">
+ <span>
+ 총 {attachments.length}개 파일
+ {attachments.some(a => a.fileSize) &&
+ ` · 전체 크기: ${formatFileSize(
+ attachments.reduce((sum, a) => sum + (a.fileSize || 0), 0)
+ )}`
+ }
+ </span>
+ </div>
+ )}
+
+ {/* 전체 다운로드 버튼 추가 */}
+ {attachments.length > 0 && !isLoading && (
+ <Button
+ onClick={handleDownloadAll}
+ disabled={isDownloadingAll}
+ variant="outline"
+ size="sm"
+ >
+ {isDownloadingAll ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ 다운로드 중...
+ </>
+ ) : (
+ <>
+ <Download className="mr-2 h-4 w-4" />
+ 전체 다운로드
+ </>
+ )}
+ </Button>
+ )}
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file