summaryrefslogtreecommitdiff
path: root/lib/swp/table/swp-table.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-23 18:44:19 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-23 18:44:19 +0900
commit04bd1965c3699a4b29ed9c9627574bfeedd3d6c6 (patch)
tree691b9a6e844a788937a240d47e77e8cfa848a88a /lib/swp/table/swp-table.tsx
parent535e234dbd674bf2e5ecf344e03ed8ae5b2cbd6c (diff)
(김준회) SWP 문서 업로드 (Submisssion) 초기 개발건
Diffstat (limited to 'lib/swp/table/swp-table.tsx')
-rw-r--r--lib/swp/table/swp-table.tsx394
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>
+ );
+}