sn: 시도별 노후관로현황

This commit is contained in:
2026-07-27 17:51:33 +09:00
parent e8e38310ee
commit 09b5cc3798
12 changed files with 512 additions and 1 deletions
@@ -0,0 +1,209 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Button, Input } from "@common/components";
import { UiTable } from "@common/business_components/ui";
import { getRawJson } from "@/components/lib/api";
import LoadingOverlay from "@/components/ui/LoadingBar";
import {
AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS,
AGED_PIPE_STATUS_BY_CTPV_PAGE_SIZE,
buildAgedPipeStatusByCtpvHeadList,
buildAgedPipeStatusYearOptions,
normalizeAgedPipeStatusByCtpvResponse,
normalizeAgedPipeStatusByCtpvFilters,
toAgedPipeStatusByCtpvRows,
validateAgedPipeStatusByCtpvFilters,
type AgedPipeStatusByCtpvFilters,
type AgedPipeStatusByCtpvResponse,
type AgedPipeStatusByCtpvRow,
} from "./agedPipeStatusByCtpvUtils";
import { downloadAgedPipeStatusByCtpvCsv } from "./agedPipeStatusByCtpvExport";
export default function AgedPipeStatusByCtpvPopupPage() {
// 조회조건 입력 상태다. 기본값 30년, 최근 연도 기준 단년 조회로 시작한다.
const [filters, setFilters] = useState<AgedPipeStatusByCtpvFilters>(() =>
normalizeAgedPipeStatusByCtpvFilters({
elapsedYears: AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS,
}),
);
// 서버 응답을 화면 그리드에 맞게 평탄화한 row 목록이다.
const [rows, setRows] = useState<AgedPipeStatusByCtpvRow[]>([]);
// 동적 컬럼 생성을 위한 기준년도 목록이다.
const [years, setYears] = useState<number[]>([]);
// 조회/다운로드 중 로딩 overlay를 제어한다.
const [isLoading, setIsLoading] = useState(false);
// 사용자에게 현재 다운로드 상태를 보여주는 진행 문구다.
const [downloadProgressText, setDownloadProgressText] = useState("");
const yearOptions = useMemo(() => buildAgedPipeStatusYearOptions(), []);
const headList = useMemo(() => buildAgedPipeStatusByCtpvHeadList(years), [years]);
const loadRows = useCallback(async (nextFilters: AgedPipeStatusByCtpvFilters) => {
setIsLoading(true);
try {
const normalized = normalizeAgedPipeStatusByCtpvFilters(nextFilters);
const queryParams = new URLSearchParams({
startYear: String(normalized.startYear),
endYear: String(normalized.endYear),
elapsedYears: String(normalized.elapsedYears),
});
const rawResponse = await getRawJson(
`v1/swagePpl/aged-status-by-ctpv?${queryParams.toString()}`,
);
const response = normalizeAgedPipeStatusByCtpvResponse(rawResponse) as AgedPipeStatusByCtpvResponse;
setYears(Array.isArray(response?.years) ? response.years : []);
setRows(toAgedPipeStatusByCtpvRows(response));
} catch (error) {
console.error("시도별 노후관로현황 조회 중 오류:", error);
setYears([]);
setRows([]);
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
void loadRows(filters);
}, [filters, loadRows]);
const handleSearch = async () => {
const normalized = normalizeAgedPipeStatusByCtpvFilters(filters);
if (!validateAgedPipeStatusByCtpvFilters(normalized)) {
alert("기준년도는 최대 3년 범위 내에서 선택하고, 경과년도는 1~100 사이로 입력해주세요.");
return;
}
setFilters(normalized);
await loadRows(normalized);
};
const handleDownload = async () => {
try {
setIsLoading(true);
setDownloadProgressText("시도별 노후관로현황 CSV를 준비하고 있습니다.");
await downloadAgedPipeStatusByCtpvCsv(filters);
setDownloadProgressText("");
} catch (error) {
console.error("시도별 노후관로현황 다운로드 중 오류:", error);
setDownloadProgressText("");
alert(
error instanceof Error
? error.message
: "시도별 노후관로현황 CSV 다운로드 중 오류가 발생했습니다.",
);
} finally {
setIsLoading(false);
}
};
return (
<main className="sn-popup-page w-full max-w-full overflow-x-hidden overflow-y-hidden">
<LoadingOverlay isLoading={isLoading} />
<header className="sn-popup-header">
<h1 className="sn-popup-title">시도별 노후관로현황</h1>
<div />
</header>
{downloadProgressText ? (
<div className="sn-popup-success-bar">
{downloadProgressText}
</div>
) : null}
<section className="flex min-h-0 flex-1 flex-col overflow-x-hidden px-4 pb-3 pt-3">
<div className="sn-popup-filter-panel-compact grid grid-cols-1 gap-3 md:grid-cols-[160px_160px_160px_auto] md:items-end">
<label className="sn-popup-field">
<div className="sn-popup-field-label">기준년도 시작</div>
<select
className="sn-popup-input !text-[1.1rem]"
value={filters.startYear}
onChange={(event) =>
setFilters((current) =>
normalizeAgedPipeStatusByCtpvFilters({
...current,
startYear: Number(event.target.value),
}),
)
}
>
{yearOptions.map((year) => (
<option key={year} value={year}>{year}</option>
))}
</select>
</label>
<label className="sn-popup-field">
<div className="sn-popup-field-label">기준년도 종료</div>
<select
className="sn-popup-input !text-[1.1rem]"
value={filters.endYear}
onChange={(event) =>
setFilters((current) => ({
...current,
endYear: Number(event.target.value),
}))
}
>
{yearOptions.map((year) => (
<option key={year} value={year}>{year}</option>
))}
</select>
</label>
<label className="sn-popup-field">
<div className="sn-popup-field-label">노후관로 기준(년)</div>
<Input
type="text"
inputMode="numeric"
value={String(filters.elapsedYears)}
onChange={(event) =>
setFilters((current) => ({
...current,
elapsedYears: Number(event.target.value.replace(/[^0-9]/g, "")),
}))
}
className="sn-popup-input !text-[1.1rem]"
onKeyDown={(event) => {
if (event.key === "Enter") void handleSearch();
}}
/>
</label>
<div className="sn-popup-actions md:pr-3">
<Button
onClick={handleSearch}
color="primary"
className="sn-popup-primary-button !text-[1.1rem]"
>
조회
</Button>
<Button
onClick={handleDownload}
color="primary"
className="sn-popup-primary-button !text-[1.1rem]"
>
엑셀 다운로드
</Button>
</div>
</div>
<div className="min-h-0 min-w-0 flex-1 overflow-x-auto overflow-y-auto pt-2">
<UiTable<AgedPipeStatusByCtpvRow>
headName={<>시도별 노후관로현황 조회 결과 총 {rows.length.toLocaleString()}건</>}
tableData={rows}
headlist={headList}
pageSize={AGED_PIPE_STATUS_BY_CTPV_PAGE_SIZE}
total={rows.length}
cellClick={() => undefined}
onPageChange={() => undefined}
curPage={1}
index={-1}
widthFixed={true}
isHideCount={true}
cellClassName={() => "text-[1.2rem]"}
/>
</div>
</section>
</main>
);
}
@@ -0,0 +1,25 @@
"use client";
import { postBlobData } from "@/components/lib/api";
import { fileDownload } from "@/components/lib/utils/fileDownload";
import {
normalizeAgedPipeStatusByCtpvFilters,
type AgedPipeStatusByCtpvFilters,
} from "./agedPipeStatusByCtpvUtils";
/** 시도별 노후관로현황 CSV 다운로드를 요청한다. */
export const downloadAgedPipeStatusByCtpvCsv = async (
filters: AgedPipeStatusByCtpvFilters,
) => {
const normalizedFilters = normalizeAgedPipeStatusByCtpvFilters(filters);
const { blob, fileName } = await postBlobData("v1/swagePpl/aged-status-by-ctpv/exportJobs/download", {
startYear: normalizedFilters.startYear,
endYear: normalizedFilters.endYear,
elapsedYears: normalizedFilters.elapsedYears,
format: "csv",
});
const resolvedFileName = fileName || "시도별_노후관로현황.csv";
fileDownload(blob, resolvedFileName);
return resolvedFileName;
};
@@ -0,0 +1,149 @@
"use client";
import type { UiHeadProps } from "@common/business_components/ui";
export const AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS = 30;
export const AGED_PIPE_STATUS_BY_CTPV_MAX_YEAR_SPAN = 3;
export const AGED_PIPE_STATUS_BY_CTPV_PAGE_SIZE = 50;
export type AgedPipeStatusByCtpvFilters = {
startYear: number;
endYear: number;
elapsedYears: number;
};
export type AgedPipeStatusByCtpvYearStat = {
year: number;
totalLengthKm: number;
agedLengthKm: number;
agedRate: number;
};
export type AgedPipeStatusByCtpvRow = {
rowNumber: number;
ctpvCd: string;
ctpvNm: string;
yearStats: AgedPipeStatusByCtpvYearStat[];
[key: string]: string | number | AgedPipeStatusByCtpvYearStat[];
};
export type AgedPipeStatusByCtpvResponse = {
startYear: number;
endYear: number;
elapsedYears: number;
years: number[];
rows: Array<{
ctpvCd: string;
ctpvNm: string;
yearStats: AgedPipeStatusByCtpvYearStat[];
}>;
};
export const normalizeAgedPipeStatusByCtpvResponse = (
response: Partial<AgedPipeStatusByCtpvResponse> | null | undefined,
): AgedPipeStatusByCtpvResponse => ({
startYear: Number(response?.startYear ?? 0),
endYear: Number(response?.endYear ?? 0),
elapsedYears: Number(response?.elapsedYears ?? AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS),
years: Array.isArray(response?.years) ? response.years.map((year) => Number(year)) : [],
rows: Array.isArray(response?.rows) ? response.rows : [],
});
export const buildAgedPipeStatusYearOptions = () => {
const currentYear = new Date().getFullYear();
return Array.from({ length: 15 }, (_, index) => currentYear - index);
};
export const normalizeAgedPipeStatusByCtpvFilters = (
filters: Partial<AgedPipeStatusByCtpvFilters>,
): AgedPipeStatusByCtpvFilters => {
const currentYear = new Date().getFullYear();
let startYear = Math.trunc(Number(filters.startYear ?? currentYear));
let endYear = Math.trunc(Number(filters.endYear ?? startYear));
let elapsedYears = Math.trunc(
Number(filters.elapsedYears ?? AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS),
);
if (!Number.isFinite(startYear)) startYear = currentYear;
if (!Number.isFinite(endYear)) endYear = startYear;
if (!Number.isFinite(elapsedYears) || elapsedYears < 1 || elapsedYears > 100) {
elapsedYears = AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS;
}
if (startYear > endYear) {
const swap = startYear;
startYear = endYear;
endYear = swap;
}
if (endYear - startYear + 1 > AGED_PIPE_STATUS_BY_CTPV_MAX_YEAR_SPAN) {
endYear = startYear + (AGED_PIPE_STATUS_BY_CTPV_MAX_YEAR_SPAN - 1);
}
return {
startYear,
endYear,
elapsedYears,
};
};
export const validateAgedPipeStatusByCtpvFilters = (filters: AgedPipeStatusByCtpvFilters) => {
if (filters.startYear > filters.endYear) return false;
if (filters.endYear - filters.startYear + 1 > AGED_PIPE_STATUS_BY_CTPV_MAX_YEAR_SPAN) return false;
return filters.elapsedYears >= 1 && filters.elapsedYears <= 100;
};
const formatMetric = (value: number) => Number(value ?? 0).toFixed(3);
export const toAgedPipeStatusByCtpvRows = (
response: Partial<AgedPipeStatusByCtpvResponse> | null | undefined,
): AgedPipeStatusByCtpvRow[] =>
normalizeAgedPipeStatusByCtpvResponse(response).rows.map((row, index) => {
const displayRow: AgedPipeStatusByCtpvRow = {
rowNumber: index + 1,
ctpvCd: String(row.ctpvCd ?? "").trim(),
ctpvNm: String(row.ctpvNm ?? "").trim(),
yearStats: row.yearStats ?? [],
};
(row.yearStats ?? []).forEach((yearStat) => {
displayRow[`year${yearStat.year}TotalLengthKm`] = formatMetric(Number(yearStat.totalLengthKm ?? 0));
displayRow[`year${yearStat.year}AgedLengthKm`] = formatMetric(Number(yearStat.agedLengthKm ?? 0));
displayRow[`year${yearStat.year}AgedRate`] = formatMetric(Number(yearStat.agedRate ?? 0));
});
return displayRow;
});
export const buildAgedPipeStatusByCtpvHeadList = (years: number[]): UiHeadProps[] => {
const heads: UiHeadProps[] = [
{ id: "rowNumber", title: "NO", widthPercent: 6, sortable: false },
{ id: "ctpvNm", title: "시도", widthPercent: 14, sortable: false },
];
years.forEach((year) => {
heads.push(
{
id: `year${year}TotalLengthKm`,
title: `${year} 전체관로연장(km)`,
widthPercent: 13,
sortable: false,
align: "right",
},
{
id: `year${year}AgedLengthKm`,
title: `${year} 노후관로연장(km)`,
widthPercent: 13,
sortable: false,
align: "right",
},
{
id: `year${year}AgedRate`,
title: `${year} 노후화율(%)`,
widthPercent: 11,
sortable: false,
align: "right",
},
);
});
return heads;
};
@@ -61,6 +61,7 @@ const ANALYSIS_POPUP_ITEMS: Array<{
{ key: "infoSearch", label: "정보검색", icon: Search },
{ key: "pipeAnalysisPopup", label: "관로분석", icon: ChartColumnBig },
{ key: "floodPredictionMapPopup", label: "침수예측지도", icon: Waves },
{ key: "agedPipeStatusByCtpvPopup", label: "시도별 노후관로현황", icon: ChartColumnBig },
{ key: "gisEditPopup", label: "GIS 편집", icon: PencilRuler },
{ key: "gisManagementPopup", label: "GIS 관리", icon: Settings2 },
{ key: "flowRiskPopup", label: "통수 위험", icon: TriangleAlert },
@@ -8,9 +8,11 @@ import { Button, Input } from "@common/components";
import { UiTable, downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
import type { UiTableSort } from "@common/business_components/ui";
import { getRawJson } from "@/components/lib/api";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
import useStore from "@/store/useStore";
import LoadingOverlay from "@/components/ui/LoadingBar";
import {
FLOOD_HISTORY_LAYER_TYPE,
FLDNG_HSTRY_DEFAULT_FILTERS,
FLDNG_HSTRY_DOWNLOAD_BATCH_SIZE,
FLDNG_HSTRY_DOWNLOAD_HEADERS,
@@ -46,6 +48,15 @@ const resolveFldngHstryApiSort = (sort?: UiTableSort) => {
return { key: sort.key, direction: sort.direction } as UiTableSort;
};
const resolveFldngHstryMapFeatureFid = (row: FldngHstryTableRow) => {
const fid = String(row.fid ?? "").trim();
if (fid.startsWith(`${FLOOD_HISTORY_LAYER_TYPE}.`)) return fid;
if (fid) return `${FLOOD_HISTORY_LAYER_TYPE}.${fid}`;
const mapFid = String(row.fldngMapFid ?? "").trim();
return mapFid || "";
};
const buildDownloadRows = (rows: FldngHstryTableRow[]) =>
rows.map((row) => ({
rowNo: row.rowNo,
@@ -200,6 +211,22 @@ export default function FldngHstryPopupPage() {
if (!row) return;
setSelectedIndex(index);
const mapFeatureFid = resolveFldngHstryMapFeatureFid(row);
if (mapFeatureFid) {
window.opener?.postMessage(
{
type: WINDOW_POPUP_MESSAGE_TYPE.MAP_SEARCH_SELECT_FEATURE,
payload: {
layerNm: FLOOD_HISTORY_LAYER_TYPE,
fid: mapFeatureFid,
},
},
window.location.origin,
);
return;
}
window.opener?.postMessage(
{
type: FLDNG_HSTRY_ROW_SELECT_MESSAGE,
@@ -39,6 +39,7 @@ import LoadingOverlay from '@/components/ui/LoadingBar';
import { searchUpperPpls } from "./smh";
import { getElapsedYearsDisplayText, isInvalidInstallYmd } from "../agedPipe/agedPipeUtils";
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
import useStore from "@/store/useStore";
const linkedFacilityHighlightStyle = new Style({
image: new CircleStyle({
@@ -92,6 +93,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
const linkedFacilityFeatureRef = useRef<any>(null);
const linkedFacilityOriginalStyleRef = useRef<any>(null);
const [mngInstOptions, setMngInstOptions] = useState<MngInstOption[]>([]);
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
// 🎯 관로관리번호 추출
const pplMngNo = featureData?.values_?.fclt_mng_no || featureData?.fcltMngNo || "";
@@ -149,6 +151,11 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
};
}, [isPipeFeature]);
useEffect(() => {
if (ctpv.length === 0) void fetchCtpv();
if (sgg.length === 0) void fetchSgg();
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
const mngInstNameMap = useMemo(() => {
const map = new Map<string, string>();
mngInstOptions.forEach((option) => {
@@ -169,6 +176,24 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
};
};
const withResolvedRegionNames = (data: any) => {
if (!data || typeof data !== "object") return data;
const stdgCd = String(data.stdg_cd ?? data.stdgCd ?? "").trim();
const ctpvCd = String(data.ctpv_cd ?? data.ctpvCd ?? stdgCd.slice(0, 2)).trim();
const sggCd = String(data.sgg_cd ?? data.sggCd ?? stdgCd.slice(0, 5)).trim();
const resolvedCtpvNm = ctpv.find((item) => String(item.val).trim() === ctpvCd)?.text ?? "";
const resolvedSggNm = sgg.find((item) => String(item.val).trim() === sggCd)?.text ?? "";
return {
...data,
ctpv_nm: data.ctpv_nm ?? data.ctpvNm ?? resolvedCtpvNm,
ctpvNm: data.ctpvNm ?? data.ctpv_nm ?? resolvedCtpvNm,
sgg_nm: data.sgg_nm ?? data.sggNm ?? resolvedSggNm,
sggNm: data.sggNm ?? data.sgg_nm ?? resolvedSggNm,
};
};
const hasDetailValue = (value: unknown) => {
if (value === null || value === undefined) return false;
if (typeof value === "string") return value.trim() !== "";
@@ -446,7 +471,10 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
}
// 🎯 기본 데이터 포맷팅
const formattedData = withMngInstName(formatDisplayData(data));
const normalizedRegionData = selectedLayerName === '침수흔적도'
? withResolvedRegionNames(data)
: data;
const formattedData = withMngInstName(formatDisplayData(normalizedRegionData));
if (selectedLayerName === '하수관로') {
const installYmd = data.instl_ymd ?? data.instlYmd;
formattedData.elapsedYears = getElapsedYearsDisplayText(installYmd);
@@ -0,0 +1,10 @@
# SFR-011-001 시도별 노후관로현황 FE 검증 기록
## 1. 정적 확인
- 분석도구 메뉴, popup definition, popup registry, popup page 연결을 코드 기준으로 확인했다.
- 동적 연도 컬럼 생성 로직과 CSV 다운로드 호출 경로를 확인했다.
## 2. 실행 확인
- `pnpm --filter sn build`
- 결과: 시간 제한 내 완료되지 않아 최종 성공/실패를 확정하지 못했다.
- 비고: 이 저장소는 기존에도 FE build가 환경 영향으로 지연 또는 `EMFILE` 이슈가 있었으므로 사용자 로컬 실행 확인이 추가로 필요하다.
@@ -0,0 +1,19 @@
# SFR-011-001 시도별 노후관로현황 FE 작업카드
## 1. 작업 목적
- 하수관망도 우측 분석도구에서 `시도별 노후관로현황` 팝업을 열고 기준년도 구간별 시도 집계를 조회한다.
## 2. 구현 범위
- `AnalysisPopupMenu`에 진입 버튼 추가
- `windowPopup` registry에 새 popup key 추가
- 전용 popup page 구현
- 기준년도 시작/종료, 경과년도 기본값 30년, CSV 다운로드 연결
## 3. 구현 기준
- 기존 `노후관로 현황` popup의 레이아웃 톤과 다운로드 UX를 최대한 재사용한다.
- 집계형 표이므로 지도 feature 선택 연동은 넣지 않는다.
- 기준년도 범위는 최대 3년까지만 허용한다.
## 4. 연동 API
- `GET /api/v1/swagePpl/aged-status-by-ctpv`
- `POST /api/v1/swagePpl/aged-status-by-ctpv/exportJobs/download`
@@ -0,0 +1,22 @@
# SFR-011-001 시도별 노후관로현황 FE 통합 구현 기록
## 1. 구현 요약
- 분석도구 메뉴에 `시도별 노후관로현황` 항목을 추가했다.
- 새 popup key `agedPipeStatusByCtpvPopup`와 registry content를 연결했다.
- 전용 popup page에서 기준년도 시작/종료, 경과년도(기본 30년), 조회, CSV 다운로드를 구현했다.
- 응답의 `years + rows.yearStats` 구조를 동적 컬럼 `UiTable` row로 변환하도록 유틸을 추가했다.
## 2. 주요 수정 파일
- `app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx`
- `src/features/windowPopup/model/popupDefinitions.ts`
- `src/features/windowPopup/registry.tsx`
- `src/features/windowPopup/ui/AgedPipeStatusByCtpvPopupContent.tsx`
- `app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage.tsx`
- `app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvUtils.ts`
- `app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvExport.ts`
## 3. 화면 동작
- popup 제목은 `시도별 노후관로현황`이다.
- 기준년도 범위는 최대 3년으로 제한한다.
- 경과년도는 기본 `30년`으로 시작하고, 후속에 사용자가 변경 가능한 입력 구조를 미리 반영했다.
- CSV 다운로드는 기존 노후관로 popup과 동일하게 `postBlobData + fileDownload` 흐름을 따른다.
@@ -141,6 +141,12 @@ export const windowPopupDefinitions = {
width: 1080,
height: 760,
},
agedPipeStatusByCtpvPopup: {
key: "agedPipeStatusByCtpvPopup",
title: "시도별 노후관로현황",
width: 1160,
height: 760,
},
fldngHstryPopup: {
key: "fldngHstryPopup",
title: "침수발생이력",
@@ -1,6 +1,7 @@
import { windowPopupDefinitions, type WindowPopupKey } from "./model/popupDefinitions";
import type { WindowPopupRegistryItem } from "./model/types";
import AgedPipePopupContent from "./ui/AgedPipePopupContent";
import AgedPipeStatusByCtpvPopupContent from "./ui/AgedPipeStatusByCtpvPopupContent";
import ExmnBusinessRegisterPopupContent from "./ui/ExmnBusinessRegisterPopupContent";
import ExmnResultRegisterPopupContent from "./ui/ExmnResultRegisterPopupContent";
import ExmnSurveyDetailPopupContent from "./ui/ExmnSurveyDetailPopupContent";
@@ -117,6 +118,10 @@ export const windowPopupRegistry = {
...windowPopupDefinitions.floodPredictionMapPopup,
component: FloodPredictionPlaceholderPopupContent,
},
agedPipeStatusByCtpvPopup: {
...windowPopupDefinitions.agedPipeStatusByCtpvPopup,
component: AgedPipeStatusByCtpvPopupContent,
},
fldngHstryPopup: {
...windowPopupDefinitions.fldngHstryPopup,
component: FldngHstryPopupContent,
@@ -0,0 +1,10 @@
import { Suspense } from "react";
import AgedPipeStatusByCtpvPopupPage from "@/app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage";
export default function AgedPipeStatusByCtpvPopupContent() {
return (
<Suspense fallback={null}>
<AgedPipeStatusByCtpvPopupPage />
</Suspense>
);
}