// @/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, X } 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"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; // 타입 정의 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 [isUpdating, setIsUpdating] = React.useState(false); const [showTypeDialog, setShowTypeDialog] = React.useState(false); const [selectedType, setSelectedType] = React.useState<"구매" | "설계" | "">(""); console.log(data,"data") const [selectedVendor, setSelectedVendor] = React.useState(null); const filteredData = React.useMemo(() => { if (!selectedVendor) return data; return data.filter(item => item.vendorName === selectedVendor); }, [data, selectedVendor]); // 데이터 새로고침 const handleRefresh = React.useCallback(async () => { setIsRefreshing(true); try { const result = await getRfqVendorAttachments(rfqId); if (result.vendorSuccess && result.vendorData) { setData(result.vendorData); toast.success("데이터를 새로고침했습니다."); } else { toast.error("데이터를 불러오는데 실패했습니다."); } } catch (error) { console.error("Refresh error:", error); toast.error("새로고침 중 오류가 발생했습니다."); } finally { setIsRefreshing(false); } }, [rfqId]); const toggleVendorFilter = (vendor: string) => { if (selectedVendor === vendor) { setSelectedVendor(null); // 이미 선택된 벤더를 다시 클릭하면 필터 해제 } else { setSelectedVendor(vendor); // 필터 변경 시 선택 초기화 (옵션) setSelectedRows([]); } }; // 문서 유형 일괄 변경 const handleBulkTypeChange = React.useCallback(async () => { if (!selectedType || selectedRows.length === 0) return; setIsUpdating(true); try { const ids = selectedRows.map(row => row.id); const result = await updateAttachmentTypes(ids, selectedType as "구매" | "설계"); if (result.success) { toast.success(result.message); // 데이터 새로고침 await handleRefresh(); // 선택 초기화 setSelectedRows([]); setShowTypeDialog(false); setSelectedType(""); } else { toast.error(result.message); } } catch (error) { toast.error("문서 유형 변경 중 오류가 발생했습니다."); } finally { setIsUpdating(false); } }, [selectedType, selectedRows, handleRefresh]); // 액션 처리 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 (
{/* 벤더 필터 섹션 */}
{/* 필터 헤더 */}
벤더별 필터 {selectedVendor && ( )}
{/* 벤더 버튼들 */}
{/* 전체 보기 버튼 */} {/* 각 벤더별 버튼 */} {Array.from(vendorCounts.entries()).map(([vendor, count]) => ( ))}
{/* 현재 필터 상태 표시 */} {selectedVendor && (
"{selectedVendor}" 벤더의 {filteredData.length}개 항목만 표시 중
)}
{additionalActions} {/* 문서 유형 변경 다이얼로그 */} 문서 유형 변경 선택한 {selectedRows.length}개 항목의 문서 유형을 변경합니다.
{/* 현재 선택된 항목들의 정보 표시 */}

변경될 항목:

    {selectedRows.slice(0, 5).map((row) => (
  • • {row.vendorName} - {row.originalFileName}
  • ))} {selectedRows.length > 5 && (
  • ... 외 {selectedRows.length - 5}개
  • )}
); }