그리드 css 통합
This commit is contained in:
+148
-151
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, useMemo, type CSSProperties, type ReactNode } from "react";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
@@ -8,24 +8,28 @@ import {
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
StandardDataTable,
|
||||
type StandardDataTableLayout,
|
||||
type UiHeadProps,
|
||||
} from "@common/business_components";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import type {
|
||||
PipeAnalysisSummaryViewRow,
|
||||
PipeAnalysisYearMaterialGroup,
|
||||
PipeAnalysisYearMaterialGroupRow,
|
||||
} from "./pipeAnalysisUtils";
|
||||
import "./pipe-analysis-summary-table.css";
|
||||
|
||||
type SummaryRow = PipeAnalysisSummaryViewRow | PipeAnalysisYearMaterialGroup;
|
||||
type SummaryRow =
|
||||
| {
|
||||
kind: "flat";
|
||||
value: PipeAnalysisSummaryViewRow;
|
||||
sourceIndex: number;
|
||||
}
|
||||
| {
|
||||
kind: "grouped";
|
||||
group: PipeAnalysisYearMaterialGroup;
|
||||
value: PipeAnalysisYearMaterialGroupRow;
|
||||
groupIndex: number;
|
||||
groupRowIndex: number;
|
||||
};
|
||||
type Props = {
|
||||
rows: PipeAnalysisSummaryViewRow[];
|
||||
groups: PipeAnalysisYearMaterialGroup[];
|
||||
@@ -53,6 +57,16 @@ const FIXED_WIDTHS: Record<string, number> = {
|
||||
swtrmtZoneNm: 160,
|
||||
totalLengthKm: 100,
|
||||
};
|
||||
/** 공통 그리드가 같은 순서로 왼쪽 고정 위치를 계산할 관로 식별 열이다. */
|
||||
const PINNED_COLUMN_IDS = Object.keys(FIXED_WIDTHS);
|
||||
/** 관로분석에서 선택하는 공통 표 레이아웃이며 화면 전용 CSS를 대체한다. */
|
||||
const PIPE_SUMMARY_LAYOUT: StandardDataTableLayout = {
|
||||
tableLayout: "fixed",
|
||||
stickyHeader: true,
|
||||
headerHeight: "64px",
|
||||
headerWrap: true,
|
||||
cellOverflow: "ellipsis",
|
||||
};
|
||||
/** 기존 집계와 같은 천 단위 구분 및 소수점 세 자리 표시를 유지한다. */
|
||||
const formatValue = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
@@ -61,6 +75,24 @@ const formatValue = (value: unknown) => {
|
||||
: String(value);
|
||||
};
|
||||
|
||||
/** 결과가 있는 셀만 링크형 버튼으로 표시하고 나머지는 전체값 title을 유지한다. */
|
||||
const renderValue = (value: unknown, onClick?: () => void) => {
|
||||
const formatted = formatValue(value);
|
||||
return onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full text-inherit text-primary underline underline-offset-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
||||
onClick={onClick}
|
||||
>
|
||||
{formatted}
|
||||
</button>
|
||||
) : (
|
||||
<span className="block overflow-hidden text-ellipsis" title={formatted}>
|
||||
{formatted}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
/** 표준 카드·페이징 안에서 관로 집계의 병합 셀과 고정 열을 렌더링한다. */
|
||||
export default function PipeAnalysisSummaryTable({
|
||||
rows,
|
||||
@@ -76,15 +108,91 @@ export default function PipeAnalysisSummaryTable({
|
||||
onCellClick,
|
||||
onGroupedCellClick,
|
||||
}: Props) {
|
||||
// 표준 페이징이 기존 행 또는 처리구역 그룹 수를 기준으로 동작하도록 연결한다.
|
||||
// 병합 표도 공통 렌더러가 행을 순회할 수 있도록 처리구역의 4개 경과년도 행만 펼친다.
|
||||
const data = useMemo<SummaryRow[]>(
|
||||
() => (grouped ? groups : rows),
|
||||
() =>
|
||||
grouped
|
||||
? groups.flatMap((group, groupIndex) =>
|
||||
group.rows.map((value, groupRowIndex) => ({
|
||||
kind: "grouped" as const,
|
||||
group,
|
||||
value,
|
||||
groupIndex,
|
||||
groupRowIndex,
|
||||
})),
|
||||
)
|
||||
: rows.map((value, sourceIndex) => ({
|
||||
kind: "flat" as const,
|
||||
value,
|
||||
sourceIndex,
|
||||
})),
|
||||
[grouped, groups, rows],
|
||||
);
|
||||
// 실제 셀은 renderTable에서 그리며 표준 테이블에는 열 식별자를 등록한다.
|
||||
// 공통 렌더러가 colgroup과 고정 위치에 사용할 열별 픽셀 너비를 한곳에서 계산한다.
|
||||
const layout = useMemo(() => {
|
||||
return heads.map((head) => {
|
||||
const width =
|
||||
FIXED_WIDTHS[head.id] ??
|
||||
(head.id === "elapsedYearLabel"
|
||||
? 112
|
||||
: String(head.title).length > 10
|
||||
? 160
|
||||
: 120);
|
||||
const fixed = head.id in FIXED_WIDTHS;
|
||||
return { ...head, width, fixed };
|
||||
});
|
||||
}, [heads]);
|
||||
// 모든 재질 열의 너비 합계를 최소 폭으로 사용해 좁은 팝업에서 열 압축을 막는다.
|
||||
const tableWidth = layout.reduce((sum, head) => sum + head.width, 0);
|
||||
// 열은 값·업무 클릭만 정의하고 실제 th/td, 고정 열과 overflow는 공통 렌더러가 소유한다.
|
||||
const columns = useMemo<ColumnDef<SummaryRow>[]>(
|
||||
() => heads.map((head) => ({ id: head.id })),
|
||||
[heads],
|
||||
() =>
|
||||
layout.map((head) => ({
|
||||
id: head.id,
|
||||
size: head.width,
|
||||
minSize: head.width,
|
||||
maxSize: head.width,
|
||||
header: () => <span title={String(head.title)}>{head.title}</span>,
|
||||
cell: ({ row }) => {
|
||||
const summaryRow = row.original;
|
||||
if (summaryRow.kind === "flat") {
|
||||
const value =
|
||||
head.id === "rowNo"
|
||||
? total - ((page - 1) * pageSize + summaryRow.sourceIndex)
|
||||
: summaryRow.value[head.id];
|
||||
const onClick =
|
||||
summaryRow.value.__cellFilters[head.id] && Number(value) > 0
|
||||
? () => onCellClick(summaryRow.value, head.id)
|
||||
: undefined;
|
||||
return renderValue(value, onClick);
|
||||
}
|
||||
|
||||
if (head.fixed) {
|
||||
return renderValue(
|
||||
summaryRow.group[head.id as keyof PipeAnalysisYearMaterialGroup],
|
||||
);
|
||||
}
|
||||
if (head.id === "elapsedYearLabel") {
|
||||
return renderValue(summaryRow.value.elapsedYearLabel);
|
||||
}
|
||||
|
||||
const value = Number(summaryRow.value.metrics[head.id] ?? 0);
|
||||
const onClick =
|
||||
value > 0 && summaryRow.value.cellFilters[head.id]
|
||||
? () =>
|
||||
onGroupedCellClick(
|
||||
summaryRow.group,
|
||||
summaryRow.value,
|
||||
head.id,
|
||||
)
|
||||
: undefined;
|
||||
return renderValue(value, onClick);
|
||||
},
|
||||
meta: {
|
||||
align: head.align === "right" ? "right" : "center",
|
||||
},
|
||||
})),
|
||||
[layout, onCellClick, onGroupedCellClick, page, pageSize, total],
|
||||
);
|
||||
// 페이지 단위는 기존과 동일하게 일반 표는 행, 병합 표는 처리구역 그룹이다.
|
||||
const pagination = { pageIndex: Math.max(0, page - 1), pageSize };
|
||||
@@ -95,6 +203,10 @@ export default function PipeAnalysisSummaryTable({
|
||||
rowCount: total,
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (row) =>
|
||||
row.kind === "flat"
|
||||
? `flat-${row.sourceIndex}`
|
||||
: `group-${row.groupIndex}-${row.value.elapsedYearBucket}`,
|
||||
onPaginationChange: (updater) => {
|
||||
const next =
|
||||
typeof updater === "function" ? updater(pagination) : updater;
|
||||
@@ -102,51 +214,6 @@ export default function PipeAnalysisSummaryTable({
|
||||
onPageChange(next.pageIndex + 1);
|
||||
},
|
||||
});
|
||||
// 열별 픽셀 너비와 왼쪽 고정 위치를 한곳에서 계산해 헤더·본문을 일치시킨다.
|
||||
const layout = useMemo(() => {
|
||||
let left = 0;
|
||||
return heads.map((head) => {
|
||||
const width =
|
||||
FIXED_WIDTHS[head.id] ??
|
||||
(head.id === "elapsedYearLabel"
|
||||
? 112
|
||||
: String(head.title).length > 10
|
||||
? 160
|
||||
: 120);
|
||||
const fixed = head.id in FIXED_WIDTHS;
|
||||
const style: CSSProperties = fixed ? { left } : {};
|
||||
if (fixed) left += width;
|
||||
return { ...head, width, fixed, style };
|
||||
});
|
||||
}, [heads]);
|
||||
// 모든 재질 열의 너비 합계를 최소 폭으로 사용해 좁은 팝업에서 열 압축을 막는다.
|
||||
const tableWidth = layout.reduce((sum, head) => sum + head.width, 0);
|
||||
|
||||
/** 실제 결과가 있는 수치만 기존 상세 조회 핸들러로 연결한다. */
|
||||
const cell = (
|
||||
head: (typeof layout)[number],
|
||||
value: unknown,
|
||||
onClick?: () => void,
|
||||
rowSpan?: number,
|
||||
) => (
|
||||
<TableCell
|
||||
key={head.id}
|
||||
rowSpan={rowSpan}
|
||||
style={head.style}
|
||||
className={`${head.fixed ? "pipe-summary-fixed" : ""} ${head.id === "totalLengthKm" ? "pipe-summary-fixed-end" : ""}`}
|
||||
align={head.align === "right" ? "right" : "center"}
|
||||
>
|
||||
{onClick ? (
|
||||
<button type="button" className="pipe-summary-value" onClick={onClick}>
|
||||
{formatValue(value)}
|
||||
</button>
|
||||
) : (
|
||||
<span className="pipe-summary-text" title={formatValue(value)}>
|
||||
{formatValue(value)}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
|
||||
return (
|
||||
<StandardDataTable
|
||||
@@ -154,99 +221,29 @@ export default function PipeAnalysisSummaryTable({
|
||||
table={table}
|
||||
title={title}
|
||||
flushOuterMargin
|
||||
className="sn-pipe-summary-grid"
|
||||
scrollMode="both"
|
||||
maxHeight="calc(100dvh - 34rem)"
|
||||
maxHeight="max(240px, calc(100dvh - 34rem))"
|
||||
minTableWidth={`${tableWidth}px`}
|
||||
layout={PIPE_SUMMARY_LAYOUT}
|
||||
pinnedColumns={PINNED_COLUMN_IDS}
|
||||
getCellProps={({ row, columnId }) => {
|
||||
const summaryRow = row.original;
|
||||
const isPinnedColumn = PINNED_COLUMN_IDS.includes(columnId);
|
||||
const isGrouped = summaryRow.kind === "grouped";
|
||||
|
||||
return {
|
||||
hidden: isGrouped && isPinnedColumn && summaryRow.groupRowIndex > 0,
|
||||
rowSpan:
|
||||
isGrouped && isPinnedColumn && summaryRow.groupRowIndex === 0
|
||||
? summaryRow.group.rows.length
|
||||
: undefined,
|
||||
className:
|
||||
isGrouped && summaryRow.groupRowIndex === 0
|
||||
? "border-t border-border"
|
||||
: undefined,
|
||||
};
|
||||
}}
|
||||
actions={actions}
|
||||
renderTable={() => (
|
||||
<Table
|
||||
variant="default"
|
||||
style={{
|
||||
width: `max(100%, ${tableWidth}px)`,
|
||||
minWidth: tableWidth,
|
||||
tableLayout: "fixed",
|
||||
}}
|
||||
>
|
||||
<colgroup>
|
||||
{layout.map((head) => (
|
||||
<col key={head.id} style={{ width: head.width }} />
|
||||
))}
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{layout.map((head) => (
|
||||
<TableHead
|
||||
key={head.id}
|
||||
scope="col"
|
||||
style={head.style}
|
||||
className={`${head.fixed ? "pipe-summary-fixed" : ""} ${head.id === "totalLengthKm" ? "pipe-summary-fixed-end" : ""}`}
|
||||
>
|
||||
<span title={String(head.title)}>{head.title}</span>
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{grouped
|
||||
? groups.map((group, groupIndex) => (
|
||||
<Fragment
|
||||
key={`${group.base.ctpvCd}-${group.base.sggCd}-${group.base.swtrmtZoneNm}-${groupIndex}`}
|
||||
>
|
||||
{group.rows.map((row, rowIndex) => (
|
||||
<TableRow
|
||||
key={row.elapsedYearBucket}
|
||||
className={
|
||||
rowIndex === 0 ? "pipe-summary-group-start" : ""
|
||||
}
|
||||
>
|
||||
{layout.map((head) => {
|
||||
if (head.fixed) {
|
||||
if (rowIndex !== 0) return null;
|
||||
return cell(
|
||||
head,
|
||||
group[
|
||||
head.id as keyof PipeAnalysisYearMaterialGroup
|
||||
],
|
||||
undefined,
|
||||
group.rows.length,
|
||||
);
|
||||
}
|
||||
if (head.id === "elapsedYearLabel")
|
||||
return cell(head, row.elapsedYearLabel);
|
||||
const value = Number(row.metrics[head.id] ?? 0);
|
||||
return cell(
|
||||
head,
|
||||
value,
|
||||
value > 0 && row.cellFilters[head.id]
|
||||
? () => onGroupedCellClick(group, row, head.id)
|
||||
: undefined,
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</Fragment>
|
||||
))
|
||||
: rows.map((row, index) => (
|
||||
<TableRow
|
||||
key={`${row.__base.ctpvCd}-${row.__base.sggCd}-${row.__base.swtrmtZoneNm}-${index}`}
|
||||
>
|
||||
{layout.map((head) =>
|
||||
cell(
|
||||
head,
|
||||
head.id === "rowNo"
|
||||
? total - ((page - 1) * pageSize + index)
|
||||
: row[head.id],
|
||||
row.__cellFilters[head.id] && Number(row[head.id]) > 0
|
||||
? () => onCellClick(row, head.id)
|
||||
: undefined,
|
||||
),
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/* 관로 집계 전용: 퍼센트 열 압축과 중첩 가로 스크롤 없이 병합 셀을 유지한다. */
|
||||
.sn-pipe-summary-grid .standard-data-table__scroll {
|
||||
overflow: auto;
|
||||
max-height: max(240px, calc(100dvh - 34rem));
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.sn-pipe-summary-grid .table-ui {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
.sn-pipe-summary-grid .table-ui thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
.sn-pipe-summary-grid .table-ui th {
|
||||
height: 64px;
|
||||
word-break: keep-all;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
.sn-pipe-summary-grid .table-ui td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sn-pipe-summary-grid .pipe-summary-fixed {
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
}
|
||||
.sn-pipe-summary-grid td.pipe-summary-fixed {
|
||||
background: var(--background, #fff);
|
||||
}
|
||||
.sn-pipe-summary-grid th.pipe-summary-fixed {
|
||||
z-index: 5;
|
||||
}
|
||||
.sn-pipe-summary-grid .pipe-summary-text {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sn-pipe-summary-grid .pipe-summary-value {
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
text-align: inherit;
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.sn-pipe-summary-grid .pipe-summary-value:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.sn-pipe-summary-grid .pipe-summary-group-start td {
|
||||
border-top: 1px solid var(--border, #ddd);
|
||||
}
|
||||
@@ -10,13 +10,24 @@ import {
|
||||
type ReactNode,
|
||||
type UIEventHandler,
|
||||
} from "react";
|
||||
import type { Table as TableInstance } from "@tanstack/react-table";
|
||||
import {
|
||||
flexRender,
|
||||
type Cell,
|
||||
type Row,
|
||||
type Table as TableInstance,
|
||||
} from "@tanstack/react-table";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
LoadingWithAnimation,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
cn,
|
||||
} from "@common/components";
|
||||
|
||||
@@ -26,6 +37,28 @@ type StandardDataTableRenderContext<T> = {
|
||||
table: TableInstance<T>;
|
||||
};
|
||||
|
||||
type StandardDataTableLayout = {
|
||||
tableLayout?: "auto" | "fixed";
|
||||
stickyHeader?: boolean;
|
||||
headerHeight?: string;
|
||||
headerWrap?: boolean;
|
||||
cellOverflow?: "wrap" | "nowrap" | "ellipsis";
|
||||
};
|
||||
|
||||
type StandardDataTableCellContext<T> = {
|
||||
row: Row<T>;
|
||||
cell: Cell<T, unknown>;
|
||||
rowIndex: number;
|
||||
columnId: string;
|
||||
};
|
||||
|
||||
type StandardDataTableCellProps = {
|
||||
hidden?: boolean;
|
||||
rowSpan?: number;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
type StandardDataTableProps<T> = {
|
||||
table: TableInstance<T>;
|
||||
title?: ReactNode;
|
||||
@@ -47,12 +80,25 @@ type StandardDataTableProps<T> = {
|
||||
renderTable?: (context: StandardDataTableRenderContext<T>) => ReactNode;
|
||||
/** 고정 열 경계처럼 가로 스크롤 위치가 필요한 소비 화면에 공통 스크롤 이벤트를 전달한다. */
|
||||
onScroll?: UIEventHandler<HTMLDivElement>;
|
||||
/** 표 레이아웃·고정 헤더·셀 넘침처럼 업무와 무관한 공통 표시 방식을 선택한다. */
|
||||
layout?: StandardDataTableLayout;
|
||||
/** 왼쪽에 고정할 열 ID이며 실제 left 위치는 등록된 열 너비를 기준으로 계산한다. */
|
||||
pinnedColumns?: string[];
|
||||
/** 병합 셀처럼 행별 HTML 셀 속성이 필요한 복합표의 표시 계약만 전달한다. */
|
||||
getCellProps?: (
|
||||
context: StandardDataTableCellContext<T>,
|
||||
) => StandardDataTableCellProps | undefined;
|
||||
onRowClick?: (row: T) => void;
|
||||
};
|
||||
|
||||
type StandardDataTableStyle = CSSProperties & {
|
||||
"--standard-data-table-min-width"?: string;
|
||||
"--standard-data-table-max-height"?: string;
|
||||
"--standard-data-table-header-height"?: string;
|
||||
};
|
||||
|
||||
type StandardPinnedCellStyle = CSSProperties & {
|
||||
"--standard-data-table-pinned-left"?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_PAGINATION_MARGIN_TOP = 24;
|
||||
@@ -179,6 +225,129 @@ function StandardDataTablePagination<T>({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 고정 열과 행별 병합 속성이 필요한 표를 공통 Table 마크업으로 렌더링한다.
|
||||
* 일반 표는 기존 DataTable 경로를 유지해 확장 옵션을 사용하지 않는 화면의 동작을 바꾸지 않는다.
|
||||
*/
|
||||
function StandardDataTableRenderer<T>({
|
||||
table,
|
||||
variant,
|
||||
layout,
|
||||
pinnedColumns,
|
||||
getCellProps,
|
||||
onRowClick,
|
||||
}: {
|
||||
table: TableInstance<T>;
|
||||
variant: "default" | "secondary" | "line";
|
||||
layout?: StandardDataTableLayout;
|
||||
pinnedColumns: string[];
|
||||
getCellProps?: (
|
||||
context: StandardDataTableCellContext<T>,
|
||||
) => StandardDataTableCellProps | undefined;
|
||||
onRowClick?: (row: T) => void;
|
||||
}) {
|
||||
const pinnedColumnIds = new Set(pinnedColumns);
|
||||
const pinnedLeftByColumnId = new Map<string, number>();
|
||||
let pinnedLeft = 0;
|
||||
|
||||
table.getVisibleLeafColumns().forEach((column) => {
|
||||
if (!pinnedColumnIds.has(column.id)) return;
|
||||
pinnedLeftByColumnId.set(column.id, pinnedLeft);
|
||||
pinnedLeft += column.getSize();
|
||||
});
|
||||
|
||||
/** 열 ID에 대응하는 공통 고정 위치를 헤더와 본문에 동일하게 전달한다. */
|
||||
const getPinnedProps = (columnId: string) => {
|
||||
const left = pinnedLeftByColumnId.get(columnId);
|
||||
if (left === undefined) return { pinned: undefined, style: undefined };
|
||||
|
||||
const style: StandardPinnedCellStyle = {
|
||||
"--standard-data-table-pinned-left": `${left}px`,
|
||||
};
|
||||
return { pinned: "left", style };
|
||||
};
|
||||
|
||||
return (
|
||||
<Table variant={variant} style={{ tableLayout: layout?.tableLayout }}>
|
||||
<colgroup>
|
||||
{table.getVisibleLeafColumns().map((column) => (
|
||||
<col key={column.id} style={{ width: column.getSize() }} />
|
||||
))}
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const pinned = getPinnedProps(header.column.id);
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
data-pinned={pinned.pinned}
|
||||
style={pinned.style}
|
||||
align={header.column.columnDef.meta?.align}
|
||||
>
|
||||
<div className="standard-data-table__header-content">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</div>
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
className={cn(
|
||||
onRowClick && "cursor-pointer hover:bg-accent",
|
||||
row.getIsSelected() && "!bg-primary-transparent",
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const presentation = getCellProps?.({
|
||||
row,
|
||||
cell,
|
||||
rowIndex: row.index,
|
||||
columnId: cell.column.id,
|
||||
});
|
||||
if (presentation?.hidden) return null;
|
||||
|
||||
const pinned = getPinnedProps(cell.column.id);
|
||||
const style: StandardPinnedCellStyle = {
|
||||
...presentation?.style,
|
||||
...pinned.style,
|
||||
};
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
rowSpan={presentation?.rowSpan}
|
||||
className={presentation?.className}
|
||||
data-pinned={pinned.pinned}
|
||||
style={style}
|
||||
align={cell.column.columnDef.meta?.align}
|
||||
>
|
||||
<div className="standard-data-table__cell-content">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
function StandardDataTable<T>({
|
||||
table,
|
||||
title,
|
||||
@@ -196,6 +365,9 @@ function StandardDataTable<T>({
|
||||
fillAvailableHeight = false,
|
||||
renderTable,
|
||||
onScroll,
|
||||
layout,
|
||||
pinnedColumns = [],
|
||||
getCellProps,
|
||||
onRowClick,
|
||||
}: StandardDataTableProps<T>) {
|
||||
const rowCount = table.getRowModel().rows.length;
|
||||
@@ -214,6 +386,12 @@ function StandardDataTable<T>({
|
||||
"--standard-data-table-min-width": minTableWidth,
|
||||
"--standard-data-table-max-height": maxHeight,
|
||||
};
|
||||
const layoutStyle: StandardDataTableStyle = {
|
||||
"--standard-data-table-header-height": layout?.headerHeight,
|
||||
};
|
||||
const usesExtendedRenderer = Boolean(
|
||||
layout || pinnedColumns.length > 0 || getCellProps,
|
||||
);
|
||||
|
||||
/** 실제 가로 스크롤바가 차지하는 높이만큼 페이징 여백을 줄여 CS의 24px 체감 간격을 유지한다. */
|
||||
useEffect(() => {
|
||||
@@ -253,11 +431,16 @@ function StandardDataTable<T>({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"standard-data-table card card-border w-full max-w-full min-w-0 overflow-hidden",
|
||||
"standard-data-table card card-border w-full min-w-0 max-w-full overflow-hidden",
|
||||
flushOuterMargin && "!m-0",
|
||||
shouldFillAvailableHeight ? "flex min-h-0 flex-1 flex-col" : "shrink-0",
|
||||
className,
|
||||
)}
|
||||
data-sticky-header={layout?.stickyHeader ? "true" : undefined}
|
||||
data-header-wrap={layout?.headerWrap ? "true" : undefined}
|
||||
data-cell-overflow={layout?.cellOverflow}
|
||||
data-has-pinned-columns={pinnedColumns.length > 0 ? "true" : undefined}
|
||||
style={layoutStyle}
|
||||
>
|
||||
{title || actions ? (
|
||||
<div className="card-header flex flex-wrap items-center justify-between gap-4">
|
||||
@@ -310,7 +493,17 @@ function StandardDataTable<T>({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
(renderTable?.({ table }) ?? (
|
||||
(renderTable?.({ table }) ??
|
||||
(usesExtendedRenderer ? (
|
||||
<StandardDataTableRenderer
|
||||
table={table}
|
||||
variant={variant}
|
||||
layout={layout}
|
||||
pinnedColumns={pinnedColumns}
|
||||
getCellProps={getCellProps}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
) : (
|
||||
<DataTable
|
||||
table={table}
|
||||
variant={variant}
|
||||
@@ -318,7 +511,7 @@ function StandardDataTable<T>({
|
||||
viewPagination={false}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
))
|
||||
)))
|
||||
)}
|
||||
</div>
|
||||
{viewPagination && !loading && rowCount > 0 ? (
|
||||
@@ -335,6 +528,9 @@ function StandardDataTable<T>({
|
||||
export { STANDARD_ROW_NUMBER_HEADER, StandardDataTable, getStandardRowNumber };
|
||||
export type {
|
||||
ScrollMode,
|
||||
StandardDataTableCellContext,
|
||||
StandardDataTableCellProps,
|
||||
StandardDataTableLayout,
|
||||
StandardDataTableProps,
|
||||
StandardDataTableRenderContext,
|
||||
StandardRowNumberParams,
|
||||
|
||||
+44
@@ -20,4 +20,48 @@
|
||||
.standard-data-table__scroll thead th {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.standard-data-table[data-has-pinned-columns="true"] .table-ui {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.standard-data-table[data-sticky-header="true"] .table-ui thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.standard-data-table[data-header-wrap="true"] .table-ui th {
|
||||
height: var(--standard-data-table-header-height, auto);
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.standard-data-table[data-cell-overflow="nowrap"] .table-ui td,
|
||||
.standard-data-table[data-cell-overflow="ellipsis"] .table-ui td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.standard-data-table[data-cell-overflow="ellipsis"]
|
||||
.standard-data-table__cell-content {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.standard-data-table .table-ui [data-pinned="left"] {
|
||||
left: var(--standard-data-table-pinned-left);
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.standard-data-table .table-ui td[data-pinned="left"] {
|
||||
background: var(--background, #fff);
|
||||
}
|
||||
|
||||
.standard-data-table .table-ui th[data-pinned="left"] {
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user