"use client"; import React, { useMemo, useState } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Download, XCircle } from "lucide-react"; import { cancelVendorUploadedFile } from "@/lib/swp/vendor-actions"; import type { SwpFileApiResponse } from "@/lib/swp/api-client"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { formatSwpDate } from "@/lib/swp/utils"; import { SwpInboxHistoryDialog } from "./swp-inbox-history-dialog"; // 업로드 필요 문서 타입 (DB stageDocuments에서 조회) interface RequiredDocument { vendorDocNumber: string; title: string; buyerSystemComment: string | null; } interface SwpInboxTableProps { files: SwpFileApiResponse[]; requiredDocs: RequiredDocument[]; projNo: string; vendorCode: string; userId: string; } // 테이블 행 데이터 (플랫하게 펼침) interface TableRowData { uploadId: string | null; // 업로드 필요 문서는 null docNo: string; revNo: string | null; stage: string | null; status: string | null; statusNm: string | null; actvNo: string | null; crter: string | null; // CRTER (그대로 표시) note: string | null; // Activity의 note 또는 buyerSystemComment file: SwpFileApiResponse | null; // 업로드 필요 문서는 null uploadDate: string | null; // 각 행이 속한 그룹의 정보 (계층적 rowSpan 처리) isFirstInUpload: boolean; fileCountInUpload: number; isFirstInDoc: boolean; fileCountInDoc: number; isFirstInRev: boolean; fileCountInRev: number; isFirstInActivity: boolean; fileCountInActivity: number; // 업로드 필요 문서 여부 isRequiredDoc: boolean; } // Status 집계 타입 interface StatusCount { status: string; statusNm: string; count: number; color: string; } export function SwpInboxTable({ files, requiredDocs, projNo, userId, }: SwpInboxTableProps) { const [selectedStatus, setSelectedStatus] = useState(null); const [selectedFiles, setSelectedFiles] = useState>(new Set()); // 선택된 파일 (fileKey) const [historyDialogOpen, setHistoryDialogOpen] = useState(false); const [selectedDocNo, setSelectedDocNo] = useState(null); // Status 집계 (API 응답 + 업로드 필요 문서) const statusCounts = useMemo(() => { const statusMap = new Map(); // API 응답 파일 집계 files.forEach((file) => { const status = file.STAT || "UNKNOWN"; const statusNm = file.STAT_NM || status; if (statusMap.has(status)) { statusMap.get(status)!.count++; } else { statusMap.set(status, { statusNm, count: 1 }); } }); // 업로드 필요 문서 집계 if (requiredDocs.length > 0) { const status = "UPLOAD_REQUIRED"; const statusNm = "Upload Required"; statusMap.set(status, { statusNm, count: requiredDocs.length }); } const counts: StatusCount[] = []; statusMap.forEach((value, status) => { const color = status === "SCW03" || status === "SCW08" ? "bg-green-100 text-green-800 hover:bg-green-200" : status === "SCW02" ? "bg-blue-100 text-blue-800 hover:bg-blue-200" : status === "SCW01" ? "bg-yellow-100 text-yellow-800 hover:bg-yellow-200" : status === "SCW04" || status === "SCW05" || status === "SCW06" ? "bg-red-100 text-red-800 hover:bg-red-200" : status === "SCW07" ? "bg-purple-100 text-purple-800 hover:bg-purple-200" : status === "SCW09" ? "bg-gray-100 text-gray-800 hover:bg-gray-200" : status === "SCW00" ? "bg-orange-100 text-orange-800 hover:bg-orange-200" : status === "UPLOAD_REQUIRED" ? "bg-amber-100 text-amber-800 hover:bg-amber-200" : "bg-gray-100 text-gray-800 hover:bg-gray-200"; counts.push({ status, statusNm: value.statusNm, count: value.count, color, }); }); // 개수 순으로 정렬 (Upload Required를 맨 앞으로) return counts.sort((a, b) => { if (a.status === "UPLOAD_REQUIRED") return -1; if (b.status === "UPLOAD_REQUIRED") return 1; return b.count - a.count; }); }, [files, requiredDocs]); // 데이터 그룹화 및 플랫 변환 (API 응답 + 업로드 필요 문서) const tableRows = useMemo(() => { const rows: TableRowData[] = []; // 1. API 응답 파일 처리 // Status 필터링 let filteredFiles = files; if (selectedStatus && selectedStatus !== "UPLOAD_REQUIRED") { filteredFiles = files.filter((file) => file.STAT === selectedStatus); } // 1단계: BOX_SEQ (Upload ID) 기준으로 그룹화 const uploadGroups = new Map(); if (!selectedStatus || selectedStatus !== "UPLOAD_REQUIRED") { filteredFiles.forEach((file) => { const uploadId = file.BOX_SEQ || "NO_UPLOAD_ID"; if (!uploadGroups.has(uploadId)) { uploadGroups.set(uploadId, []); } uploadGroups.get(uploadId)!.push(file); }); } // Upload ID별로 처리 uploadGroups.forEach((uploadFiles, uploadId) => { // 2단계: Document No 기준으로 그룹화 const docGroups = new Map(); uploadFiles.forEach((file) => { const docNo = file.OWN_DOC_NO; if (!docGroups.has(docNo)) { docGroups.set(docNo, []); } docGroups.get(docNo)!.push(file); }); // 전체 Upload ID의 파일 수 계산 const totalUploadFileCount = uploadFiles.length; let isFirstInUpload = true; // Document No별로 처리 docGroups.forEach((docFiles, docNo) => { // 3단계: 최신 RevNo 찾기 (CRTE_DTM 기준) const sortedByDate = [...docFiles].sort((a, b) => (b.CRTE_DTM || "").localeCompare(a.CRTE_DTM || "") ); const latestRevNo = sortedByDate[0]?.REV_NO || ""; const latestStage = sortedByDate[0]?.STAGE || null; const latestStatus = sortedByDate[0]?.STAT || null; const latestStatusNm = sortedByDate[0]?.STAT_NM || null; // 4단계: 최신 Rev의 파일들만 필터링 const latestRevFiles = docFiles.filter( (file) => file.REV_NO === latestRevNo ); const totalDocFileCount = latestRevFiles.length; let isFirstInDoc = true; // 5단계: Activity 기준으로 그룹화 const activityGroups = new Map(); latestRevFiles.forEach((file) => { const actvNo = file.ACTV_NO || "NO_ACTIVITY"; if (!activityGroups.has(actvNo)) { activityGroups.set(actvNo, []); } activityGroups.get(actvNo)!.push(file); }); let isFirstInRev = true; // Activity별로 처리 activityGroups.forEach((activityFiles, actvNo) => { // 6단계: Upload Date 기준 DESC 정렬 const sortedFiles = activityFiles.sort((a, b) => (b.CRTE_DTM || "").localeCompare(a.CRTE_DTM || "") ); const totalActivityFileCount = sortedFiles.length; // Activity의 첫 번째 파일에서 메타데이터 가져오기 const firstActivityFile = sortedFiles[0]; if (!firstActivityFile) return; // 7단계: 각 파일을 테이블 행으로 변환 sortedFiles.forEach((file, idx) => { rows.push({ uploadId, docNo, revNo: latestRevNo, stage: latestStage, status: latestStatus, statusNm: latestStatusNm, actvNo: actvNo === "NO_ACTIVITY" ? null : actvNo, crter: firstActivityFile.CRTER, // Activity 첫 파일의 CRTER note: firstActivityFile.NOTE || null, // Activity 첫 파일의 note file, uploadDate: file.CRTE_DTM, isFirstInUpload, fileCountInUpload: totalUploadFileCount, isFirstInDoc, fileCountInDoc: totalDocFileCount, isFirstInRev, fileCountInRev: totalDocFileCount, // Rev = Doc의 최신 Rev이므로 파일 수 동일 isFirstInActivity: idx === 0, fileCountInActivity: totalActivityFileCount, isRequiredDoc: false, }); // 첫 번째 플래그들 업데이트 if (idx === 0) { isFirstInUpload = false; isFirstInDoc = false; isFirstInRev = false; } }); }); }); }); // 2. 업로드 필요 문서 추가 (Upload Required 필터일 때만 또는 필터 없을 때) if (!selectedStatus || selectedStatus === "UPLOAD_REQUIRED") { requiredDocs.forEach((doc) => { rows.push({ uploadId: null, docNo: doc.vendorDocNumber, revNo: null, stage: null, status: "UPLOAD_REQUIRED", statusNm: "Upload Required", actvNo: null, crter: null, note: doc.buyerSystemComment, file: null, uploadDate: null, isFirstInUpload: true, fileCountInUpload: 1, isFirstInDoc: true, fileCountInDoc: 1, isFirstInRev: true, fileCountInRev: 1, isFirstInActivity: true, fileCountInActivity: 1, isRequiredDoc: true, }); }); } // Upload Date 기준 전체 정렬 (null은 맨 뒤로) return rows.sort((a, b) => { if (!a.uploadDate) return 1; if (!b.uploadDate) return -1; return b.uploadDate.localeCompare(a.uploadDate); }); }, [files, requiredDocs, selectedStatus]); // 선택 가능한 파일들 (Standby 상태만) const selectableFiles = useMemo(() => { return tableRows .filter((row) => row.file && row.status === "SCW01") .map((row) => `${row.file!.BOX_SEQ}_${row.file!.FILE_SEQ}`); }, [tableRows]); // 전체 선택/해제 const handleSelectAll = (checked: boolean) => { if (checked) { setSelectedFiles(new Set(selectableFiles)); } else { setSelectedFiles(new Set()); } }; // 개별 선택/해제 const handleSelectFile = (fileKey: string, checked: boolean | "indeterminate") => { const isChecked = checked === true; setSelectedFiles((prev) => { const newSet = new Set(prev); if (isChecked) { newSet.add(fileKey); } else { newSet.delete(fileKey); } return newSet; }); }; // 선택된 파일 일괄 취소 const handleBulkCancel = async () => { if (selectedFiles.size === 0) { toast.error("취소할 파일을 선택해주세요"); return; } const filesToCancel = tableRows.filter((row) => row.file && selectedFiles.has(`${row.file.BOX_SEQ}_${row.file.FILE_SEQ}`) ); if (filesToCancel.length === 0) { toast.error("취소할 파일이 없습니다"); return; } try { toast.info(`${filesToCancel.length}개 파일 취소 중...`); // 병렬 취소 const cancelPromises = filesToCancel.map((row) => cancelVendorUploadedFile({ boxSeq: row.file!.BOX_SEQ!, actvSeq: row.file!.ACTV_SEQ!, userId, }) ); await Promise.all(cancelPromises); toast.success(`${filesToCancel.length}개 파일 취소 완료`); setSelectedFiles(new Set()); // 선택 초기화 // 페이지 리프레시 window.location.reload(); } catch (error) { console.error("일괄 취소 실패:", error); toast.error("일부 파일 취소에 실패했습니다"); } }; const handleDownloadFile = async (file: SwpFileApiResponse) => { try { toast.info("파일 다운로드 준비 중..."); // API route를 통해 다운로드 const downloadUrl = `/api/swp/download/${encodeURIComponent(file.OWN_DOC_NO)}?projNo=${encodeURIComponent(projNo)}&fileName=${encodeURIComponent(file.FILE_NM)}`; // 새 탭에서 다운로드 window.open(downloadUrl, "_blank"); toast.success(`파일 다운로드 시작: ${file.FILE_NM}`); } catch (error) { console.error("파일 다운로드 실패:", error); toast.error("파일 다운로드에 실패했습니다"); } }; // 행 클릭 핸들러 (Document No 기준 전체 이력 보기) const handleRowClick = (docNo: string) => { setSelectedDocNo(docNo); setHistoryDialogOpen(true); }; const getStatusBadge = (status: string | null, statusNm: string | null) => { const displayStatus = statusNm || status || "-"; if (!status) return {displayStatus}; const color = status === "SCW03" || status === "SCW08" ? "bg-green-100 text-green-800" : status === "SCW02" ? "bg-blue-100 text-blue-800" : status === "SCW01" ? "bg-yellow-100 text-yellow-800" : status === "SCW04" || status === "SCW05" || status === "SCW06" ? "bg-red-100 text-red-800" : status === "SCW07" ? "bg-purple-100 text-purple-800" : status === "SCW09" ? "bg-gray-100 text-gray-800" : status === "SCW00" ? "bg-orange-100 text-orange-800" : "bg-gray-100 text-gray-800"; return ( {displayStatus} ); }; if (files.length === 0 && requiredDocs.length === 0) { return (
업로드한 파일이 없습니다.
); } return (
{/* Status 필터 UI */}
{statusCounts.map((statusCount) => ( ))}
{/* 선택된 파일 정보 및 일괄 취소 버튼 */} {selectedFiles.size > 0 && (
{selectedFiles.size}개 선택됨
)}
{/* 테이블 */} {tableRows.length === 0 ? (
해당 상태의 파일이 없습니다.
) : (
0 && selectedFiles.size === selectableFiles.length} onCheckedChange={handleSelectAll} disabled={selectableFiles.length === 0} /> Upload ID Document No Rev No Stage Status Activity Upload ID (User) Note Attachment File Upload Date {tableRows.map((row, idx) => { const fileKey = row.file ? `${row.file.BOX_SEQ}_${row.file.FILE_SEQ}` : `required_${idx}`; const canSelect = row.status === "SCW01" && row.file; // Standby이고 file이 있는 경우만 선택 가능 const isSelected = canSelect && selectedFiles.has(fileKey); return ( handleRowClick(row.docNo)} > {/* Select Checkbox */} e.stopPropagation()}> {canSelect ? ( handleSelectFile(fileKey, checked)} /> ) : null} {/* Upload ID - Upload의 첫 파일에만 표시 */} {row.isFirstInUpload ? ( {row.uploadId || -} ) : null} {/* Document No - Document의 첫 파일에만 표시 */} {row.isFirstInDoc ? ( {row.docNo} ) : null} {/* Rev No - Rev의 첫 파일에만 표시 */} {row.isFirstInRev ? ( {row.revNo || -} ) : null} {/* Stage - Rev의 첫 파일에만 표시 */} {row.isFirstInRev ? ( {row.stage || -} ) : null} {/* Status - Rev의 첫 파일에만 표시 */} {row.isFirstInRev ? ( {getStatusBadge(row.status, row.statusNm)} ) : null} {/* Activity - Activity의 첫 파일에만 표시 */} {row.isFirstInActivity ? ( {row.actvNo || -} ) : null} {/* CRTER (Upload ID User) - Activity의 첫 파일에만 표시 */} {row.isFirstInActivity ? ( {row.crter || -} ) : null} {/* Note - Activity의 첫 파일에만 표시 (개행문자 처리) */} {row.isFirstInActivity ? ( {row.note || -} ) : null} {/* Attachment File - 각 파일마다 표시 (줄바꿈 허용) */} {row.file ? (
{row.file.FILE_NM}
) : ( - )}
{/* Upload Date - 각 파일마다 표시 */} {row.uploadDate ? formatSwpDate(row.uploadDate) : -}
); })}
)} {/* Document 전체 이력 Dialog */}
); }