727 lines
28 KiB
TypeScript
727 lines
28 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { Button, Table, TableBody, TableCell, TableHeader, TableHead, TableRow } from "@common/components";
|
|
import { UiTable, downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
|
import type { UiTableSort } from "@common/business_components/ui";
|
|
import UiPageNations from "@common/business_components/ui/table/uiPageNations";
|
|
import { getData } from "@/components/lib/api";
|
|
import LoadingOverlay from "@/components/ui/LoadingBar";
|
|
import useStore from "@/store/useStore";
|
|
import { useUserStore } from "@/shared/lib/use-user-store";
|
|
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
|
import {
|
|
PIPE_ANALYSIS_SUMMARY_DEFAULT_FILTERS,
|
|
PIPE_ANALYSIS_TYPE_OPTIONS,
|
|
applyScopeToSummaryFilters,
|
|
buildPipeAnalysisFileDate,
|
|
buildPipeAnalysisYearMaterialGroups,
|
|
buildPipeAnalysisScope,
|
|
buildPipeAnalysisSummaryDownloadHeaders,
|
|
buildPipeAnalysisSummaryHeadList,
|
|
buildPipeAnalysisSummaryViewRows,
|
|
fetchPipeAnalysisZoneOptions,
|
|
resolvePipeAnalysisTypeLabel,
|
|
type PipeAnalysisMaterialColumn,
|
|
type PipeAnalysisSummaryFilters,
|
|
type PipeAnalysisSummaryResponse,
|
|
type PipeAnalysisSummaryViewRow,
|
|
type PipeAnalysisYearMaterialGroup,
|
|
} from "./pipeAnalysisUtils";
|
|
|
|
const SUMMARY_FILE_PREFIX: Record<string, string> = {
|
|
YEAR: "관로분석_경과년도별",
|
|
MATERIAL: "관로분석_관재질별",
|
|
YEAR_MATERIAL: "관로분석_경과년도별_관재질별",
|
|
};
|
|
const PIPE_ANALYSIS_SUMMARY_PAGE_SIZE = 10;
|
|
const YEAR_MATERIAL_SORTABLE_KEYS = new Set([
|
|
"rowNo",
|
|
"ctpvNm",
|
|
"sggNm",
|
|
"swtrmtZoneNm",
|
|
"totalLengthKm",
|
|
]);
|
|
|
|
const toExcelColumnName = (index: number) => {
|
|
let current = index;
|
|
let result = "";
|
|
|
|
while (current > 0) {
|
|
const remainder = (current - 1) % 26;
|
|
result = String.fromCharCode(65 + remainder) + result;
|
|
current = Math.floor((current - 1) / 26);
|
|
}
|
|
|
|
return result || "A";
|
|
};
|
|
|
|
const formatGridNumber = (value: unknown) => {
|
|
const num = Number(value ?? 0);
|
|
if (!Number.isFinite(num)) {
|
|
return String(value ?? "");
|
|
}
|
|
return num.toLocaleString("ko-KR", {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 3,
|
|
});
|
|
};
|
|
|
|
const resolveErrorMessage = (error: unknown, fallbackMessage: string) =>
|
|
error instanceof Error && error.message.trim() ? error.message : fallbackMessage;
|
|
const KOREAN_GRID_COLLATOR = new Intl.Collator("ko", {
|
|
numeric: true,
|
|
sensitivity: "base",
|
|
});
|
|
|
|
/** Sorts the complete summary result before client-side pagination. */
|
|
const sortSummaryRows = (
|
|
sourceRows: PipeAnalysisSummaryViewRow[],
|
|
sort?: UiTableSort,
|
|
) => {
|
|
if (!sort) return sourceRows;
|
|
|
|
const sortableRows = sourceRows.filter((row) => !row.__isNoZone && !row.__isSummaryTotal);
|
|
const pinnedRows = sourceRows.filter((row) => row.__isNoZone || row.__isSummaryTotal);
|
|
|
|
sortableRows.sort((left, right) => {
|
|
const leftValue = left[sort.key];
|
|
const rightValue = right[sort.key];
|
|
const leftEmpty = leftValue === null || leftValue === undefined || leftValue === "";
|
|
const rightEmpty = rightValue === null || rightValue === undefined || rightValue === "";
|
|
|
|
if (leftEmpty || rightEmpty) {
|
|
if (leftEmpty && rightEmpty) return 0;
|
|
return leftEmpty ? 1 : -1;
|
|
}
|
|
|
|
const leftNumber = typeof leftValue === "number" ? leftValue : Number.NaN;
|
|
const rightNumber = typeof rightValue === "number" ? rightValue : Number.NaN;
|
|
const compared = Number.isFinite(leftNumber) && Number.isFinite(rightNumber)
|
|
? leftNumber - rightNumber
|
|
: KOREAN_GRID_COLLATOR.compare(String(leftValue), String(rightValue));
|
|
|
|
return sort.direction === "asc" ? compared : -compared;
|
|
});
|
|
|
|
return [...sortableRows, ...pinnedRows];
|
|
};
|
|
|
|
const getYearMaterialGroupSortValue = (
|
|
group: PipeAnalysisYearMaterialGroup,
|
|
key: string,
|
|
) => {
|
|
switch (key) {
|
|
case "rowNo":
|
|
return group.rowNo;
|
|
case "ctpvNm":
|
|
return group.ctpvNm;
|
|
case "sggNm":
|
|
return group.sggNm;
|
|
case "swtrmtZoneNm":
|
|
return group.swtrmtZoneNm;
|
|
case "totalLengthKm":
|
|
return group.totalLengthKm;
|
|
default:
|
|
return group.rows.reduce((sum, row) => sum + Number(row.metrics[key] ?? 0), 0);
|
|
}
|
|
};
|
|
|
|
const sortYearMaterialGroups = (
|
|
groups: PipeAnalysisYearMaterialGroup[],
|
|
sort?: UiTableSort,
|
|
) => {
|
|
if (!sort || !YEAR_MATERIAL_SORTABLE_KEYS.has(sort.key)) return groups;
|
|
|
|
const sortedGroups = [...groups];
|
|
sortedGroups.sort((left, right) => {
|
|
const leftValue = getYearMaterialGroupSortValue(left, sort.key);
|
|
const rightValue = getYearMaterialGroupSortValue(right, sort.key);
|
|
const compared = typeof leftValue === "number" && typeof rightValue === "number"
|
|
? leftValue - rightValue
|
|
: KOREAN_GRID_COLLATOR.compare(String(leftValue), String(rightValue));
|
|
|
|
if (compared !== 0) {
|
|
return sort.direction === "asc" ? compared : -compared;
|
|
}
|
|
|
|
return KOREAN_GRID_COLLATOR.compare(left.swtrmtZoneNm, right.swtrmtZoneNm);
|
|
});
|
|
|
|
return sortedGroups;
|
|
};
|
|
|
|
export default function PipeAnalysisPopupPage() {
|
|
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
|
const userInfo = useUserStore((state) => state.userInfo);
|
|
const scope = useMemo(() => buildPipeAnalysisScope(userInfo), [userInfo]);
|
|
const [draftFilters, setDraftFilters] = useState<PipeAnalysisSummaryFilters>(() =>
|
|
applyScopeToSummaryFilters(PIPE_ANALYSIS_SUMMARY_DEFAULT_FILTERS, scope),
|
|
);
|
|
const [appliedFilters, setAppliedFilters] = useState<PipeAnalysisSummaryFilters>(() =>
|
|
applyScopeToSummaryFilters(PIPE_ANALYSIS_SUMMARY_DEFAULT_FILTERS, scope),
|
|
);
|
|
const [materialColumns, setMaterialColumns] = useState<PipeAnalysisMaterialColumn[]>([]);
|
|
const [rows, setRows] = useState<PipeAnalysisSummaryViewRow[]>([]);
|
|
const [zoneOptions, setZoneOptions] = useState<Array<{ value: string; label: string }>>([]);
|
|
const [summaryCurrentPage, setSummaryCurrentPage] = useState(1);
|
|
const [sort, setSort] = useState<UiTableSort>();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const initializedRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (ctpv.length === 0) fetchCtpv();
|
|
if (sgg.length === 0) fetchSgg();
|
|
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
|
|
|
useEffect(() => {
|
|
setDraftFilters((current) => applyScopeToSummaryFilters(current, scope));
|
|
setAppliedFilters((current) => applyScopeToSummaryFilters(current, scope));
|
|
}, [scope]);
|
|
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
|
|
(async () => {
|
|
try {
|
|
const zones = await fetchPipeAnalysisZoneOptions({
|
|
ctpvCd: draftFilters.ctpvCd,
|
|
sggCd: draftFilters.sggCd,
|
|
scopeRegionCode: scope.regionCode,
|
|
});
|
|
if (!mounted) return;
|
|
setZoneOptions([{ value: "0", label: "전체" }, ...zones]);
|
|
} catch (error) {
|
|
console.error("처리구역 목록 조회 중 오류:", error);
|
|
if (!mounted) return;
|
|
setZoneOptions([{ value: "0", label: "전체" }]);
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
mounted = false;
|
|
};
|
|
}, [draftFilters.ctpvCd, draftFilters.sggCd, scope.regionCode]);
|
|
|
|
const fetchSummary = useCallback(
|
|
async (nextFilters: PipeAnalysisSummaryFilters) => {
|
|
const params = new URLSearchParams({
|
|
analysisType: nextFilters.analysisType,
|
|
});
|
|
|
|
if (nextFilters.ctpvCd !== "0") params.set("ctpvCd", nextFilters.ctpvCd);
|
|
if (nextFilters.sggCd !== "0") params.set("sggCd", nextFilters.sggCd);
|
|
if (nextFilters.swtrmtZoneNm !== "0") params.set("swtrmtZoneNm", nextFilters.swtrmtZoneNm);
|
|
if (nextFilters.scopeRegionCode) params.set("scopeRegionCode", nextFilters.scopeRegionCode);
|
|
|
|
const response = (await getData(
|
|
`v1/fclts/ppl-analysis/summaries?${params.toString()}`,
|
|
)) as PipeAnalysisSummaryResponse;
|
|
|
|
const nextMaterialColumns = Array.isArray(response?.materialColumns)
|
|
? response.materialColumns
|
|
: [];
|
|
setMaterialColumns(nextMaterialColumns);
|
|
setRows(buildPipeAnalysisSummaryViewRows(response, ctpv, sgg));
|
|
},
|
|
[ctpv, sgg],
|
|
);
|
|
|
|
const handleSearch = useCallback(
|
|
async (nextFilters = draftFilters) => {
|
|
setIsLoading(true);
|
|
try {
|
|
const scopedFilters = applyScopeToSummaryFilters(nextFilters, scope);
|
|
setDraftFilters(scopedFilters);
|
|
setAppliedFilters(scopedFilters);
|
|
setSummaryCurrentPage(1);
|
|
await fetchSummary(scopedFilters);
|
|
} catch (error) {
|
|
console.error("관로분석 집계 조회 중 오류:", error);
|
|
alert(resolveErrorMessage(error, "관로분석 집계 조회 중 오류가 발생했습니다."));
|
|
setRows([]);
|
|
setMaterialColumns([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
},
|
|
[draftFilters, fetchSummary, scope],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (initializedRef.current) return;
|
|
if (ctpv.length === 0 || sgg.length === 0) return;
|
|
|
|
initializedRef.current = true;
|
|
void handleSearch(applyScopeToSummaryFilters(draftFilters, scope));
|
|
}, [ctpv.length, draftFilters, handleSearch, scope, sgg.length]);
|
|
|
|
const handleDownload = async () => {
|
|
if (rows.length === 0) {
|
|
alert("다운로드할 데이터가 없습니다.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const exportHeadList = buildPipeAnalysisSummaryHeadList(
|
|
appliedFilters.analysisType,
|
|
materialColumns,
|
|
);
|
|
const headerRange = `A1:${toExcelColumnName(exportHeadList.length)}1`;
|
|
const mergeRanges = appliedFilters.analysisType === "YEAR_MATERIAL"
|
|
? (() => {
|
|
const ranges: string[] = [];
|
|
let currentRow = 2;
|
|
|
|
orderedYearMaterialGroups.forEach((group) => {
|
|
const rowSpan = group.rows.length;
|
|
if (rowSpan > 1) {
|
|
["A", "B", "C", "D", "E"].forEach((column) => {
|
|
ranges.push(`${column}${currentRow}:${column}${currentRow + rowSpan - 1}`);
|
|
});
|
|
}
|
|
currentRow += rowSpan;
|
|
});
|
|
|
|
return ranges;
|
|
})()
|
|
: [];
|
|
const exportRows = appliedFilters.analysisType === "YEAR_MATERIAL"
|
|
? orderedYearMaterialGroups.flatMap((group) =>
|
|
group.rows.map((yearRow, rowIndex) => {
|
|
const exportRow: Record<string, unknown> = {
|
|
rowNo: rowIndex === 0 ? group.rowNo : "",
|
|
ctpvNm: rowIndex === 0 ? group.ctpvNm : "",
|
|
sggNm: rowIndex === 0 ? group.sggNm : "",
|
|
swtrmtZoneNm: rowIndex === 0 ? group.swtrmtZoneNm : "",
|
|
totalLengthKm: rowIndex === 0 ? group.totalLengthKm : "",
|
|
elapsedYearLabel: yearRow.elapsedYearLabel,
|
|
};
|
|
|
|
materialColumns.forEach((column) => {
|
|
exportRow[column.key] = yearRow.metrics[column.key] ?? 0;
|
|
});
|
|
|
|
return exportRow;
|
|
}),
|
|
)
|
|
: sortedRows.map((row) => {
|
|
const exportRow: Record<string, unknown> = {};
|
|
exportHeadList.forEach((head) => {
|
|
exportRow[head.id] = row[head.id];
|
|
});
|
|
return exportRow;
|
|
});
|
|
await downloadExcelWithKoreanHeaders({
|
|
data: exportRows as unknown as JSON[],
|
|
fileName: `${SUMMARY_FILE_PREFIX[appliedFilters.analysisType]}_${buildPipeAnalysisFileDate()}`,
|
|
sheetNameArray: ["관로분석 현황"],
|
|
headerRange,
|
|
headerMapping: buildPipeAnalysisSummaryDownloadHeaders(appliedFilters.analysisType, materialColumns),
|
|
mergeRanges,
|
|
});
|
|
} catch (error) {
|
|
console.error("관로분석 집계 다운로드 중 오류:", error);
|
|
alert(resolveErrorMessage(error, "관로분석 집계 다운로드 중 오류가 발생했습니다."));
|
|
}
|
|
};
|
|
|
|
const handleCellClick = (index?: number, columnId?: string) => {
|
|
if (typeof index !== "number" || !columnId) return;
|
|
const row = pagedRows[index];
|
|
if (!row) return;
|
|
if (row.__isNoZone || row.__isSummaryTotal) return;
|
|
const cellFilter = row.__cellFilters[columnId];
|
|
if (!cellFilter) return;
|
|
|
|
openWindowPopup("pipeAnalysisDetailPopup", {
|
|
analysisType: appliedFilters.analysisType,
|
|
ctpvCd: String(row.__base.ctpvCd || appliedFilters.ctpvCd),
|
|
sggCd: String(row.__base.sggCd || appliedFilters.sggCd || "0"),
|
|
swtrmtZoneNm: String(row.__base.swtrmtZoneNm || appliedFilters.swtrmtZoneNm),
|
|
elapsedYearBucket: cellFilter.elapsedYearBucket ?? "",
|
|
materialCode: cellFilter.materialCode ?? "",
|
|
scopeRegionCode: scope.regionCode,
|
|
});
|
|
};
|
|
|
|
const handleYearMaterialCellClick = (
|
|
group: PipeAnalysisYearMaterialGroup,
|
|
row: PipeAnalysisYearMaterialGroup["rows"][number],
|
|
columnId: string,
|
|
) => {
|
|
const cellFilter = row.cellFilters[columnId];
|
|
if (!cellFilter) return;
|
|
|
|
openWindowPopup("pipeAnalysisDetailPopup", {
|
|
analysisType: appliedFilters.analysisType,
|
|
ctpvCd: String(group.base.ctpvCd || appliedFilters.ctpvCd),
|
|
sggCd: String(group.base.sggCd || appliedFilters.sggCd || "0"),
|
|
swtrmtZoneNm: String(group.base.swtrmtZoneNm || appliedFilters.swtrmtZoneNm),
|
|
elapsedYearBucket: cellFilter.elapsedYearBucket ?? "",
|
|
materialCode: cellFilter.materialCode ?? "",
|
|
scopeRegionCode: scope.regionCode,
|
|
});
|
|
};
|
|
|
|
const headList = useMemo(
|
|
() => buildPipeAnalysisSummaryHeadList(appliedFilters.analysisType, materialColumns).map((head) => ({ ...head, sortable: true })),
|
|
[appliedFilters.analysisType, materialColumns],
|
|
);
|
|
|
|
const sortedRows = useMemo(
|
|
() => sortSummaryRows(rows, sort),
|
|
[rows, sort],
|
|
);
|
|
|
|
const yearMaterialGroups = useMemo(
|
|
() => appliedFilters.analysisType === "YEAR_MATERIAL"
|
|
? buildPipeAnalysisYearMaterialGroups(rows, materialColumns)
|
|
: [],
|
|
[appliedFilters.analysisType, materialColumns, rows],
|
|
);
|
|
const sortedYearMaterialGroups = useMemo(
|
|
() => sortYearMaterialGroups(yearMaterialGroups, sort),
|
|
[sort, yearMaterialGroups],
|
|
);
|
|
const orderedYearMaterialGroups = useMemo(
|
|
() => sortedYearMaterialGroups.map((group, index) => ({
|
|
...group,
|
|
rowNo: sort?.key === "rowNo" && sort.direction === "desc"
|
|
? sortedYearMaterialGroups.length - index
|
|
: index + 1,
|
|
})),
|
|
[sort, sortedYearMaterialGroups],
|
|
);
|
|
|
|
const useHorizontalScrollTable = appliedFilters.analysisType !== "YEAR";
|
|
const summaryPageOffset = (summaryCurrentPage - 1) * PIPE_ANALYSIS_SUMMARY_PAGE_SIZE;
|
|
const pagedRows = useMemo(
|
|
() =>
|
|
appliedFilters.analysisType === "YEAR_MATERIAL"
|
|
? rows
|
|
: sortedRows.slice(summaryPageOffset, summaryPageOffset + PIPE_ANALYSIS_SUMMARY_PAGE_SIZE),
|
|
[appliedFilters.analysisType, rows, sortedRows, summaryPageOffset],
|
|
);
|
|
const visibleYearMaterialGroups = useMemo(
|
|
() =>
|
|
orderedYearMaterialGroups.slice(
|
|
summaryPageOffset,
|
|
summaryPageOffset + PIPE_ANALYSIS_SUMMARY_PAGE_SIZE,
|
|
),
|
|
[orderedYearMaterialGroups, summaryPageOffset],
|
|
);
|
|
const summaryTotalCount = appliedFilters.analysisType === "YEAR_MATERIAL"
|
|
? orderedYearMaterialGroups.length
|
|
: rows.length;
|
|
|
|
const sggOptions = useMemo(
|
|
() =>
|
|
draftFilters.ctpvCd === "0"
|
|
? sgg
|
|
: sgg.filter((item) => String(item.val).startsWith(draftFilters.ctpvCd.slice(0, 2))),
|
|
[draftFilters.ctpvCd, sgg],
|
|
);
|
|
|
|
return (
|
|
<main className="sn-popup-page">
|
|
<LoadingOverlay isLoading={isLoading} />
|
|
<header className="sn-popup-header">
|
|
<h1 className="sn-popup-title">관로분석</h1>
|
|
</header>
|
|
|
|
<section className="sn-popup-section overflow-y-auto">
|
|
<div className="grid grid-cols-1 gap-3 border-b border-border bg-secondary pb-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_auto] md:items-end">
|
|
<label className="block">
|
|
<div className="mb-1 text-[1.05rem] font-medium text-label">시도</div>
|
|
<select
|
|
className="sn-popup-input !text-[1.1rem]"
|
|
value={draftFilters.ctpvCd}
|
|
disabled={scope.ctpvLocked}
|
|
onChange={(event) =>
|
|
setDraftFilters((current) => ({
|
|
...current,
|
|
ctpvCd: event.target.value,
|
|
sggCd: "0",
|
|
swtrmtZoneNm: "0",
|
|
}))
|
|
}
|
|
>
|
|
<option value="0">전체</option>
|
|
{ctpv.map((item) => (
|
|
<option key={item.val} value={String(item.val)}>
|
|
{item.text}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<div className="mb-1 text-[1.05rem] font-medium text-label">시군구</div>
|
|
<select
|
|
className="sn-popup-input !text-[1.1rem]"
|
|
value={draftFilters.sggCd}
|
|
disabled={scope.sggLocked}
|
|
onChange={(event) =>
|
|
setDraftFilters((current) => ({
|
|
...current,
|
|
sggCd: event.target.value,
|
|
swtrmtZoneNm: "0",
|
|
}))
|
|
}
|
|
>
|
|
<option value="0">전체</option>
|
|
{sggOptions.map((item) => (
|
|
<option key={item.val} value={String(item.val)}>
|
|
{item.text}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<div className="mb-1 text-[1.05rem] font-medium text-label">처리구역</div>
|
|
<select
|
|
className="sn-popup-input !text-[1.1rem]"
|
|
value={draftFilters.swtrmtZoneNm}
|
|
onChange={(event) =>
|
|
setDraftFilters((current) => ({
|
|
...current,
|
|
swtrmtZoneNm: event.target.value,
|
|
}))
|
|
}
|
|
>
|
|
{zoneOptions.map((item) => (
|
|
<option key={item.value} value={item.value}>
|
|
{item.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<div className="mb-1 text-[1.05rem] font-medium text-label">분석구분</div>
|
|
<select
|
|
className="sn-popup-input !text-[1.1rem]"
|
|
value={draftFilters.analysisType}
|
|
onChange={(event) =>
|
|
setDraftFilters((current) => ({
|
|
...current,
|
|
analysisType: event.target.value as PipeAnalysisSummaryFilters["analysisType"],
|
|
}))
|
|
}
|
|
>
|
|
{PIPE_ANALYSIS_TYPE_OPTIONS.map((item) => (
|
|
<option key={item.value} value={item.value}>
|
|
{item.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<div className="flex flex-wrap justify-end gap-2 md:pr-3">
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
color="primary"
|
|
className="h-10 px-5 !text-[1.1rem]"
|
|
onClick={() => void handleSearch()}
|
|
>
|
|
조회
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
color="green"
|
|
className="h-10 px-5 !text-[1.1rem]"
|
|
onClick={() => void handleDownload()}
|
|
>
|
|
다운로드
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{appliedFilters.analysisType === "YEAR_MATERIAL" ? (
|
|
<>
|
|
<div className="card-header">
|
|
<h3 className="text-2xl font-medium text-label">
|
|
{resolvePipeAnalysisTypeLabel(appliedFilters.analysisType)} 관로현황 {summaryTotalCount.toLocaleString("ko-KR")}건
|
|
</h3>
|
|
</div>
|
|
<div className="card !p-0r !m-[1.2rem] overflow-x-auto">
|
|
<Table variant="secondary" className="w-max min-w-full caption-bottom text-sm">
|
|
<TableHeader className="font-bold">
|
|
<TableRow>
|
|
{headList.map((head) => (
|
|
<TableHead
|
|
key={head.id}
|
|
className={`border border-border bg-secondary px-4 py-[1.2rem] text-[1.3rem] font-medium text-label ${
|
|
head.align === "right" ? "text-right" : "text-center"
|
|
}`}
|
|
aria-sort={
|
|
head.id === "elapsedYearLabel"
|
|
? undefined
|
|
: sort?.key === head.id
|
|
? sort.direction === "asc"
|
|
? "ascending"
|
|
: "descending"
|
|
: "none"
|
|
}
|
|
>
|
|
{!YEAR_MATERIAL_SORTABLE_KEYS.has(head.id) ? (
|
|
head.title
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className="inline-flex w-full items-center justify-center gap-1.5"
|
|
aria-label={`${head.title} 정렬`}
|
|
onClick={() => {
|
|
if (sort?.key !== head.id) {
|
|
setSort({ key: head.id, direction: "asc" });
|
|
setSummaryCurrentPage(1);
|
|
return;
|
|
}
|
|
if (sort.direction === "asc") {
|
|
setSort({ key: head.id, direction: "desc" });
|
|
setSummaryCurrentPage(1);
|
|
return;
|
|
}
|
|
setSort(undefined);
|
|
setSummaryCurrentPage(1);
|
|
}}
|
|
>
|
|
<span>{head.title}</span>
|
|
{sort?.key === head.id ? (
|
|
<span
|
|
className="inline-flex min-w-2 justify-center text-[8px] leading-none text-[#1d4ed8]"
|
|
aria-hidden="true"
|
|
>
|
|
{sort.direction === "asc" ? "▲" : "▼"}
|
|
</span>
|
|
) : (
|
|
<span
|
|
className="inline-flex flex-col items-center gap-px text-[6px] leading-none text-[#475569]"
|
|
aria-hidden="true"
|
|
>
|
|
<span>▲</span>
|
|
<span>▼</span>
|
|
</span>
|
|
)}
|
|
</button>
|
|
)}
|
|
</TableHead>
|
|
))}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody className="text-center">
|
|
{visibleYearMaterialGroups.length > 0 ? (
|
|
visibleYearMaterialGroups.flatMap((group) =>
|
|
group.rows.map((yearRow, rowIndex) => (
|
|
<TableRow key={`${group.base.ctpvCd}-${group.base.sggCd}-${group.base.swtrmtZoneNm}-${yearRow.elapsedYearBucket}`}>
|
|
{rowIndex === 0 ? (
|
|
<>
|
|
<TableCell rowSpan={group.rows.length} className="border border-border px-2 py-[1.2rem] text-center text-[1.3rem]">
|
|
{group.rowNo}
|
|
</TableCell>
|
|
<TableCell rowSpan={group.rows.length} className="border border-border px-2 py-[1.2rem] text-center text-[1.3rem]">
|
|
{group.ctpvNm}
|
|
</TableCell>
|
|
<TableCell rowSpan={group.rows.length} className="border border-border px-2 py-[1.2rem] text-center text-[1.3rem]">
|
|
{group.sggNm}
|
|
</TableCell>
|
|
<TableCell rowSpan={group.rows.length} className="border border-border px-2 py-[1.2rem] text-center text-[1.3rem]">
|
|
{group.swtrmtZoneNm}
|
|
</TableCell>
|
|
<TableCell rowSpan={group.rows.length} className="border border-border px-2 py-[1.2rem] text-right text-[1.3rem]">
|
|
{formatGridNumber(group.totalLengthKm)}
|
|
</TableCell>
|
|
</>
|
|
) : null}
|
|
<TableCell className="border border-border px-2 py-[1.2rem] text-center text-[1.3rem]">
|
|
{yearRow.elapsedYearLabel}
|
|
</TableCell>
|
|
{materialColumns.map((column) => {
|
|
const value = Number(yearRow.metrics[column.key] ?? 0);
|
|
const isClickable = value > 0 && Boolean(yearRow.cellFilters[column.key]);
|
|
return (
|
|
<TableCell
|
|
key={`${group.base.ctpvCd}-${group.base.sggCd}-${group.base.swtrmtZoneNm}-${yearRow.elapsedYearBucket}-${column.key}`}
|
|
className={`border border-border px-2 py-[1.2rem] text-right text-[1.3rem] ${
|
|
isClickable ? "cursor-pointer font-semibold text-primary underline underline-offset-2" : ""
|
|
}`}
|
|
onClick={() => {
|
|
if (isClickable) {
|
|
handleYearMaterialCellClick(group, yearRow, column.key);
|
|
}
|
|
}}
|
|
>
|
|
{formatGridNumber(value)}
|
|
</TableCell>
|
|
);
|
|
})}
|
|
</TableRow>
|
|
))
|
|
)
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={headList.length} className="h-24 border border-border text-center text-[1.3rem] text-label">
|
|
조회 결과가 없습니다.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
{orderedYearMaterialGroups.length > PIPE_ANALYSIS_SUMMARY_PAGE_SIZE ? (
|
|
<div className="flex justify-center">
|
|
<UiPageNations
|
|
total={orderedYearMaterialGroups.length}
|
|
cur={summaryCurrentPage}
|
|
pageSize={PIPE_ANALYSIS_SUMMARY_PAGE_SIZE}
|
|
onPageChange={setSummaryCurrentPage}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</>
|
|
) : null}
|
|
|
|
<UiTable<PipeAnalysisSummaryViewRow>
|
|
className={appliedFilters.analysisType === "YEAR_MATERIAL"
|
|
? "hidden"
|
|
: useHorizontalScrollTable
|
|
? "overflow-x-auto overflow-y-visible [&_table]:min-w-full [&_table]:w-max"
|
|
: ""}
|
|
headName={
|
|
appliedFilters.analysisType === "YEAR_MATERIAL"
|
|
? `경과년도&재질 관로현황 ${summaryTotalCount.toLocaleString("ko-KR")}건`
|
|
: `관로분석현황 ${summaryTotalCount.toLocaleString("ko-KR")}건`
|
|
}
|
|
tableData={pagedRows}
|
|
headlist={headList}
|
|
pageSize={PIPE_ANALYSIS_SUMMARY_PAGE_SIZE}
|
|
total={rows.length}
|
|
cellClick={(index, columnId) =>
|
|
handleCellClick(
|
|
typeof index === "number" ? index + summaryPageOffset : index,
|
|
columnId,
|
|
)}
|
|
onPageChange={setSummaryCurrentPage}
|
|
curPage={summaryCurrentPage}
|
|
widthFixed={!useHorizontalScrollTable}
|
|
isHideCount={true}
|
|
sort={sort}
|
|
onSortChange={(nextSort) => {
|
|
setSort(nextSort);
|
|
setSummaryCurrentPage(1);
|
|
}}
|
|
cellClassName={(row, columnId) =>
|
|
!row.__isNoZone && row.__cellFilters[columnId]
|
|
? "cursor-pointer font-semibold text-blue-700 underline underline-offset-2"
|
|
: ""
|
|
}
|
|
/>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|