"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" import { decryptWithServerAction } from "@/components/drm/drmUtils" // 첨부파일 타입 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([]) const [isLoading, setIsLoading] = React.useState(false) const [downloadingFiles, setDownloadingFiles] = React.useState>(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(attachments.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]) // 파일 다운로드 핸들러 (DRM 복호화 적용) const handleDownload = async (attachment: RfqAttachment) => { const attachmentId = attachment.attachmentId setDownloadingFiles(prev => new Set([...prev, attachmentId])) try { // 1. 파일 가져오기 const fileUrl = attachment.filePath.startsWith('http') ? attachment.filePath : `${window.location.origin}${attachment.filePath}` toast.loading(`파일 다운로드 준비 중...`, { id: `download-${attachmentId}` }) const response = await fetch(fileUrl) if (!response.ok) { throw new Error(`파일 다운로드 실패: ${response.status}`) } // 2. Blob을 File로 변환 const blob = await response.blob() const file = new File([blob], attachment.originalFileName, { type: blob.type }) // 3. DRM 복호화 toast.loading(`파일 복호화 중...`, { id: `download-${attachmentId}` }) const decryptedBuffer = await decryptWithServerAction(file) // 4. 복호화된 파일 다운로드 const decryptedBlob = new Blob([decryptedBuffer], { type: blob.type }) const downloadUrl = URL.createObjectURL(decryptedBlob) const link = document.createElement('a') link.href = downloadUrl link.download = attachment.originalFileName link.style.display = 'none' document.body.appendChild(link) link.click() document.body.removeChild(link) // 메모리 정리 setTimeout(() => URL.revokeObjectURL(downloadUrl), 100) toast.success(`파일 다운로드 완료: ${attachment.originalFileName}`, { id: `download-${attachmentId}` }) console.log(`다운로드 완료: ${attachment.originalFileName} (${formatFileSize(decryptedBlob.size)})`) } catch (error) { console.error("파일 다운로드 오류:", error) const errorMessage = error instanceof Error ? error.message : "파일 다운로드에 실패했습니다" toast.error(errorMessage, { id: `download-${attachmentId}` }) } 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 (
견적 첨부파일 {rfqData.rfqCode} - {rfqData.rfqTitle || rfqData.itemName || "견적"} {attachments.length > 0 && ` (${attachments.length}개 파일)`}
{isLoading ? (
{[...Array(3)].map((_, i) => (
))}
) : ( 타입 파일명 {/* 설명 */} 리비전 크기 생성자 생성일 액션 {attachments.length === 0 ? (
첨부된 파일이 없습니다.
) : ( attachments.map((attachment) => { const fileInfo = getFileInfo(attachment.originalFileName) const isDownloading = downloadingFiles.has(attachment.attachmentId) return ( {attachment.attachmentType}
{fileInfo.icon}
{attachment.originalFileName}
{/* {attachment.description || "-"} {attachment.revisionComment && (
{attachment.revisionComment}
)}
*/} {attachment.currentRevision} {attachment.fileSize ? formatFileSize(attachment.fileSize) : "-"} {attachment.createdByName || "-"} {attachment.createdAt ? format(new Date(attachment.createdAt), "MM-dd HH:mm") : "-"}
{/* 미리보기 버튼 (미리보기 가능한 파일만) */} {fileInfo.canPreview && ( )} {/* 다운로드 버튼 */} {/* 스마트 액션 버튼 (메인 액션) */} {/* */}
) }) )}
)}
{/* 하단 정보 */} {attachments.length > 0 && !isLoading && (
총 {attachments.length}개 파일 {attachments.some(a => a.fileSize) && ` · 전체 크기: ${formatFileSize( attachments.reduce((sum, a) => sum + (a.fileSize || 0), 0) )}` }
)} {/* 전체 다운로드 버튼 추가 */} {/* {attachments.length > 0 && !isLoading && ( )} */}
) }