diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-23 18:44:19 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-23 18:44:19 +0900 |
| commit | 04bd1965c3699a4b29ed9c9627574bfeedd3d6c6 (patch) | |
| tree | 691b9a6e844a788937a240d47e77e8cfa848a88a /lib/swp/table/swp-table.tsx | |
| parent | 535e234dbd674bf2e5ecf344e03ed8ae5b2cbd6c (diff) | |
(김준회) SWP 문서 업로드 (Submisssion) 초기 개발건
Diffstat (limited to 'lib/swp/table/swp-table.tsx')
| -rw-r--r-- | lib/swp/table/swp-table.tsx | 394 |
1 files changed, 394 insertions, 0 deletions
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<ExpandedState>({}); + const [revisionData, setRevisionData] = useState<Record<string, RevisionRow[]>>({}); + const [fileData, setFileData] = useState<Record<number, FileRow[]>>({}); + const [loadingRevisions, setLoadingRevisions] = useState<Set<string>>(new Set()); + const [loadingFiles, setLoadingFiles] = useState<Set<number>>(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 ( + <div className="space-y-4"> + {/* 테이블 */} + <div className="rounded-md border"> + <Table> + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => ( + <TableHead key={header.id}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ))} + </TableRow> + ))} + </TableHeader> + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <> + {/* 문서 행 */} + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + className="hover:bg-muted/50" + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {cell.column.id === "expander" ? ( + <div + onClick={() => { + row.toggleExpanded(); + handleDocumentExpand(row.original.DOC_NO, row.getIsExpanded()); + }} + > + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </div> + ) : ( + flexRender(cell.column.columnDef.cell, cell.getContext()) + )} + </TableCell> + ))} + </TableRow> + + {/* 리비전 행들 (확장 시) */} + {row.getIsExpanded() && ( + <TableRow> + <TableCell colSpan={swpDocumentColumns.length} className="p-0 bg-muted/30"> + {loadingRevisions.has(row.original.DOC_NO) ? ( + <div className="flex items-center justify-center p-8"> + <Loader2 className="h-6 w-6 animate-spin" /> + <span className="ml-2">리비전 로딩 중...</span> + </div> + ) : revisionData[row.original.DOC_NO]?.length ? ( + <RevisionSubTable + revisions={revisionData[row.original.DOC_NO]} + fileData={fileData} + loadingFiles={loadingFiles} + onLoadFiles={loadFiles} + /> + ) : ( + <div className="p-8 text-center text-muted-foreground"> + 리비전 없음 + </div> + )} + </TableCell> + </TableRow> + )} + </> + )) + ) : ( + <TableRow> + <TableCell colSpan={swpDocumentColumns.length} className="h-24 text-center"> + 데이터 없음 + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {/* 페이지네이션 */} + <div className="flex items-center justify-between"> + <div className="text-sm text-muted-foreground"> + 총 {total}개 중 {(page - 1) * pageSize + 1}- + {Math.min(page * pageSize, total)}개 표시 + </div> + <div className="flex items-center space-x-2"> + <Button + variant="outline" + size="sm" + onClick={() => onPageChange(page - 1)} + disabled={page === 1} + > + 이전 + </Button> + <div className="text-sm"> + {page} / {totalPages} + </div> + <Button + variant="outline" + size="sm" + onClick={() => onPageChange(page + 1)} + disabled={page === totalPages} + > + 다음 + </Button> + </div> + </div> + </div> + ); +} + +// ============================================================================ +// 리비전 서브 테이블 +// ============================================================================ + +interface RevisionSubTableProps { + revisions: RevisionRow[]; + fileData: Record<number, FileRow[]>; + loadingFiles: Set<number>; + onLoadFiles: (revisionId: number) => void; +} + +function RevisionSubTable({ + revisions, + fileData, + loadingFiles, + onLoadFiles, +}: RevisionSubTableProps) { + const [expandedRevisions, setExpandedRevisions] = useState<ExpandedState>({}); + + 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 ( + <div className="border-l-4 border-blue-200"> + <Table> + <TableHeader> + {revisionTable.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id} className="bg-muted/50"> + {headerGroup.headers.map((header) => ( + <TableHead key={header.id} className="font-semibold"> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ))} + </TableRow> + ))} + </TableHeader> + <TableBody> + {revisionTable.getRowModel().rows.map((row) => ( + <> + {/* 리비전 행 */} + <TableRow key={row.id} className="bg-muted/20"> + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {cell.column.id === "expander" ? ( + <div + onClick={() => { + row.toggleExpanded(); + handleRevisionExpand(row.original.id, row.getIsExpanded()); + }} + > + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </div> + ) : ( + flexRender(cell.column.columnDef.cell, cell.getContext()) + )} + </TableCell> + ))} + </TableRow> + + {/* 파일 행들 (확장 시) */} + {row.getIsExpanded() && ( + <TableRow> + <TableCell colSpan={swpRevisionColumns.length} className="p-0 bg-blue-50/30"> + {loadingFiles.has(row.original.id) ? ( + <div className="flex items-center justify-center p-4"> + <Loader2 className="h-5 w-5 animate-spin" /> + <span className="ml-2 text-sm">파일 로딩 중...</span> + </div> + ) : fileData[row.original.id]?.length ? ( + <FileSubTable files={fileData[row.original.id]} /> + ) : ( + <div className="p-4 text-center text-sm text-muted-foreground"> + 파일 없음 + </div> + )} + </TableCell> + </TableRow> + )} + </> + ))} + </TableBody> + </Table> + </div> + ); +} + +// ============================================================================ +// 파일 서브 테이블 +// ============================================================================ + +interface FileSubTableProps { + files: FileRow[]; +} + +function FileSubTable({ files }: FileSubTableProps) { + const fileTable = useReactTable({ + data: files, + columns: swpFileColumns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( + <div className="border-l-4 border-green-200"> + <Table> + <TableHeader> + {fileTable.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id} className="bg-blue-50/50"> + {headerGroup.headers.map((header) => ( + <TableHead key={header.id} className="font-semibold text-xs"> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ))} + </TableRow> + ))} + </TableHeader> + <TableBody> + {fileTable.getRowModel().rows.map((row) => ( + <TableRow key={row.id} className="bg-green-50/20 hover:bg-green-50/40"> + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id} className="py-2"> + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </TableCell> + ))} + </TableRow> + ))} + </TableBody> + </Table> + </div> + ); +} |
