217 lines
5.9 KiB
TypeScript
217 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { type ReactNode, useMemo } from "react";
|
|
|
|
import {
|
|
type ColumnDef,
|
|
getCoreRowModel,
|
|
type PaginationState,
|
|
useReactTable,
|
|
} from "@tanstack/react-table";
|
|
|
|
import {
|
|
getStandardRowNumber,
|
|
type ScrollMode,
|
|
StandardDataTable,
|
|
type UiHeadProps,
|
|
} from "@common/business_components";
|
|
import type { UiTableSort } from "@common/business_components/ui/table";
|
|
|
|
type MapPopupDataTableProps<T extends Record<string, unknown>> = {
|
|
rows: T[];
|
|
headlist: UiHeadProps[];
|
|
title: ReactNode;
|
|
total: number;
|
|
page: number;
|
|
pageSize: number;
|
|
onPageChange: (page: number) => void;
|
|
actions?: ReactNode;
|
|
loading?: boolean;
|
|
scrollMode?: ScrollMode;
|
|
minTableWidth?: string;
|
|
emptyMessage?: string;
|
|
viewPagination?: boolean;
|
|
fillAvailableHeight?: boolean;
|
|
rowId?: (row: T, index: number) => string;
|
|
selectedRowId?: string;
|
|
onRowClick?: (row: T, index: number) => void;
|
|
onCellClick?: (row: T, rowIndex: number, columnId: string) => void;
|
|
clickableHeadIds?: string[];
|
|
renderCell?: (
|
|
row: T,
|
|
headId: string,
|
|
rowIndex: number,
|
|
) => ReactNode | undefined;
|
|
sort?: UiTableSort;
|
|
onSortChange?: (sort: UiTableSort | undefined) => void;
|
|
};
|
|
|
|
const ROW_NUMBER_IDS = new Set(["no", "rowNo", "rowNumber"]);
|
|
|
|
const formatCellValue = (value: unknown) => {
|
|
if (value === null || value === undefined || value === "") return "-";
|
|
if (typeof value === "number") return value.toLocaleString("ko-KR");
|
|
return String(value);
|
|
};
|
|
|
|
/** 지도 WindowPopup 목록을 CS 공통 표·순번·페이징 규칙으로 연결한다. */
|
|
export default function MapPopupDataTable<T extends Record<string, unknown>>({
|
|
rows,
|
|
headlist,
|
|
title,
|
|
total,
|
|
page,
|
|
pageSize,
|
|
onPageChange,
|
|
actions,
|
|
loading = false,
|
|
scrollMode = "none",
|
|
minTableWidth = "100%",
|
|
emptyMessage,
|
|
viewPagination = true,
|
|
fillAvailableHeight = false,
|
|
rowId,
|
|
selectedRowId,
|
|
onRowClick,
|
|
onCellClick,
|
|
clickableHeadIds,
|
|
renderCell,
|
|
sort,
|
|
onSortChange,
|
|
}: MapPopupDataTableProps<T>) {
|
|
const visibleHeads = useMemo(
|
|
() =>
|
|
headlist.filter(
|
|
(head) => head.visible !== false && head.id && head.id.length > 1,
|
|
),
|
|
[headlist],
|
|
);
|
|
const columns = useMemo<ColumnDef<T>[]>(
|
|
() =>
|
|
visibleHeads.map((head) => ({
|
|
id: head.id,
|
|
accessorFn: (row) => row[head.id],
|
|
header: () => {
|
|
const title = ROW_NUMBER_IDS.has(head.id) ? "순번" : head.title;
|
|
if (!head.sortable || !onSortChange) return title;
|
|
const key = head.sortKey ?? head.id;
|
|
const direction = sort?.key === key ? sort.direction : undefined;
|
|
return (
|
|
<button
|
|
type="button"
|
|
className="inline-flex w-full items-center justify-center gap-1.5"
|
|
aria-label={`${title} 정렬`}
|
|
onClick={() => {
|
|
if (direction === undefined) {
|
|
onSortChange({ key, direction: "asc" });
|
|
} else if (direction === "asc") {
|
|
onSortChange({ key, direction: "desc" });
|
|
} else {
|
|
onSortChange(undefined);
|
|
}
|
|
}}
|
|
>
|
|
<span>{title}</span>
|
|
<span className="text-[8px] leading-none" aria-hidden="true">
|
|
{direction === "asc" ? "▲" : direction === "desc" ? "▼" : "↕"}
|
|
</span>
|
|
</button>
|
|
);
|
|
},
|
|
cell: ({ row, getValue }) => {
|
|
const rowIndex = row.index;
|
|
const rendered = renderCell?.(row.original, head.id, rowIndex);
|
|
const value = ROW_NUMBER_IDS.has(head.id)
|
|
? getStandardRowNumber({
|
|
total,
|
|
rowIndex,
|
|
pageIndex: page - 1,
|
|
pageSize,
|
|
})
|
|
: rendered === undefined
|
|
? formatCellValue(getValue())
|
|
: rendered;
|
|
const isClickable =
|
|
Boolean(onCellClick) &&
|
|
(!clickableHeadIds || clickableHeadIds.includes(head.id));
|
|
|
|
return isClickable ? (
|
|
<button
|
|
type="button"
|
|
className="w-full cursor-pointer text-inherit"
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onCellClick?.(row.original, rowIndex, head.id);
|
|
}}
|
|
>
|
|
{value}
|
|
</button>
|
|
) : (
|
|
value
|
|
);
|
|
},
|
|
meta: {
|
|
align:
|
|
head.align === "left" || head.align === "right"
|
|
? head.align
|
|
: "center",
|
|
width: head.widthPercent,
|
|
},
|
|
})),
|
|
[
|
|
clickableHeadIds,
|
|
onCellClick,
|
|
page,
|
|
pageSize,
|
|
renderCell,
|
|
sort,
|
|
total,
|
|
visibleHeads,
|
|
onSortChange,
|
|
],
|
|
);
|
|
const pagination: PaginationState = {
|
|
pageIndex: Math.max(page - 1, 0),
|
|
pageSize,
|
|
};
|
|
const table = useReactTable({
|
|
data: rows,
|
|
columns,
|
|
state: {
|
|
pagination,
|
|
rowSelection: selectedRowId ? { [selectedRowId]: true } : {},
|
|
},
|
|
rowCount: total,
|
|
manualPagination: true,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getRowId: (row, index) => rowId?.(row, index) ?? String(index),
|
|
onPaginationChange: (updater) => {
|
|
const next =
|
|
typeof updater === "function" ? updater(pagination) : updater;
|
|
if (next.pageIndex === pagination.pageIndex) return;
|
|
onPageChange(next.pageIndex + 1);
|
|
},
|
|
});
|
|
|
|
return (
|
|
<StandardDataTable
|
|
table={table}
|
|
title={title}
|
|
actions={actions}
|
|
loading={loading}
|
|
emptyMessage={emptyMessage}
|
|
flushOuterMargin
|
|
className="!w-full !max-w-full"
|
|
scrollMode={scrollMode}
|
|
minTableWidth={minTableWidth}
|
|
viewPagination={viewPagination}
|
|
fillAvailableHeight={fillAvailableHeight}
|
|
onRowClick={
|
|
onRowClick ? (row) => onRowClick(row, rows.indexOf(row)) : undefined
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export type { MapPopupDataTableProps };
|