"use client"; import { useMemo, type ReactNode } from "react"; import { getCoreRowModel, useReactTable, type ColumnDef, } from "@tanstack/react-table"; import { StandardDataTable, type StandardDataTableLayout, type UiHeadProps, } from "@common/business_components"; import type { PipeAnalysisSummaryViewRow, PipeAnalysisYearMaterialGroup, PipeAnalysisYearMaterialGroupRow, } from "./pipeAnalysisUtils"; type SummaryRow = | { kind: "flat"; value: PipeAnalysisSummaryViewRow; sourceIndex: number; } | { kind: "grouped"; group: PipeAnalysisYearMaterialGroup; value: PipeAnalysisYearMaterialGroupRow; groupIndex: number; groupRowIndex: number; }; type Props = { rows: PipeAnalysisSummaryViewRow[]; groups: PipeAnalysisYearMaterialGroup[]; grouped: boolean; heads: UiHeadProps[]; title: string; total: number; page: number; pageSize: number; actions: ReactNode; onPageChange: (page: number) => void; onCellClick: (row: PipeAnalysisSummaryViewRow, columnId: string) => void; onGroupedCellClick: ( group: PipeAnalysisYearMaterialGroup, row: PipeAnalysisYearMaterialGroupRow, columnId: string, ) => void; }; /** 지역 식별 열은 가로 스크롤해도 남겨 두고, 모든 집계 방식에 같은 너비를 사용한다. */ const FIXED_WIDTHS: Record = { rowNo: 56, ctpvNm: 120, sggNm: 120, 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 "-"; return typeof value === "number" ? value.toLocaleString("ko-KR", { maximumFractionDigits: 3 }) : String(value); }; /** 결과가 있는 셀만 링크형 버튼으로 표시하고 나머지는 전체값 title을 유지한다. */ const renderValue = (value: unknown, onClick?: () => void) => { const formatted = formatValue(value); return onClick ? ( ) : ( {formatted} ); }; /** 표준 카드·페이징 안에서 관로 집계의 병합 셀과 고정 열을 렌더링한다. */ export default function PipeAnalysisSummaryTable({ rows, groups, grouped, heads, title, total, page, pageSize, actions, onPageChange, onCellClick, onGroupedCellClick, }: Props) { // 병합 표도 공통 렌더러가 행을 순회할 수 있도록 처리구역의 4개 경과년도 행만 펼친다. const data = useMemo( () => 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], ); // 공통 렌더러가 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[]>( () => layout.map((head) => ({ id: head.id, size: head.width, minSize: head.width, maxSize: head.width, header: () => {head.title}, 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 }; const table = useReactTable({ data, columns, state: { pagination }, 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; if (next.pageIndex !== pagination.pageIndex) onPageChange(next.pageIndex + 1); }, }); return ( { 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} /> ); }