Merge branch 'iipbs/sn/dev' into iipbs/sn/dev_kic

This commit is contained in:
kiminchan
2026-07-29 13:31:01 +09:00
24 changed files with 827 additions and 147 deletions
+1 -5
View File
@@ -9,9 +9,5 @@ export default function AuthLayout({
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>{children}</body>
</html>
);
return children;
}
@@ -0,0 +1,210 @@
"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>
// 조회 결과 총 {rows.length.toLocaleString()}건
headName={<>시도별 노후관로현황 </>}
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,153 @@
"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: currentYear - 1900 + 1 }, (_, 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).toLocaleString("ko-KR", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
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 },
@@ -484,17 +484,20 @@ export const FldngWtlvMapCol = [
{ id: 'fldng_dstr_nm', title: '침수재해명' },
{ id: 'ctpv_nm', title: '시도명' },
{ id: 'sgg_nm', title: '시군구명' },
{ id: 'bgng_ymd', title: '침수시작일자' },
{ id: 'fldng_bgng_hm', title: '침수시작시분' },
{ id: 'end_ymd', title: '침수종료일자' },
{ id: 'fldng_end_hm', title: '침수종료시분' },
{ id: 'fldng_bgng_dtm', title: '침수시작일시' },
{ id: 'fldng_end_dtm', title: '침수종료일시' },
{ id: 'fldng_hr', title: '침수시간' },
{ id: 'fldng_dstrct_nm', title: '침수지구명' },
{ id: 'fldng_cs_dtl_cd', title: '침수원인상세코드' },
{ id: 'dtl_ocrn_cs_cn', title: '침수발생원인' },
{ id: 'avg_fldng_wtlv', title: '평균침수위' },
{ id: 'fldng_area', title: '침수면적' },
{ id: 'exmn_yr', title: '조사년도' },
{ id: 'dam_amt', title: '피해금액' },
{ id: 'rain_bgng_tm', title: '강우시작일시' },
{ id: 'rain_end_tm', title: '강우종료일시' },
{ id: 'rain_tot', title: '총강우량(mm)' },
{ id: 'rain_str', title: '강우강도(mm/hr)' },
];
export const FldngWtlvMapCol2 = [
{ id: 'rowNumber', title: '일련번호' },
@@ -502,10 +505,8 @@ export const FldngWtlvMapCol2 = [
{ id: 'fldngDstrNm', title: '침수재해명' },
{ id: 'ctpvNm', title: '시도' },
{ id: 'sggNm', title: '시군구' },
{ id: 'bgngYmd', title: '침수시작일자' },
{ id: 'fldngBgngHm', title: '침수시작시분' },
{ id: 'endYmd', title: '침수종료일자' },
{ id: 'fldngEndHm', title: '침수종료시분' },
{ id: 'fldngBgngDtm', title: '침수시작일시' },
{ id: 'fldngEndDtm', title: '침수종료일시' },
{ id: 'fldngHr', title: '침수시간' },
{ id: 'fldngDstrctNm', title: '침수지구명' },
{ id: 'fldngCsDtlCd', title: '침수원인상세코드' },
@@ -513,6 +514,10 @@ export const FldngWtlvMapCol2 = [
{ id: 'fldngArea', title: '침수면적' },
{ id: 'exmnYr', title: '조사년도' },
{ id: 'damAmt', title: '피해금액' },
{ id: 'rainBgngTm', title: '강우시작일시' },
{ id: 'rainEndTm', title: '강우종료일시' },
{ id: 'rainTot', title: '총강우량(mm)' },
{ id: 'rainStr', title: '강우강도(mm/hr)' },
];
export const FldngWtlvMapExcelCol = {
'fldngYr': '침수년도',
@@ -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,
@@ -27,36 +27,36 @@ export type FldngHstryDepthZone = {
};
export type FldngHstryApiItem = {
fid: string;
fldngMapFid?: string;
fldngWtlvMapSn?: number | null;
taskClCd?: string;
fldngDstrNm?: string;
exmnYr?: string;
avgFldngWtlv?: number | null;
fldngArea?: number | null;
dtlOcrnCsCn?: string;
stdgCd?: string;
ctpvCd?: string;
sggCd?: string;
dataSn?: number | null;
bgngYmd?: string;
endYmd?: string;
fldngBgngHm?: string;
fldngEndHm?: string;
fldngCsDtlCd?: string;
fldngDstrctNm?: string;
damAmt?: number | null;
fldngHr?: number | null;
fldngYr?: string;
cstrnFbctnYn?: string;
atchFileSn?: number | null;
confArea?: number | null;
confLen?: number | null;
rainBgngTm?: string | null;
rainEndTm?: string | null;
rainTot?: number | null;
rainStr?: number | null;
fid: string; // FID
fldngMapFid?: string; // 침수흔적도지도 FID
fldngWtlvMapSn?: number | null; // 침수흔적도지도 일련번호
taskClCd?: string; // 업무 구분 코드
fldngDstrNm?: string; // 침수재해명
exmnYr?: string; // 조사년도
avgFldngWtlv?: number | null; // 평균침수위
fldngArea?: number | null; // 침수면적
dtlOcrnCsCn?: string; // 상세발생원인내용
stdgCd?: string; // 표준지역코드
ctpvCd?: string; // 시도코드
sggCd?: string; // 시군구코드
dataSn?: number | null; // 데이터 일련번호
bgngYmd?: string; // 시작일자
endYmd?: string; // 종료일자
fldngBgngHm?: string; // 침수시작시분
fldngEndHm?: string; // 침수종료시분
fldngCsDtlCd?: string; // 침수원인상세코드
fldngDstrctNm?: string; // 침수지구명
damAmt?: number | null; // 피해금액
fldngHr?: number | null; // 침수시간
fldngYr?: string; // 침수년도
cstrnFbctnYn?: string; // 공사여부
atchFileSn?: number | null; // 첨부파일 일련번호
confArea?: number | null; // 확정면적
confLen?: number | null; // 확정연장
rainBgngTm?: string | null; // 강우시작일시
rainEndTm?: string | null; // 강우종료일시
rainTot?: number | null; // 총강우량
rainStr?: number | null; // 강우강도
};
export type FldngHstryItem = FldngHstryApiItem & {
@@ -120,20 +120,20 @@ export const FLDNG_HSTRY_DEPTH_LEGEND_ITEMS = [
export const FLDNG_HSTRY_LIST_HEADLIST: UiHeadProps[] = [
{ id: "rowNo", title: "NO", widthPercent: 5, sortable: false },
{ id: "floodYearDisplay", title: "침수년도", widthPercent: 6, sortable: true, sortKey: "fldngYr" },
{ id: "floodYearDisplay", title: "침수\n년도", widthPercent: 6, sortable: true, sortKey: "fldngYr" },
{ id: "ctpvNm", title: "시도", widthPercent: 7, sortable: true, sortKey: "ctpvCd" },
{ id: "sggNm", title: "시군구", widthPercent: 8, sortable: true, sortKey: "sggCd" },
{ id: "disasterName", title: "침수재해명", widthPercent: 14, sortable: true, sortKey: "fldngDstrNm" },
{ id: "rainBgngTmDisplay", title: "강우시작일시", widthPercent: 10, sortable: true, sortKey: "rainBgngTm" },
{ id: "rainEndTmDisplay", title: "강우종료일시", widthPercent: 10, sortable: true, sortKey: "rainEndTm" },
{ id: "rainBgngTmDisplay", title: "강우\n시작일시", widthPercent: 10, sortable: true, sortKey: "rainBgngTm" },
{ id: "rainEndTmDisplay", title: "강우\n종료일시", widthPercent: 10, sortable: true, sortKey: "rainEndTm" },
{ id: "rainTotDisplay", title: "총강우량(mm)", widthPercent: 8, sortable: true, sortKey: "rainTot" },
{ id: "rainStrDisplay", title: "강우강도(mm/hr)", widthPercent: 8, sortable: true, sortKey: "rainStr" },
{ id: "floodHourDisplay", title: "침수시간", widthPercent: 7, sortable: true, sortKey: "fldngHr" },
{ id: "districtName", title: "침수지구명", widthPercent: 10, sortable: true, sortKey: "fldngDstrctNm" },
{ id: "causeDetailCode", title: "침수원인상세코드", widthPercent: 8, sortable: true, sortKey: "fldngCsDtlCd" },
{ id: "causeDetailName", title: "침수원인상세", widthPercent: 12, sortable: false },
{ id: "avgFloodDepthDisplay", title: "평균침수위", widthPercent: 7, sortable: true, sortKey: "avgFldngWtlv" },
{ id: "floodAreaDisplay", title: "침수면적", widthPercent: 7, sortable: true, sortKey: "fldngArea" },
//{ id: "causeDetailCode", title: "침수원인상세코드", widthPercent: 8, sortable: true, sortKey: "fldngCsDtlCd" },
//{ id: "causeDetailName", title: "침수원인상세", widthPercent: 12, sortable: false },
{ id: "avgFloodDepthDisplay", title: "평균\n침수위", widthPercent: 7, sortable: true, sortKey: "avgFldngWtlv" },
{ id: "floodAreaDisplay", title: "침수\n면적\(ha)", widthPercent: 7, sortable: true, sortKey: "fldngArea" },
{ id: "exmnYearDisplay", title: "조사년도", widthPercent: 6, sortable: true, sortKey: "exmnYr" },
{ id: "damageAmountDisplay", title: "피해금액", widthPercent: 9, sortable: true, sortKey: "damAmt" },
];
@@ -291,6 +291,7 @@ const normalizeHm = (value?: string | null) => {
const normalizeDateTime = (value?: string | null) => {
const source = String(value ?? "").trim();
if (!source) return "";
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(source)) return source;
const date = new Date(source);
if (Number.isNaN(date.getTime())) return source.slice(0, 16).replace("T", " ");
const yyyy = date.getFullYear();
@@ -313,6 +314,13 @@ const formatInteger = (value?: number | null, suffix = "") => {
return `${numeric.toLocaleString("ko-KR")}${suffix}`;
};
const formatOptionalNumber = (value?: number | null, digits = 1, suffix = "") => {
if (value === null || value === undefined || value === "") return "-";
const numeric = Number(value);
if (!Number.isFinite(numeric)) return "-";
return `${numeric.toFixed(digits)}${suffix}`;
};
const buildCauseDetailName = (item: FldngHstryApiItem) => {
const direct = String(item.dtlOcrnCsCn ?? "").trim();
if (direct) return direct;
@@ -361,9 +369,12 @@ export const buildFldngHstryListQueryString = (
return params.toString();
};
export const formatFloodArea = (value: number) => `${value.toFixed(1)}ha`;
export const formatFloodArea = (value: number) => Number(value ?? 0).toLocaleString("ko-KR", {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
export const formatFloodDepth = (value: number) => `${value.toFixed(2)}m`;
export const formatFloodDepth = (value: number) => value.toFixed(2);
export const attachFldngHstryGeometry = (item: FldngHstryApiItem): FldngHstryItem => {
const historyId = String(item.fid ?? "");
@@ -392,10 +403,10 @@ export const attachFldngHstryGeometry = (item: FldngHstryApiItem): FldngHstryIte
endDate,
startTime,
endTime,
floodHour: formatNumber(Number(item.fldngHr ?? 0), 1, "시간"),
floodHour: formatNumber(Number(item.fldngHr ?? 0), 1),
causeDetailCode: String(item.fldngCsDtlCd ?? "").trim(),
causeDetailName: buildCauseDetailName(item),
damageAmount: formatInteger(Number(item.damAmt ?? 0), "원"),
damageAmount: formatInteger(Number(item.damAmt ?? 0)),
rainStartDateTime: normalizeDateTime(item.rainBgngTm),
rainEndDateTime: normalizeDateTime(item.rainEndTm),
rainStartTime: normalizeDateTime(item.rainBgngTm).slice(11, 16),
@@ -422,16 +433,16 @@ export const toFldngHstryTableRows = (
items.map((item, index) => ({
...item,
rowNo: (page - 1) * pageSize + index + 1,
floodYearDisplay: item.floodYear ? `${item.floodYear}년` : "-",
floodYearDisplay: item.floodYear || "-",
floodHourDisplay: item.floodHour || "-",
avgFloodDepthDisplay: formatFloodDepth(item.avgFloodDepthM),
floodAreaDisplay: formatFloodArea(item.floodAreaHa),
exmnYearDisplay: item.exmnYear ? `${item.exmnYear}년` : "-",
exmnYearDisplay: item.exmnYear || "-",
damageAmountDisplay: item.damageAmount || "-",
rainBgngTmDisplay: item.rainStartDateTime || "-",
rainEndTmDisplay: item.rainEndDateTime || "-",
rainTotDisplay: formatNumber(item.totalRainfallMm, 1),
rainStrDisplay: formatNumber(item.rainfallIntensityMmHr, 1),
rainTotDisplay: formatOptionalNumber(item.rainTot, 1),
rainStrDisplay: formatOptionalNumber(item.rainStr, 1),
}));
const resolveFldngHstryNodeName = (node: any) =>
@@ -32,13 +32,23 @@ import Fill from "ol/style/Fill.js";
import Stroke from "ol/style/Stroke.js";
import CircleStyle from "ol/style/Circle.js";
import { getLayerContentComponent } from "../common";
import { FldngWtlvMapCol, FldngWtlvMapCol2, SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table";
import { FldngWtlvMapCol, FldngWtlvMapCol2
, SwtrmtFcltCol, SwtrmtFcltCol2
, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2
, untrtSwgRainCol1, untrtSwgRainCol2
, MsSwagePplCol, MsSwagePplCol2
, ExclnSewagePumpCol, ExclnSewagePumpCol2
, DrtwSewagePumpCol, DrtwSewagePumpCol2
, MsSwageMhlCol, MsSwageMhlCol2
, stofcbrCol, stofcbrCol2
, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table";
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import { codeMapper } from "../common/codeMapper";
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({
@@ -67,6 +77,45 @@ const normalizeFldngWtlvMapDisplayId = (value: unknown) => {
return normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized;
};
const normalizeFldngWtlvMapApiFid = (value: unknown) => {
const normalized = String(value ?? "").trim();
if (!normalized) return "";
return normalizeFldngWtlvMapDisplayId(normalized);
};
const normalizeRainAnalysisPayload = (data: any) => {
if (!data || typeof data !== "object") return {};
return {
rain_bgng_tm: data.rainBgngTm ?? data.rain_bgng_tm ?? null,
rainBgngTm: data.rainBgngTm ?? data.rain_bgng_tm ?? null,
rain_end_tm: data.rainEndTm ?? data.rain_end_tm ?? null,
rainEndTm: data.rainEndTm ?? data.rain_end_tm ?? null,
rain_tot: data.rainTot ?? data.rain_tot ?? null,
rainTot: data.rainTot ?? data.rain_tot ?? null,
rain_str: data.rainStr ?? data.rain_str ?? null,
rainStr: data.rainStr ?? data.rain_str ?? null,
};
};
// 침수흔적도 침수시작일시, 침수종료일시 한줄로 보이도록
const formatFldngWtlvDateTime = (dateValue: unknown, timeValue: unknown) => {
const rawDate = String(dateValue ?? "").trim();
const rawTime = String(timeValue ?? "").trim();
const normalizedDate = /^\d{8}$/.test(rawDate)
? `${rawDate.slice(0, 4)}-${rawDate.slice(4, 6)}-${rawDate.slice(6, 8)}`
: rawDate;
const timeDigits = rawTime.replace(/[^0-9]/g, "");
const normalizedTime = !timeDigits
? ""
: timeDigits.length <= 2
? `${timeDigits.padStart(2, "0")}:00`
: `${timeDigits.padStart(4, "0").slice(0, 2)}:${timeDigits.padStart(4, "0").slice(2, 4)}`;
return [normalizedDate, normalizedTime].filter(Boolean).join(" ");
};
type MngInstOption = {
mngInstCd: string;
lwrInstNm: string;
@@ -92,6 +141,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 +199,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 +224,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 +519,25 @@ 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 fldngBgngDtm = formatFldngWtlvDateTime(
formattedData.bgngYmd ?? formattedData.bgng_ymd ?? data.bgngYmd ?? data.bgng_ymd,
formattedData.fldngBgngHm ?? formattedData.fldng_bgng_hm ?? data.fldngBgngHm ?? data.fldng_bgng_hm,
);
const fldngEndDtm = formatFldngWtlvDateTime(
formattedData.endYmd ?? formattedData.end_ymd ?? data.endYmd ?? data.end_ymd,
formattedData.fldngEndHm ?? formattedData.fldng_end_hm ?? data.fldngEndHm ?? data.fldng_end_hm,
);
formattedData.fldng_bgng_dtm = fldngBgngDtm;
formattedData.fldngBgngDtm = fldngBgngDtm;
formattedData.fldng_end_dtm = fldngEndDtm;
formattedData.fldngEndDtm = fldngEndDtm;
}
if (selectedLayerName === '하수관로') {
const installYmd = data.instl_ymd ?? data.instlYmd;
formattedData.elapsedYears = getElapsedYearsDisplayText(installYmd);
@@ -556,6 +647,26 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
console.warn("Pipe detail fallback kept initial feature data.");
}
}
if (selectedLayerName === '침수흔적도' && featureId) {
try {
const normalizedFloodMapFid = normalizeFldngWtlvMapApiFid(featureId);
if (!normalizedFloodMapFid) {
throw new Error("침수흔적도 fid가 비어 있습니다.");
}
const rainAnalysis = await getData(`v1/fldng/hstry/rain-anls/${encodeURIComponent(normalizedFloodMapFid)}`);
if (rainAnalysis) {
const mergedData = {
...formattedData,
...normalizeRainAnalysisPayload(rainAnalysis),
};
setFetchData(mergedData);
setTableData(mergedData);
}
} catch (rainError) {
console.warn("침수강우분석 조회 실패:", rainError);
}
}
} catch (error) {
console.error('데이터 로딩 중 오류:', error);
}
@@ -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` 흐름을 따른다.
@@ -71,3 +71,15 @@
- popup 라우트의 초기 컴파일 시간이 길어 자동 요청에서 timeout이 발생할 수 있다.
- `침수흔적도` 기준데이터 이름/구조와 FE 판별식이 다를 수 있다.
- 실제 API 필드가 확정되면 상세패널 항목명/정렬이 일부 바뀔 수 있다.
## 7. 2026-07-28 지도 클릭 강우분석 조회 연동
- 반영 파일
- `FE_APP_ROOT/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx`
- `FE_APP_ROOT/app/(map-layout)/map/components/common/table.ts`
- 구현 내용
- 지도에서 `침수흔적도(tb_ms_fldng_wtlv_map)`를 클릭해 우측 상세를 열면, 기존 원본 속성값만 보여주던 구조에 `fid` 기준 강우분석 조회를 추가했다.
- FE는 `GET /api/v1/fldng/hstry/rain-anls/{fid}`를 추가 호출한 뒤 `tb_sn_fldng_wtlv_rain_anls` 응답을 상세 테이블 데이터에 merge한다.
- 침수흔적도 상세 컬럼에 `강우시작일시`, `강우종료일시`, `총강우량(mm)`, `강우강도(mm/hr)`를 추가했다.
- 영향
- 레이어 트리 popup 목록과 별도로, 실제 지도 객체 클릭 상세에서도 강우분석 정보를 동일하게 확인할 수 있다.
+1
View File
@@ -72,6 +72,7 @@ export function middleware(req: NextRequest) {
typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설
|| typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역
|| typename == "hasudo:tb_ms_fldng_wtlv_map" //침수흔적도
|| typename == "hasudo:tb_ms_fldng_dpth_map" //침수흔적도-깊이
|| typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도
|| typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도
) {
-4
View File
@@ -1,9 +1,5 @@
import "./styles/base.css";
import "./styles/components.css";
import "./styles/components-black.css";
import "./styles/components-blue.css";
import "./styles/components-green.css";
import "./styles/components-red.css";
import { AppProvider } from "./providers";
import { cookies } from "next/headers";
@@ -1,20 +0,0 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-label;
}
.card-border {
@apply border border-border bg-background shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-border p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-border py-4;
}
input[type="file"] {
@apply h-[40px] w-[200px] cursor-pointer rounded-md border border-input bg-field px-3 py-2 text-sm text-foreground;
}
}
@@ -1,20 +0,0 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-label;
}
.card-border {
@apply border border-border bg-background shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-border p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-border py-4;
}
input[type="file"] {
@apply h-[40px] w-[200px] cursor-pointer rounded-md border border-input bg-field px-3 py-2 text-sm text-foreground;
}
}
@@ -1,20 +0,0 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-label;
}
.card-border {
@apply border border-border bg-background shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-border p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-border py-4;
}
input[type="file"] {
@apply h-[40px] w-[200px] cursor-pointer rounded-md border border-input bg-field px-3 py-2 text-sm text-foreground;
}
}
-20
View File
@@ -1,20 +0,0 @@
@tailwind components;
@layer components {
.card {
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-label;
}
.card-border {
@apply border border-border bg-background shadow-blur;
}
.card-header {
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-border p-4;
}
.card-footer {
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-border py-4;
}
input[type="file"] {
@apply h-[40px] w-[200px] cursor-pointer rounded-md border border-input bg-field px-3 py-2 text-sm text-foreground;
}
}
+142
View File
@@ -190,6 +190,148 @@
.sn-popup-help-text {
@apply mt-2 rounded-md border border-border bg-secondary px-4 py-3 text-[1.05rem] text-label;
}
.form-section-title {
@apply mt-4 border border-border bg-[#eaf3fb] p-2 text-center text-xl font-medium text-[#647784];
}
[data-theme="black"] {
.card {
color: #fff;
background-color: #0a0a0a !important;
}
form {
color: rgb(255 255 255);
}
label {
color: rgb(241, 241, 241);
}
input {
height: 3.2rem;
background-color: #1a1a1a;
border: 1px solid #262626;
border-radius: 4px;
}
input.text-gray-600 {
color: #f1f1f1;
}
button[role="combobox"] {
color: #f1f1f1;
background-color: rgb(26, 26, 26);
}
div [role="tablist"] {
border: unset;
gap: 4px;
}
.tabs-trigger[data-state="inactive"] {
color: #a4b7bf;
background: #55595d;
}
.tabs-trigger[data-state="inactive"]:hover {
color: #6bbaff;
background: #4c4f52;
}
.card-border {
box-shadow: 0 0 10px -1px #0000000f;
border: 1px solid #393939;
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
a[aria-current="page"] {
background-color: rgb(32, 148, 250);
color: #000;
}
.card-header {
@apply border-b-[#3f4042];
}
.form-section-title {
@apply bg-[#5e7b9c] text-[#fff];
}
.gis-layer-panel {
background: #111214;
border-right-color: #2f3136;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.03);
}
.gis-layer-panel-title {
color: #f3f4f6;
}
.gis-layer-panel hr {
border-color: #2f3136;
}
.gis-layer-row {
color: #e5e7eb;
}
.gis-layer-row:hover {
background: rgba(255, 255, 255, 0.08);
}
.gis-layer-row span,
.gis-layer-row label {
color: #e5e7eb !important;
}
.gis-layer-row input[type="checkbox"] {
accent-color: #c026d3;
}
.gis-layer-action-button {
background: #1a1c20;
border-color: #3b82f6;
color: #cfe3ff;
}
.gis-layer-action-button:hover {
background: #23262b;
}
.gis-layer-setting {
background: #111214;
border-top: 1px solid #2f3136;
}
.gis-layer-toggle {
background: #111214;
border-color: #2f3136;
color: #f3f4f6;
}
.gis-layer-toggle:hover {
background: #1a1c20;
}
input[type="file"] {
background-color: #1a1a1a;
border-color: #262626;
color: #f1f1f1;
}
}
[data-theme="blue"] {
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
.form-section-title {
@apply bg-[#eaf3fb] text-[#647784];
}
}
[data-theme="green"] {
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
.form-section-title {
@apply bg-[#ebf7f7] text-[#64908f];
}
}
[data-theme="red"] {
.card-border {
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
}
.card-header {
@apply border-b-[#3f4042];
}
.form-section-title {
@apply bg-[#f6f6f9] text-[#808080];
}
}
}
/* `I/I 분석 > 침입수 분석결과`는 나중에 다시 사용할 수 있도록 라우트는 유지하고 LNB에서만 임시 숨김 처리한다. */
/* `I/I 분석 > 침입수 분석결과`, `RDII 분석결과`는 지도 popup 흐름으로 대체하고 레거시 라우트는 유지한 채 LNB에서만 숨긴다. */
@@ -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>
);
}
@@ -585,10 +585,8 @@ export default React.memo(function GisTree({
const isSinkholeRootTreeNode =
String(node.id) === "SN_SINKHOLE" ||
nodeLabel === "지반침하정보";
const isFloodHistoryRootTreeNode = nodeLabel === "침수흔적도";
const hideTreeLegendIcon =
(isSinkholeRootTreeNode && !sinkholeLegendIconVisible) ||
isFloodHistoryRootTreeNode;
(isSinkholeRootTreeNode && !sinkholeLegendIconVisible);
const showLayerActionButton = layerIconVisible?.(node);
const showLayerActionIcon = depth > 0 && imageVisible;