"use client"; import React, { useState } from "react"; import { useReactTable, getCoreRowModel, getExpandedRowModel, flexRender, ExpandedState, } from "@tanstack/react-table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Loader2 } from "lucide-react"; import { swpDocumentColumns, swpRevisionColumns, swpFileColumns, type RevisionRow, type FileRow } from "./swp-table-columns"; import { fetchDocumentRevisions, fetchRevisionFiles, type SwpDocumentWithStats } from "../actions"; interface SwpTableProps { initialData: SwpDocumentWithStats[]; total: number; page: number; pageSize: number; totalPages: number; onPageChange: (page: number) => void; } export function SwpTable({ initialData, total, page, pageSize, totalPages, onPageChange, }: SwpTableProps) { const [expanded, setExpanded] = useState({}); const [revisionData, setRevisionData] = useState>({}); const [fileData, setFileData] = useState>({}); const [loadingRevisions, setLoadingRevisions] = useState>(new Set()); const [loadingFiles, setLoadingFiles] = useState>(new Set()); const [dialogOpen, setDialogOpen] = useState(false); const [selectedDocument, setSelectedDocument] = useState(null); const table = useReactTable({ data: initialData, columns: swpDocumentColumns, state: { expanded, }, onExpandedChange: setExpanded, getCoreRowModel: getCoreRowModel(), getExpandedRowModel: getExpandedRowModel(), getRowCanExpand: () => true, // 모든 문서는 확장 가능 }); // 리비전 로드 const loadRevisions = async (docNo: string) => { if (revisionData[docNo]) return; // 이미 로드됨 setLoadingRevisions((prev) => { const newSet = new Set(prev); newSet.add(docNo); return newSet; }); try { const revisions = await fetchDocumentRevisions(docNo); setRevisionData((prev) => ({ ...prev, [docNo]: revisions })); } catch (error) { console.error("리비전 로드 실패:", error); } finally { setLoadingRevisions((prev) => { const next = new Set(prev); next.delete(docNo); return next; }); } }; // 파일 로드 const loadFiles = async (revisionId: number) => { if (fileData[revisionId]) return; // 이미 로드됨 setLoadingFiles((prev) => { const newSet = new Set(prev); newSet.add(revisionId); return newSet; }); try { const files = await fetchRevisionFiles(revisionId); setFileData((prev) => ({ ...prev, [revisionId]: files })); } catch (error) { console.error("파일 로드 실패:", error); } finally { setLoadingFiles((prev) => { const next = new Set(prev); next.delete(revisionId); return next; }); } }; // 문서 클릭 핸들러 - Dialog 열기 const handleDocumentClick = async (document: SwpDocumentWithStats) => { setSelectedDocument(document); setDialogOpen(true); // 리비전 데이터 로드 if (!revisionData[document.DOC_NO]) { await loadRevisions(document.DOC_NO); } }; // 모든 리비전의 파일을 로드 const loadAllFiles = async (docNo: string) => { const revisions = revisionData[docNo]; if (!revisions) return; for (const revision of revisions) { if (!fileData[revision.id]) { await loadFiles(revision.id); } } }; return (
{/* 테이블 */}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} ))} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {/* 문서 행 */} {row.getVisibleCells().map((cell) => ( {cell.column.id === "expander" ? (
handleDocumentClick(row.original)} className="cursor-pointer" > {flexRender( cell.column.columnDef.cell, cell.getContext() )}
) : ( flexRender(cell.column.columnDef.cell, cell.getContext()) )}
))}
)) ) : ( 데이터 없음 )}
{/* 페이지네이션 */}
총 {total}개 중 {(page - 1) * pageSize + 1}- {Math.min(page * pageSize, total)}개 표시
{page} / {totalPages}
{/* 문서 상세 Dialog */} 문서 상세 {selectedDocument && ( {selectedDocument.DOC_NO} - {selectedDocument.DOC_TITLE} )} {selectedDocument && (
{/* 문서 정보 */}
프로젝트:
{selectedDocument.PROJ_NO}
{selectedDocument.PROJ_NM && (
{selectedDocument.PROJ_NM}
)}
패키지:
{selectedDocument.PKG_NO || "-"}
업체:
{selectedDocument.CPY_NM || "-"}
{selectedDocument.VNDR_CD && (
{selectedDocument.VNDR_CD}
)}
최신 리비전:
{selectedDocument.LTST_REV_NO || "-"}
{/* 리비전 및 파일 목록 */} {loadingRevisions.has(selectedDocument.DOC_NO) ? (
리비전 로딩 중...
) : revisionData[selectedDocument.DOC_NO]?.length ? ( loadAllFiles(selectedDocument.DOC_NO)} /> ) : (
리비전 없음
)}
)}
); } // ============================================================================ // 문서 상세 뷰 (Dialog용) // ============================================================================ interface DocumentDetailViewProps { revisions: RevisionRow[]; fileData: Record; loadingFiles: Set; onLoadFiles: (revisionId: number) => void; onLoadAllFiles: () => void; } function DocumentDetailView({ revisions, fileData, loadingFiles, onLoadFiles, onLoadAllFiles, }: DocumentDetailViewProps) { const [expandedRevisions, setExpandedRevisions] = useState({}); const [allExpanded, setAllExpanded] = useState(false); const revisionTable = useReactTable({ data: revisions, columns: swpRevisionColumns, state: { expanded: expandedRevisions, }, onExpandedChange: setExpandedRevisions, getCoreRowModel: getCoreRowModel(), getExpandedRowModel: getExpandedRowModel(), getRowCanExpand: () => true, }); const handleExpandAll = () => { if (allExpanded) { setExpandedRevisions({}); } else { const expanded: ExpandedState = {}; revisions.forEach((_, index) => { expanded[index] = true; }); setExpandedRevisions(expanded); onLoadAllFiles(); } setAllExpanded(!allExpanded); }; const handleRevisionExpand = (revisionId: number) => { onLoadFiles(revisionId); }; return (
{/* 전체 펼치기/접기 버튼 */}
{/* 리비전 테이블 */}
{revisionTable.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} ))} ))} {revisionTable.getRowModel().rows.map((row) => ( {/* 리비전 행 */} {row.getVisibleCells().map((cell) => ( {cell.column.id === "expander" ? (
{ row.toggleExpanded(); if (!row.getIsExpanded()) { handleRevisionExpand(row.original.id); } }} className="cursor-pointer" > {flexRender( cell.column.columnDef.cell, cell.getContext() )}
) : ( flexRender(cell.column.columnDef.cell, cell.getContext()) )}
))}
{/* 파일 행들 (확장 시) */} {row.getIsExpanded() && ( {loadingFiles.has(row.original.id) ? (
파일 로딩 중...
) : fileData[row.original.id]?.length ? ( ) : (
파일 없음
)}
)}
))}
); } // ============================================================================ // 파일 서브 테이블 // ============================================================================ interface FileSubTableProps { files: FileRow[]; } function FileSubTable({ files }: FileSubTableProps) { const fileTable = useReactTable({ data: files, columns: swpFileColumns, getCoreRowModel: getCoreRowModel(), }); return (
{fileTable.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} ))} ))} {fileTable.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ))}
); }