From 04bd1965c3699a4b29ed9c9627574bfeedd3d6c6 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Thu, 23 Oct 2025 18:44:19 +0900 Subject: (김준회) SWP 문서 업로드 (Submisssion) 초기 개발건 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/swp/table/swp-table.tsx | 394 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 lib/swp/table/swp-table.tsx (limited to 'lib/swp/table/swp-table.tsx') diff --git a/lib/swp/table/swp-table.tsx b/lib/swp/table/swp-table.tsx new file mode 100644 index 00000000..4024c711 --- /dev/null +++ b/lib/swp/table/swp-table.tsx @@ -0,0 +1,394 @@ +"use client"; + +import { 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 { 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; + mode?: "admin" | "vendor"; +} + +export function SwpTable({ + initialData, + total, + page, + pageSize, + totalPages, + onPageChange, + mode = "admin", +}: 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 table = useReactTable({ + data: initialData, + columns: swpDocumentColumns, + state: { + expanded, + }, + onExpandedChange: setExpanded, + getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: getExpandedRowModel(), + getRowCanExpand: (row) => 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; + }); + } + }; + + // 문서 행 확장 핸들러 + const handleDocumentExpand = (docNo: string, isExpanded: boolean) => { + if (!isExpanded) { + loadRevisions(docNo); + } + }; + + 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" ? ( +
{ + row.toggleExpanded(); + handleDocumentExpand(row.original.DOC_NO, row.getIsExpanded()); + }} + > + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+ ) : ( + flexRender(cell.column.columnDef.cell, cell.getContext()) + )} +
+ ))} +
+ + {/* 리비전 행들 (확장 시) */} + {row.getIsExpanded() && ( + + + {loadingRevisions.has(row.original.DOC_NO) ? ( +
+ + 리비전 로딩 중... +
+ ) : revisionData[row.original.DOC_NO]?.length ? ( + + ) : ( +
+ 리비전 없음 +
+ )} +
+
+ )} + + )) + ) : ( + + + 데이터 없음 + + + )} +
+
+
+ + {/* 페이지네이션 */} +
+
+ 총 {total}개 중 {(page - 1) * pageSize + 1}- + {Math.min(page * pageSize, total)}개 표시 +
+
+ +
+ {page} / {totalPages} +
+ +
+
+
+ ); +} + +// ============================================================================ +// 리비전 서브 테이블 +// ============================================================================ + +interface RevisionSubTableProps { + revisions: RevisionRow[]; + fileData: Record; + loadingFiles: Set; + onLoadFiles: (revisionId: number) => void; +} + +function RevisionSubTable({ + revisions, + fileData, + loadingFiles, + onLoadFiles, +}: RevisionSubTableProps) { + const [expandedRevisions, setExpandedRevisions] = useState({}); + + const revisionTable = useReactTable({ + data: revisions, + columns: swpRevisionColumns, + state: { + expanded: expandedRevisions, + }, + onExpandedChange: setExpandedRevisions, + getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: getExpandedRowModel(), + getRowCanExpand: () => true, + }); + + const handleRevisionExpand = (revisionId: number, isExpanded: boolean) => { + if (!isExpanded) { + 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(); + handleRevisionExpand(row.original.id, row.getIsExpanded()); + }} + > + {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())} + + ))} + + ))} + +
+
+ ); +} -- cgit v1.2.3