sn: 시도별 노후관로현황
This commit is contained in:
+209
@@ -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>
|
||||
);
|
||||
}
|
||||
+25
@@ -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;
|
||||
};
|
||||
+149
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user