diff --git a/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage.tsx new file mode 100644 index 000000000..40abd0a68 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage.tsx @@ -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(() => + normalizeAgedPipeStatusByCtpvFilters({ + elapsedYears: AGED_PIPE_STATUS_BY_CTPV_DEFAULT_ELAPSED_YEARS, + }), + ); + // 서버 응답을 화면 그리드에 맞게 평탄화한 row 목록이다. + const [rows, setRows] = useState([]); + // 동적 컬럼 생성을 위한 기준년도 목록이다. + const [years, setYears] = useState([]); + // 조회/다운로드 중 로딩 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 ( +
+ +
+

시도별 노후관로현황

+
+
+ + {downloadProgressText ? ( +
+ {downloadProgressText} +
+ ) : null} + +
+
+ + + + + + +
+ + +
+
+ +
+ + 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]"} + /> +
+
+
+ ); +} diff --git a/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvExport.ts b/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvExport.ts new file mode 100644 index 000000000..df5dbf0d0 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvExport.ts @@ -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; +}; diff --git a/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvUtils.ts b/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvUtils.ts new file mode 100644 index 000000000..6deaa1329 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/components/agedPipeStatusByCtpv/agedPipeStatusByCtpvUtils.ts @@ -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 | 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 => { + 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 | 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; +}; diff --git a/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx b/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx index 51aff4d1b..51bd9a5f1 100644 --- a/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx +++ b/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx @@ -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 }, diff --git a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx index 470a7d3c2..487ce744e 100644 --- a/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/fldngHstry/FldngHstryPopupPage.tsx @@ -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, diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx index 3a75126fa..ae9d47898 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx @@ -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(null); const linkedFacilityOriginalStyleRef = useRef(null); const [mngInstOptions, setMngInstOptions] = useState([]); + 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(); 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); diff --git a/apps/sn/docs/작업검증/SFR-011-001-시도별 노후관로현황/001-qa-SFR-011-001-시도별 노후관로현황-검증기록.md b/apps/sn/docs/작업검증/SFR-011-001-시도별 노후관로현황/001-qa-SFR-011-001-시도별 노후관로현황-검증기록.md new file mode 100644 index 000000000..b95de821c --- /dev/null +++ b/apps/sn/docs/작업검증/SFR-011-001-시도별 노후관로현황/001-qa-SFR-011-001-시도별 노후관로현황-검증기록.md @@ -0,0 +1,10 @@ +# SFR-011-001 시도별 노후관로현황 FE 검증 기록 + +## 1. 정적 확인 +- 분석도구 메뉴, popup definition, popup registry, popup page 연결을 코드 기준으로 확인했다. +- 동적 연도 컬럼 생성 로직과 CSV 다운로드 호출 경로를 확인했다. + +## 2. 실행 확인 +- `pnpm --filter sn build` + - 결과: 시간 제한 내 완료되지 않아 최종 성공/실패를 확정하지 못했다. + - 비고: 이 저장소는 기존에도 FE build가 환경 영향으로 지연 또는 `EMFILE` 이슈가 있었으므로 사용자 로컬 실행 확인이 추가로 필요하다. diff --git a/apps/sn/docs/작업계획/SFR-011-001-시도별 노후관로현황/001-fe-SFR-011-001-시도별 노후관로현황-작업카드.md b/apps/sn/docs/작업계획/SFR-011-001-시도별 노후관로현황/001-fe-SFR-011-001-시도별 노후관로현황-작업카드.md new file mode 100644 index 000000000..e8cbd094a --- /dev/null +++ b/apps/sn/docs/작업계획/SFR-011-001-시도별 노후관로현황/001-fe-SFR-011-001-시도별 노후관로현황-작업카드.md @@ -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` diff --git a/apps/sn/docs/작업내용/SFR-011-001-시도별 노후관로현황/001-fe-SFR-011-001-시도별 노후관로현황-통합구현기록.md b/apps/sn/docs/작업내용/SFR-011-001-시도별 노후관로현황/001-fe-SFR-011-001-시도별 노후관로현황-통합구현기록.md new file mode 100644 index 000000000..5b708afa5 --- /dev/null +++ b/apps/sn/docs/작업내용/SFR-011-001-시도별 노후관로현황/001-fe-SFR-011-001-시도별 노후관로현황-통합구현기록.md @@ -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` 흐름을 따른다. diff --git a/apps/sn/src/features/windowPopup/model/popupDefinitions.ts b/apps/sn/src/features/windowPopup/model/popupDefinitions.ts index 56e9a75f3..f39bacb78 100644 --- a/apps/sn/src/features/windowPopup/model/popupDefinitions.ts +++ b/apps/sn/src/features/windowPopup/model/popupDefinitions.ts @@ -141,6 +141,12 @@ export const windowPopupDefinitions = { width: 1080, height: 760, }, + agedPipeStatusByCtpvPopup: { + key: "agedPipeStatusByCtpvPopup", + title: "시도별 노후관로현황", + width: 1160, + height: 760, + }, fldngHstryPopup: { key: "fldngHstryPopup", title: "침수발생이력", diff --git a/apps/sn/src/features/windowPopup/registry.tsx b/apps/sn/src/features/windowPopup/registry.tsx index 3c7fa98cf..891290881 100644 --- a/apps/sn/src/features/windowPopup/registry.tsx +++ b/apps/sn/src/features/windowPopup/registry.tsx @@ -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, diff --git a/apps/sn/src/features/windowPopup/ui/AgedPipeStatusByCtpvPopupContent.tsx b/apps/sn/src/features/windowPopup/ui/AgedPipeStatusByCtpvPopupContent.tsx new file mode 100644 index 000000000..fe9338af8 --- /dev/null +++ b/apps/sn/src/features/windowPopup/ui/AgedPipeStatusByCtpvPopupContent.tsx @@ -0,0 +1,10 @@ +import { Suspense } from "react"; +import AgedPipeStatusByCtpvPopupPage from "@/app/(map-layout)/map/components/agedPipeStatusByCtpv/AgedPipeStatusByCtpvPopupPage"; + +export default function AgedPipeStatusByCtpvPopupContent() { + return ( + + + + ); +}