diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-24 18:55:27 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-24 18:55:27 +0900 |
| commit | 39fc95095ac4b99186294f21fe6d8ac0cfab1f6e (patch) | |
| tree | 50f88724a3e4709316a4b0aa4a3519476d51d86f | |
| parent | 8b777900a2908efebd767493f77ee3ad489b49a7 (diff) | |
(김준회) 리비전, 파일 조회는 dialog 로 분리(옥프로 요청)
| -rw-r--r-- | lib/swp/table/swp-table-columns.tsx | 32 | ||||
| -rw-r--r-- | lib/swp/table/swp-table.tsx | 300 |
2 files changed, 220 insertions, 112 deletions
diff --git a/lib/swp/table/swp-table-columns.tsx b/lib/swp/table/swp-table-columns.tsx index f79a7441..b18e2b27 100644 --- a/lib/swp/table/swp-table-columns.tsx +++ b/lib/swp/table/swp-table-columns.tsx @@ -3,7 +3,7 @@ import { ColumnDef } from "@tanstack/react-table"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { ChevronDown, ChevronRight, FileIcon } from "lucide-react"; +import { ChevronDown, ChevronRight, FileIcon, Download } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; import { ko } from "date-fns/locale"; import type { SwpDocumentWithStats } from "../actions"; @@ -12,20 +12,16 @@ export const swpDocumentColumns: ColumnDef<SwpDocumentWithStats>[] = [ { id: "expander", header: () => null, - cell: ({ row }) => { - return row.getCanExpand() ? ( + cell: () => { + return ( <Button variant="ghost" size="sm" className="h-8 w-8 p-0" > - {row.getIsExpanded() ? ( - <ChevronDown className="h-4 w-4" /> - ) : ( - <ChevronRight className="h-4 w-4" /> - )} + <ChevronRight className="h-4 w-4" /> </Button> - ) : null; + ); }, size: 50, }, @@ -388,5 +384,23 @@ export const swpFileColumns: ColumnDef<FileRow>[] = [ ), size: 100, }, + { + id: "actions", + header: "작업", + cell: ({ row }) => ( + <Button + variant="outline" + size="sm" + onClick={() => { + // TODO: 파일 다운로드 로직 구현 + console.log("Download file:", row.original.FILE_NM); + }} + > + <Download className="h-4 w-4 mr-1" /> + 다운로드 + </Button> + ), + size: 120, + }, ]; diff --git a/lib/swp/table/swp-table.tsx b/lib/swp/table/swp-table.tsx index fb3a504e..8ae90bdd 100644 --- a/lib/swp/table/swp-table.tsx +++ b/lib/swp/table/swp-table.tsx @@ -17,6 +17,13 @@ import { 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"; @@ -43,6 +50,8 @@ export function SwpTable({ const [fileData, setFileData] = useState<Record<number, FileRow[]>>({}); const [loadingRevisions, setLoadingRevisions] = useState<Set<string>>(new Set()); const [loadingFiles, setLoadingFiles] = useState<Set<number>>(new Set()); + const [dialogOpen, setDialogOpen] = useState(false); + const [selectedDocument, setSelectedDocument] = useState<SwpDocumentWithStats | null>(null); const table = useReactTable({ data: initialData, @@ -104,10 +113,26 @@ export function SwpTable({ } }; - // 문서 행 확장 핸들러 - const handleDocumentExpand = (docNo: string, isExpanded: boolean) => { - if (isExpanded) { - loadRevisions(docNo); + // 문서 클릭 핸들러 - 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); + } } }; @@ -145,10 +170,8 @@ export function SwpTable({ <TableCell key={cell.id}> {cell.column.id === "expander" ? ( <div - onClick={() => { - row.toggleExpanded(); - handleDocumentExpand(row.original.DOC_NO, row.getIsExpanded()); - }} + onClick={() => handleDocumentClick(row.original)} + className="cursor-pointer" > {flexRender( cell.column.columnDef.cell, @@ -161,31 +184,6 @@ export function SwpTable({ </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> - )} </React.Fragment> )) ) : ( @@ -227,28 +225,95 @@ export function SwpTable({ </Button> </div> </div> + + {/* 문서 상세 Dialog */} + <Dialog open={dialogOpen} onOpenChange={setDialogOpen}> + <DialogContent className="max-w-6xl max-h-[90vh]"> + <DialogHeader> + <DialogTitle>문서 상세</DialogTitle> + {selectedDocument && ( + <DialogDescription> + {selectedDocument.DOC_NO} - {selectedDocument.DOC_TITLE} + </DialogDescription> + )} + </DialogHeader> + + {selectedDocument && ( + <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">{selectedDocument.PROJ_NO}</div> + {selectedDocument.PROJ_NM && ( + <div className="text-xs text-muted-foreground">{selectedDocument.PROJ_NM}</div> + )} + </div> + <div> + <span className="text-sm font-semibold">패키지:</span> + <div className="text-sm">{selectedDocument.PKG_NO || "-"}</div> + </div> + <div> + <span className="text-sm font-semibold">업체:</span> + <div className="text-sm">{selectedDocument.CPY_NM || "-"}</div> + {selectedDocument.VNDR_CD && ( + <div className="text-xs text-muted-foreground">{selectedDocument.VNDR_CD}</div> + )} + </div> + <div> + <span className="text-sm font-semibold">최신 리비전:</span> + <div className="text-sm">{selectedDocument.LTST_REV_NO || "-"}</div> + </div> + </div> + + {/* 리비전 및 파일 목록 */} + {loadingRevisions.has(selectedDocument.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[selectedDocument.DOC_NO]?.length ? ( + <DocumentDetailView + revisions={revisionData[selectedDocument.DOC_NO]} + fileData={fileData} + loadingFiles={loadingFiles} + onLoadFiles={loadFiles} + onLoadAllFiles={() => loadAllFiles(selectedDocument.DOC_NO)} + /> + ) : ( + <div className="p-8 text-center text-muted-foreground"> + 리비전 없음 + </div> + )} + </div> + )} + </DialogContent> + </Dialog> </div> ); } // ============================================================================ -// 리비전 서브 테이블 +// 문서 상세 뷰 (Dialog용) // ============================================================================ -interface RevisionSubTableProps { +interface DocumentDetailViewProps { revisions: RevisionRow[]; fileData: Record<number, FileRow[]>; loadingFiles: Set<number>; onLoadFiles: (revisionId: number) => void; + onLoadAllFiles: () => void; } -function RevisionSubTable({ +function DocumentDetailView({ revisions, fileData, loadingFiles, onLoadFiles, -}: RevisionSubTableProps) { + onLoadAllFiles, +}: DocumentDetailViewProps) { const [expandedRevisions, setExpandedRevisions] = useState<ExpandedState>({}); + const [allExpanded, setAllExpanded] = useState(false); const revisionTable = useReactTable({ data: revisions, @@ -262,80 +327,109 @@ function RevisionSubTable({ getRowCanExpand: () => true, }); - const handleRevisionExpand = (revisionId: number, isExpanded: boolean) => { - if (isExpanded) { - onLoadFiles(revisionId); + 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="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) => ( - <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(); - handleRevisionExpand(row.original.id, row.getIsExpanded()); - }} - > - {flexRender( - cell.column.columnDef.cell, - cell.getContext() + <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() )} - </div> - ) : ( - flexRender(cell.column.columnDef.cell, cell.getContext()) - )} - </TableCell> + </TableHead> ))} </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> + ))} + </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> - )} - </React.Fragment> - ))} - </TableBody> - </Table> + + {/* 파일 행들 (확장 시) */} + {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> ); } |
