diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-27 10:54:48 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-27 10:54:48 +0900 |
| commit | 92766c1f2096852e7f224629963a412af8a16586 (patch) | |
| tree | e79600d6df56fce1a84fe8a76d69339e5d1a85c4 /lib/swp/table | |
| parent | 65a68325658401dd8a90ea900c1542c17c63d7ce (diff) | |
(김준회) SWP 코드추출 리팩터링, 파일수 조회 쿼리 통일, 헬프다이얼로그 문구 개선
Diffstat (limited to 'lib/swp/table')
| -rw-r--r-- | lib/swp/table/swp-help-dialog.tsx | 10 | ||||
| -rw-r--r-- | lib/swp/table/swp-revision-list-dialog.tsx | 310 | ||||
| -rw-r--r-- | lib/swp/table/swp-table-toolbar.tsx | 4 | ||||
| -rw-r--r-- | lib/swp/table/swp-table.tsx | 275 |
4 files changed, 328 insertions, 271 deletions
diff --git a/lib/swp/table/swp-help-dialog.tsx b/lib/swp/table/swp-help-dialog.tsx index 18f29644..6880a8c7 100644 --- a/lib/swp/table/swp-help-dialog.tsx +++ b/lib/swp/table/swp-help-dialog.tsx @@ -32,7 +32,7 @@ export function SwpUploadHelpDialog() { <div className="space-y-6"> {/* 파일명 형식 */} <div className="space-y-2"> - <h3 className="text-sm font-semibold">📋 파일명 형식</h3> + <h3 className="text-sm font-semibold">파일명 형식</h3> <div className="rounded-lg bg-muted p-4 font-mono text-sm"> [OWN_DOC_NO]_[REV_NO]_[STAGE]_[YYYYMMDDhhmmss].[확장자] </div> @@ -43,7 +43,7 @@ export function SwpUploadHelpDialog() { {/* 각 항목 설명 - 1라인 형태 */} <div className="space-y-3"> - <h3 className="text-sm font-semibold">📝 항목 설명</h3> + <h3 className="text-sm font-semibold">항목 설명</h3> <div className="flex items-center gap-3 rounded-lg border p-3"> <Badge variant="secondary" className="font-mono shrink-0"> @@ -71,7 +71,7 @@ export function SwpUploadHelpDialog() { </Badge> <div className="text-sm"> <span className="font-medium">스테이지</span> - <span className="text-muted-foreground"> - 중공업이 설정한 스테이지입니다 (예: IFA, IFC, AFC, BFC)</span> + <span className="text-muted-foreground"> - 스테이지 정보를 입력해주세요. (예: IFA, IFC)</span> </div> </div> @@ -88,7 +88,7 @@ export function SwpUploadHelpDialog() { {/* 예시 */} <div className="space-y-2"> - <h3 className="text-sm font-semibold">✅ 올바른 예시</h3> + <h3 className="text-sm font-semibold">올바른 예시</h3> <div className="space-y-2"> <div className="rounded-lg bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800 p-3"> <code className="text-xs font-mono text-green-700 dark:text-green-300"> @@ -105,7 +105,7 @@ export function SwpUploadHelpDialog() { {/* 잘못된 예시 */} <div className="space-y-2"> - <h3 className="text-sm font-semibold">❌ 잘못된 예시</h3> + <h3 className="text-sm font-semibold">잘못된 예시</h3> <div className="space-y-2"> <div className="rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 p-3"> <code className="text-xs font-mono text-red-700 dark:text-red-300"> 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> + ); +} + diff --git a/lib/swp/table/swp-table-toolbar.tsx b/lib/swp/table/swp-table-toolbar.tsx index fc8337f5..e7a2ef30 100644 --- a/lib/swp/table/swp-table-toolbar.tsx +++ b/lib/swp/table/swp-table-toolbar.tsx @@ -227,10 +227,6 @@ export function SwpTableToolbar({ {isSyncing ? "동기화 중..." : "SWP 동기화"} </Button> </div> - - <div className="text-sm text-muted-foreground"> - SWP 문서 관리 시스템 - </div> <div className="flex items-center gap-2"> {/* 벤더만 파일 업로드 기능 사용 가능 */} diff --git a/lib/swp/table/swp-table.tsx b/lib/swp/table/swp-table.tsx index 8ae90bdd..47c9905a 100644 --- a/lib/swp/table/swp-table.tsx +++ b/lib/swp/table/swp-table.tsx @@ -17,16 +17,9 @@ 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 { swpDocumentColumns, type RevisionRow, type FileRow } from "./swp-table-columns"; import { fetchDocumentRevisions, fetchRevisionFiles, type SwpDocumentWithStats } from "../actions"; +import { SwpRevisionListDialog } from "./swp-revision-list-dialog"; interface SwpTableProps { initialData: SwpDocumentWithStats[]; @@ -227,259 +220,17 @@ export function SwpTable({ </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 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> + <SwpRevisionListDialog + open={dialogOpen} + onOpenChange={setDialogOpen} + document={selectedDocument} + revisions={selectedDocument ? revisionData[selectedDocument.DOC_NO] || [] : []} + fileData={fileData} + loadingRevisions={selectedDocument ? loadingRevisions.has(selectedDocument.DOC_NO) : false} + loadingFiles={loadingFiles} + onLoadFiles={loadFiles} + onLoadAllFiles={() => selectedDocument && loadAllFiles(selectedDocument.DOC_NO)} + /> </div> ); } |
