Files
frontend/apps/sn/src/features/windowPopup/ui/MapPopupDataTable.tsx
T

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 };