diff options
Diffstat (limited to 'lib/dolce/table')
| -rw-r--r-- | lib/dolce/table/detail-drawing-columns.tsx | 96 | ||||
| -rw-r--r-- | lib/dolce/table/drawing-list-columns.tsx | 7 | ||||
| -rw-r--r-- | lib/dolce/table/drawing-list-table-v2.tsx | 273 |
3 files changed, 289 insertions, 87 deletions
diff --git a/lib/dolce/table/detail-drawing-columns.tsx b/lib/dolce/table/detail-drawing-columns.tsx index 77d25953..c082333d 100644 --- a/lib/dolce/table/detail-drawing-columns.tsx +++ b/lib/dolce/table/detail-drawing-columns.tsx @@ -8,8 +8,8 @@ import { formatDolceDateTime } from "../utils/date-formatter"; const DRAWING_USAGE_MAP: Record<string, { ko: string; en: string }> = { APP: { ko: "승인용", en: "Approval" }, WOR: { ko: "작업용", en: "Working" }, - REC: { ko: "입수용 / GTT→SHI", en: "GTT→SHI" }, - SUB: { ko: "제출용 / SHI→GTT", en: "SHI→GTT" }, + REC: { ko: "입수용", en: "GTT→SHI" }, + SUB: { ko: "제출용", en: "SHI→GTT" }, }; const REGISTER_KIND_MAP: Record<string, { ko: string; en: string }> = { @@ -36,82 +36,6 @@ const translateRegisterKind = (code: string | null, lng: string): string => { return lng === "en" ? mapped.en : mapped.ko; }; -// 기본 컬럼 (기존 호환성 유지) -export const detailDrawingColumns: ColumnDef<DetailDwgReceiptItem>[] = [ - { - accessorKey: "RegisterSerialNo", - header: "일련번호", - minSize: 80, - cell: ({ row }) => { - return <div className="text-center">{row.getValue("RegisterSerialNo")}</div>; - }, - }, - { - accessorKey: "DrawingRevNo", - header: "Revision", - minSize: 100, - cell: ({ row }) => { - return <div className="font-medium">{row.getValue("DrawingRevNo")}</div>; - }, - }, - { - accessorKey: "Status", - header: "상태", - minSize: 120, - cell: ({ row }) => { - return <div className="text-center">{row.getValue("Status")}</div>; - }, - }, - { - accessorKey: "CategoryENM", - header: "카테고리", - minSize: 120, - cell: ({ row }) => { - const categoryENM = row.getValue("CategoryENM") as string; - const categoryNM = row.original.CategoryNM; - return <div>{categoryENM || categoryNM}</div>; - }, - }, - { - accessorKey: "DrawingUsageENM", - header: "도면용도", - minSize: 100, - cell: ({ row }) => { - const usageENM = row.getValue("DrawingUsageENM") as string | null; - const usageNM = row.original.DrawingUsageNM; - return <div>{usageENM || usageNM}</div>; - }, - }, - { - accessorKey: "RegisterKindENM", - header: "등록종류", - minSize: 180, - cell: ({ row }) => { - const kindENM = row.getValue("RegisterKindENM") as string | null; - const kindNM = row.original.RegisterKindNM; - return <div>{kindENM || kindNM}</div>; - }, - }, - { - accessorKey: "CreateUserNM", - header: "생성자", - minSize: 150, - cell: ({ row }) => { - const userENM = row.original.CreateUserENM; - const userNM = row.getValue("CreateUserNM") as string; - return <div>{userENM || userNM}</div>; - }, - }, - { - accessorKey: "CreateDt", - header: "생성일시", - minSize: 200, - cell: ({ row }) => { - return <div className="text-sm text-muted-foreground">{row.getValue("CreateDt")}</div>; - }, - }, -]; - // 다국어 지원 컬럼 생성 함수 export function createDetailDrawingColumns( lng: string, @@ -155,25 +79,25 @@ export function createDetailDrawingColumns( }, }, { - accessorKey: "DrawingUsageENM", + accessorKey: "DrawingUsage", header: t("detailDrawing.columns.drawingUsage"), minSize: 100, cell: ({ row }) => { - // API의 ENM이 제대로 번역되지 않아 코드 값으로 직접 변환 - const usageCode = row.getValue("DrawingUsageENM") as string | null; + // API 응답의 DrawingUsage는 코드값이므로 직접 매핑하여 번역 + const usageCode = row.getValue("DrawingUsage") as string; const translated = translateDrawingUsage(usageCode, lng); - return <div>{translated || usageCode || row.original.DrawingUsageNM}</div>; + return <div>{translated}</div>; }, }, { - accessorKey: "RegisterKindENM", + accessorKey: "RegisterKind", header: t("detailDrawing.columns.registerKind"), minSize: 180, cell: ({ row }) => { - // API의 ENM이 제대로 번역되지 않아 코드 값으로 직접 변환 - const kindCode = row.getValue("RegisterKindENM") as string | null; + // API 응답의 RegisterKind는 코드값이므로 직접 매핑하여 번역 + const kindCode = row.getValue("RegisterKind") as string; const translated = translateRegisterKind(kindCode, lng); - return <div>{translated || kindCode || row.original.RegisterKindNM}</div>; + return <div>{translated}</div>; }, }, { diff --git a/lib/dolce/table/drawing-list-columns.tsx b/lib/dolce/table/drawing-list-columns.tsx index 58631084..34055eff 100644 --- a/lib/dolce/table/drawing-list-columns.tsx +++ b/lib/dolce/table/drawing-list-columns.tsx @@ -15,6 +15,11 @@ export function drawingListColumns(lng: string, t: any): ColumnDef<DwgReceiptIte }, }, { + accessorKey: "RegisterGroupId", + header: "RegisterGroupId", + minSize: 150, + }, + { accessorKey: "DrawingName", header: t("drawingList.columns.drawingName"), minSize: 600, @@ -25,7 +30,7 @@ export function drawingListColumns(lng: string, t: any): ColumnDef<DwgReceiptIte { accessorKey: "Discipline", header: t("drawingList.columns.discipline"), - minSize: 80, + minSize: 120, }, { accessorKey: "Manager", diff --git a/lib/dolce/table/drawing-list-table-v2.tsx b/lib/dolce/table/drawing-list-table-v2.tsx new file mode 100644 index 00000000..2ee80f11 --- /dev/null +++ b/lib/dolce/table/drawing-list-table-v2.tsx @@ -0,0 +1,273 @@ +"use client"; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getSortedRowModel, + SortingState, + getPaginationRowModel, +} from "@tanstack/react-table"; +import { useState } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ArrowUpDown, ArrowUp, ArrowDown, ChevronLeft, ChevronRight } from "lucide-react"; + +// 도면 데이터의 기본 인터페이스 +interface DrawingData { + RegisterGroupId?: string | null; + DrawingNo?: string | null; + Discipline?: string | null; + CreateDt?: string | Date | null; +} + +interface DrawingListTableV2Props<TData extends DrawingData, TValue> { + columns: ColumnDef<TData, TValue>[]; + data: TData[]; + onRowClick?: (row: TData) => void; + selectedRow?: TData; + getRowId?: (row: TData) => string; + maxHeight?: string; // e.g., "45vh" + minHeight?: string; // e.g., "400px" + defaultPageSize?: number; +} + +export function DrawingListTableV2<TData extends DrawingData, TValue>({ + columns, + data, + onRowClick, + selectedRow, + getRowId, + maxHeight = "45vh", + minHeight = "400px", + defaultPageSize = 10, +}: DrawingListTableV2Props<TData, TValue>) { + const [sorting, setSorting] = useState<SortingState>([]); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(defaultPageSize); + + // 기본 getRowId 함수: RegisterGroupId + DrawingNo + Discipline + CreateDt 조합 + const defaultGetRowId = (row: TData): string => { + const registerId = row.RegisterGroupId || ''; + const drawingNo = row.DrawingNo || ''; + const discipline = row.Discipline || ''; + const createDt = row.CreateDt + ? (row.CreateDt instanceof Date ? row.CreateDt.toISOString() : String(row.CreateDt)) + : ''; + + return `${registerId}-${drawingNo}-${discipline}-${createDt}`; + }; + + const rowIdGetter = getRowId || defaultGetRowId; + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + onPaginationChange: (updater) => { + if (typeof updater === 'function') { + const newState = updater({ pageIndex, pageSize }); + setPageIndex(newState.pageIndex); + setPageSize(newState.pageSize); + } + }, + state: { + sorting, + pagination: { + pageIndex, + pageSize, + }, + }, + }); + + // 행이 선택되었는지 확인하는 함수 + const isRowSelected = (row: TData): boolean => { + if (!selectedRow) return false; + return rowIdGetter(row) === rowIdGetter(selectedRow); + }; + + const handlePageSizeChange = (value: string) => { + const newSize = value === "all" ? data.length : parseInt(value); + setPageSize(newSize); + setPageIndex(0); + }; + + return ( + <div className="flex flex-col h-full" style={{ minHeight }}> + {/* 테이블 영역 */} + <div + className="rounded-md border overflow-auto flex-1" + style={{ + maxHeight, + minHeight: data.length === 0 ? minHeight : undefined, + }} + > + <Table className="min-w-max"> + <TableHeader className="sticky top-0 z-10 bg-background"> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + const isSorted = header.column.getIsSorted(); + const canSort = header.column.getCanSort(); + + return ( + <TableHead + key={header.id} + style={{ minWidth: header.column.columnDef.minSize }} + className="bg-background" + > + {header.isPlaceholder ? null : ( + <div + className={`flex items-center gap-2 ${ + canSort ? "cursor-pointer select-none hover:text-primary" : "" + }`} + onClick={ + canSort + ? header.column.getToggleSortingHandler() + : undefined + } + > + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {canSort && ( + <span className="text-muted-foreground"> + {isSorted === "asc" ? ( + <ArrowUp className="h-4 w-4" /> + ) : isSorted === "desc" ? ( + <ArrowDown className="h-4 w-4" /> + ) : ( + <ArrowUpDown className="h-4 w-4 opacity-50" /> + )} + </span> + )} + </div> + )} + </TableHead> + ); + })} + </TableRow> + ))} + </TableHeader> + <TableBody> + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => { + const isSelected = isRowSelected(row.original); + return ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + onClick={() => onRowClick?.(row.original)} + className={`cursor-pointer transition-colors ${ + isSelected + ? "bg-accent hover:bg-accent" + : "hover:bg-muted/50" + }`} + > + {row.getVisibleCells().map((cell) => ( + <TableCell + key={cell.id} + style={{ minWidth: cell.column.columnDef.minSize }} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </TableCell> + ))} + </TableRow> + ); + }) + ) : ( + <TableRow> + <TableCell + colSpan={columns.length} + className="text-center text-muted-foreground" + style={{ height: "300px" }} + > + <div className="flex items-center justify-center h-full"> + 데이터가 없습니다. + </div> + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {/* 페이지네이션 컨트롤 - 항상 렌더링하여 높이 일관성 유지 */} + <div className="flex items-center justify-between py-2 px-2 border-t flex-shrink-0"> + {data.length > 0 ? ( + <> + <div className="flex items-center gap-2"> + <span className="text-sm text-muted-foreground"> + 페이지 당 행 수: + </span> + <Select value={pageSize === data.length ? "all" : String(pageSize)} onValueChange={handlePageSizeChange}> + <SelectTrigger className="w-[100px] h-8"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="10">10</SelectItem> + <SelectItem value="20">20</SelectItem> + <SelectItem value="50">50</SelectItem> + <SelectItem value="all">전체</SelectItem> + </SelectContent> + </Select> + </div> + + <div className="flex items-center gap-2"> + <span className="text-sm text-muted-foreground"> + {table.getState().pagination.pageIndex * pageSize + 1}- + {Math.min( + (table.getState().pagination.pageIndex + 1) * pageSize, + data.length + )}{" "} + / {data.length} + </span> + <div className="flex gap-1"> + <Button + variant="outline" + size="sm" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <ChevronLeft className="h-4 w-4" /> + </Button> + <Button + variant="outline" + size="sm" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <ChevronRight className="h-4 w-4" /> + </Button> + </div> + </div> + </> + ) : ( + <div className="text-sm text-muted-foreground opacity-0"> + Placeholder for consistent height + </div> + )} + </div> + </div> + ); +} + |
