// @/lib/rfq-last/vendor/vendor-response-table.tsx "use client"; import * as React from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Download, FileText, RefreshCw, Eye, Trash2, File, FileImage, FileSpreadsheet, FileCode, Building2, Calendar, AlertCircle } from "lucide-react"; import { format, formatDistanceToNow, isValid, isBefore, isAfter } from "date-fns"; import { ko } from "date-fns/locale"; import { type ColumnDef } from "@tanstack/react-table"; import { Checkbox } from "@/components/ui/checkbox"; import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header"; import { ClientDataTable } from "@/components/client-data-table/data-table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import type { DataTableAdvancedFilterField, DataTableRowAction, } from "@/types/table"; import { cn } from "@/lib/utils"; import { getRfqVendorAttachments } from "@/lib/rfq-last/service"; import { downloadFile } from "@/lib/file-download"; import { toast } from "sonner"; // 타입 정의 interface VendorAttachment { id: number; vendorResponseId: number; attachmentType: string; documentNo: string | null; fileName: string; originalFileName: string; filePath: string; fileSize: number | null; fileType: string | null; description: string | null; validFrom: Date | null; validTo: Date | null; uploadedBy: number; uploadedAt: Date; uploadedByName: string | null; vendorId: number | null; vendorName: string | null; vendorCode: string | null; responseStatus: "작성중" | "제출완료" | "수정요청" | "최종확정" | "취소" | null; responseVersion: number | null; } interface VendorResponseTableProps { rfqId: number; initialData: VendorAttachment[]; } // 파일 타입별 아이콘 반환 const getFileIcon = (fileType: string | null) => { if (!fileType) return ; const type = fileType.toLowerCase(); if (type.includes('image') || ['jpg', 'jpeg', 'png', 'gif'].includes(type)) { return ; } if (type.includes('excel') || type.includes('spreadsheet') || ['xls', 'xlsx'].includes(type)) { return ; } if (type.includes('pdf')) { return ; } if (type.includes('code') || ['js', 'ts', 'tsx', 'jsx', 'html', 'css'].includes(type)) { return ; } return ; }; // 파일 크기 포맷팅 const formatFileSize = (bytes: number | null) => { if (!bytes) return "-"; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; }; // 응답 상태별 색상 const getStatusVariant = (status: string | null) => { switch (status) { case "작성중": return "outline"; case "제출완료": return "default"; case "수정요청": return "secondary"; case "최종확정": return "success"; case "취소": return "destructive"; default: return "outline"; } }; // 유효기간 체크 const checkValidity = (validTo: Date | null) => { if (!validTo) return null; const today = new Date(); const expiry = new Date(validTo); if (isBefore(expiry, today)) { return "expired"; } else if (isBefore(expiry, new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000))) { return "expiring-soon"; // 30일 이내 만료 } return "valid"; }; export function VendorResponseTable({ rfqId, initialData, }: VendorResponseTableProps) { const [data, setData] = React.useState(initialData); const [isRefreshing, setIsRefreshing] = React.useState(false); const [selectedRows, setSelectedRows] = React.useState([]); // 데이터 새로고침 const handleRefresh = React.useCallback(async () => { setIsRefreshing(true); try { const result = await getRfqVendorAttachments(rfqId); if (result.success && result.data) { setData(result.data); toast.success("데이터를 새로고침했습니다."); } else { toast.error("데이터를 불러오는데 실패했습니다."); } } catch (error) { console.error("Refresh error:", error); toast.error("새로고침 중 오류가 발생했습니다."); } finally { setIsRefreshing(false); } }, [rfqId]); // 액션 처리 const handleAction = React.useCallback(async (action: DataTableRowAction) => { const attachment = action.row.original; switch (action.type) { case "download": if (attachment.filePath && attachment.originalFileName) { await downloadFile(attachment.filePath, attachment.originalFileName, { action: 'download', showToast: true }); } break; case "preview": if (attachment.filePath && attachment.originalFileName) { await downloadFile(attachment.filePath, attachment.originalFileName, { action: 'preview', showToast: true }); } break; } }, []); // 선택된 항목 일괄 다운로드 const handleBulkDownload = React.useCallback(async () => { if (selectedRows.length === 0) { toast.warning("다운로드할 항목을 선택해주세요."); return; } for (const attachment of selectedRows) { if (attachment.filePath && attachment.originalFileName) { await downloadFile(attachment.filePath, attachment.originalFileName, { action: 'download', showToast: false }); } } toast.success(`${selectedRows.length}개 파일을 다운로드했습니다.`); }, [selectedRows]); // 컬럼 정의 const columns: ColumnDef[] = React.useMemo(() => [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!v)} aria-label="select all" className="translate-y-0.5" /> ), cell: ({ row }) => ( row.toggleSelected(!!v)} aria-label="select row" className="translate-y-0.5" /> ), size: 40, enableSorting: false, enableHiding: false, enablePinning: true, }, { accessorKey: "vendorName", header: ({ column }) => , cell: ({ row }) => { const vendor = row.original; return (
{vendor.vendorName || "-"} {vendor.vendorCode}
); }, size: 150, enablePinning: true, }, { accessorKey: "attachmentType", header: ({ column }) => , cell: ({ row }) => { const type = row.original.attachmentType; return ( {type} ); }, size: 100, }, { accessorKey: "documentNo", header: ({ column }) => , cell: ({ row }) => ( {row.original.documentNo || "-"} ), size: 120, }, { accessorKey: "originalFileName", header: ({ column }) => , cell: ({ row }) => { const file = row.original; return (
{getFileIcon(file.fileType)}
{file.originalFileName || file.fileName || "-"}
); }, size: 300, }, { accessorKey: "description", header: ({ column }) => , cell: ({ row }) => (
{row.original.description || "-"}
), size: 200, }, { accessorKey: "validTo", header: ({ column }) => , cell: ({ row }) => { const { validFrom, validTo } = row.original; const validity = checkValidity(validTo); if (!validTo) return -; return (
{validity === "expired" && ( )} {validity === "expiring-soon" && ( )} {format(new Date(validTo), "yyyy-MM-dd")}

유효기간: {validFrom ? format(new Date(validFrom), "yyyy-MM-dd") : "?"} ~ {format(new Date(validTo), "yyyy-MM-dd")}

{validity === "expired" &&

만료됨

} {validity === "expiring-soon" &&

곧 만료 예정

}
); }, size: 120, }, { accessorKey: "responseStatus", header: ({ column }) => , cell: ({ row }) => { const status = row.original.responseStatus; return status ? ( {status} ) : ( - ); }, size: 100, }, { accessorKey: "fileSize", header: ({ column }) => , cell: ({ row }) => ( {formatFileSize(row.original.fileSize)} ), size: 80, }, { accessorKey: "uploadedAt", header: ({ column }) => , cell: ({ row }) => { const date = row.original.uploadedAt; return date ? ( {format(new Date(date), "MM-dd HH:mm")}

{format(new Date(date), "yyyy년 MM월 dd일 HH시 mm분")}

({formatDistanceToNow(new Date(date), { addSuffix: true, locale: ko })})

) : ( "-" ); }, size: 100, }, { id: "actions", header: "작업", cell: ({ row }) => { return ( handleAction({ row, type: "download" })}> 다운로드 handleAction({ row, type: "preview" })}> 미리보기 ); }, size: 60, enablePinning: true, }, ], [handleAction]); const advancedFilterFields: DataTableAdvancedFilterField[] = [ { id: "vendorName", label: "벤더명", type: "text" }, { id: "vendorCode", label: "벤더코드", type: "text" }, { id: "attachmentType", label: "문서 유형", type: "select", options: [ { label: "견적서", value: "견적서" }, { label: "기술제안서", value: "기술제안서" }, { label: "인증서", value: "인증서" }, { label: "카탈로그", value: "카탈로그" }, { label: "도면", value: "도면" }, { label: "테스트성적서", value: "테스트성적서" }, { label: "기타", value: "기타" }, ] }, { id: "documentNo", label: "문서번호", type: "text" }, { id: "originalFileName", label: "파일명", type: "text" }, { id: "description", label: "설명", type: "text" }, { id: "responseStatus", label: "응답 상태", type: "select", options: [ { label: "작성중", value: "작성중" }, { label: "제출완료", value: "제출완료" }, { label: "수정요청", value: "수정요청" }, { label: "최종확정", value: "최종확정" }, { label: "취소", value: "취소" }, ] }, { id: "validFrom", label: "유효시작일", type: "date" }, { id: "validTo", label: "유효종료일", type: "date" }, { id: "uploadedAt", label: "업로드일", type: "date" }, ]; // 추가 액션 버튼들 const additionalActions = React.useMemo(() => (
{selectedRows.length > 0 && ( )}
), [selectedRows, isRefreshing, handleBulkDownload, handleRefresh]); // 벤더별 그룹 카운트 const vendorCounts = React.useMemo(() => { const counts = new Map(); data.forEach(item => { const vendor = item.vendorName || "Unknown"; counts.set(vendor, (counts.get(vendor) || 0) + 1); }); return counts; }, [data]); return (
{/* 벤더별 요약 정보 */}
{Array.from(vendorCounts.entries()).map(([vendor, count]) => ( {vendor}: {count} ))}
{additionalActions}
); }