diff options
Diffstat (limited to 'lib/swp/table/swp-revision-list-dialog.tsx')
| -rw-r--r-- | lib/swp/table/swp-revision-list-dialog.tsx | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/lib/swp/table/swp-revision-list-dialog.tsx b/lib/swp/table/swp-revision-list-dialog.tsx new file mode 100644 index 00000000..74402bd9 --- /dev/null +++ b/lib/swp/table/swp-revision-list-dialog.tsx @@ -0,0 +1,310 @@ +"use client"; + +import React, { useState } from "react"; +import { + useReactTable, + getCoreRowModel, + getExpandedRowModel, + flexRender, + ExpandedState, +} from "@tanstack/react-table"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Button } from "@/components/ui/button"; +import { Loader2 } from "lucide-react"; +import { swpRevisionColumns, swpFileColumns, type RevisionRow, type FileRow } from "./swp-table-columns"; +import type { SwpDocumentWithStats } from "../actions"; + +interface SwpRevisionListDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + document: SwpDocumentWithStats | null; + revisions: RevisionRow[]; + fileData: Record<number, FileRow[]>; + loadingRevisions: boolean; + loadingFiles: Set<number>; + onLoadFiles: (revisionId: number) => void; + onLoadAllFiles: () => void; +} + +export function SwpRevisionListDialog({ + open, + onOpenChange, + document, + revisions, + fileData, + loadingRevisions, + loadingFiles, + onLoadFiles, + onLoadAllFiles, +}: SwpRevisionListDialogProps) { + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-6xl max-h-[90vh]"> + <DialogHeader> + <DialogTitle>문서 상세</DialogTitle> + {document && ( + <DialogDescription> + {document.DOC_NO} - {document.DOC_TITLE} + </DialogDescription> + )} + </DialogHeader> + + {document && ( + <div className="space-y-4 overflow-y-auto"> + {/* 문서 정보 */} + <div className="grid grid-cols-1 md:grid-cols-4 gap-4 p-4 bg-muted/30 rounded-lg"> + <div> + <span className="text-sm font-semibold">프로젝트:</span> + <div className="text-sm">{document.PROJ_NO}</div> + {document.PROJ_NM && ( + <div className="text-xs text-muted-foreground">{document.PROJ_NM}</div> + )} + </div> + <div> + <span className="text-sm font-semibold">패키지:</span> + <div className="text-sm">{document.PKG_NO || "-"}</div> + </div> + <div> + <span className="text-sm font-semibold">업체:</span> + <div className="text-sm">{document.CPY_NM || "-"}</div> + {document.VNDR_CD && ( + <div className="text-xs text-muted-foreground">{document.VNDR_CD}</div> + )} + </div> + <div> + <span className="text-sm font-semibold">최신 리비전:</span> + <div className="text-sm">{document.LTST_REV_NO || "-"}</div> + </div> + </div> + + {/* 리비전 및 파일 목록 */} + {loadingRevisions ? ( + <div className="flex items-center justify-center p-8"> + <Loader2 className="h-6 w-6 animate-spin" /> + <span className="ml-2">리비전 로딩 중...</span> + </div> + ) : revisions.length ? ( + <DocumentDetailView + revisions={revisions} + fileData={fileData} + loadingFiles={loadingFiles} + onLoadFiles={onLoadFiles} + onLoadAllFiles={onLoadAllFiles} + /> + ) : ( + <div className="p-8 text-center text-muted-foreground"> + 리비전 없음 + </div> + )} + </div> + )} + </DialogContent> + </Dialog> + ); +} + +// ============================================================================ +// 문서 상세 뷰 (Dialog용) +// ============================================================================ + +interface DocumentDetailViewProps { + revisions: RevisionRow[]; + fileData: Record<number, FileRow[]>; + loadingFiles: Set<number>; + onLoadFiles: (revisionId: number) => void; + onLoadAllFiles: () => void; +} + +function DocumentDetailView({ + revisions, + fileData, + loadingFiles, + onLoadFiles, + onLoadAllFiles, +}: DocumentDetailViewProps) { + const [expandedRevisions, setExpandedRevisions] = useState<ExpandedState>({}); + 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 ( + <div className="space-y-4"> + {/* 전체 펼치기/접기 버튼 */} + <div className="flex justify-end"> + <Button + variant="outline" + size="sm" + onClick={handleExpandAll} + > + {allExpanded ? "모두 접기" : "모두 펼치기"} + </Button> + </div> + + {/* 리비전 테이블 */} + <div className="rounded-md border"> + <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) => ( + <React.Fragment key={row.id}> + {/* 리비전 행 */} + <TableRow className="bg-muted/20"> + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {cell.column.id === "expander" ? ( + <div + onClick={() => { + row.toggleExpanded(); + if (!row.getIsExpanded()) { + handleRevisionExpand(row.original.id); + } + }} + className="cursor-pointer" + > + {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> + )} + </React.Fragment> + ))} + </TableBody> + </Table> + </div> + </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> + ); +} + |
