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 902be2916..b01c8b019 100644
--- a/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx
+++ b/apps/sn/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx
@@ -5,7 +5,6 @@ import {
ChartColumnBig,
ChevronLeft,
ChevronRight,
- History,
PanelsTopLeft,
PencilRuler,
Search,
@@ -31,7 +30,6 @@ const ANALYSIS_POPUP_ITEMS: Array<{
{ key: "infoSearch", label: "정보검색", icon: Search },
{ key: "pipeAnalysisPopup", label: "관로분석", icon: ChartColumnBig },
{ key: "floodPredictionMapPopup", label: "침수예측지도", icon: Waves },
- { key: "floodHistoryPopup", label: "침수발생이력", icon: History },
{ 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/floodHistory/FloodHistoryDetailPanel.tsx b/apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryDetailPanel.tsx
new file mode 100644
index 000000000..66ed55078
--- /dev/null
+++ b/apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryDetailPanel.tsx
@@ -0,0 +1,142 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import { formatFloodArea, formatFloodDepth, type FloodHistoryItem } from "./floodHistoryTypes";
+
+type Props = {
+ detail: FloodHistoryItem;
+ onClose: () => void;
+ position: {
+ left: number;
+ top: number;
+ };
+};
+
+type DetailTab = "year" | "disaster" | "rainfall";
+
+const renderRows = (rows: Array<[string, string]>) => (
+
+
+
+
+
+
+ {rows.map(([label, value], index) => (
+
+ |
+ {label}
+ |
+
+ {value}
+ |
+
+ ))}
+
+
+);
+
+export default function FloodHistoryDetailPanel({ detail, onClose, position }: Props) {
+ // 지도에서 침수흔적도 polygon을 클릭했을 때, 상세정보는 업무 의미 기준으로 3개 탭에 나눠 보여준다.
+ const [activeTab, setActiveTab] = useState("year");
+
+ const yearRows = useMemo(
+ () => [
+ ["침수심", `${detail.floodDepthM.toFixed(2)} m`],
+ ["침수면적", formatFloodArea(detail.floodAreaHa)],
+ ["침수년도", `${detail.floodYear}년`],
+ ["등급", detail.gradeLabel],
+ ["침수구역", detail.districtName],
+ ] as Array<[string, string]>,
+ [detail],
+ );
+
+ const disasterRows = useMemo(
+ () => [
+ ["침수재해명", detail.disasterName],
+ ["침수면적", formatFloodArea(detail.floodAreaHa)],
+ ["침수원인상세명", detail.causeDetailName],
+ ["침수시작년월일", detail.startDate],
+ ["침수종료년월일", detail.endDate],
+ ["침수시작시간", detail.startTime],
+ ] as Array<[string, string]>,
+ [detail],
+ );
+
+ const rainfallRows = useMemo(
+ () => [
+ ["침수시작일자", detail.startDate],
+ ["침수종료일자", detail.endDate],
+ ["침수시작시간", detail.startTime],
+ ["침수종료시간", detail.endTime],
+ ["침수시간", detail.floodHour],
+ ["강우시작시간", detail.rainStartTime],
+ ["강우종료시간", detail.rainEndTime],
+ ["총강우량(mm)", `${detail.totalRainfallMm.toFixed(0)}`],
+ ["강우강도(mm/hr)", `${detail.rainfallIntensityMmHr.toFixed(0)}`],
+ ["침수시간", detail.floodHour],
+ ["침수원인상세코드", detail.causeDetailCode],
+ ["피해금액", detail.damageAmount],
+ ] as Array<[string, string]>,
+ [detail],
+ );
+
+ const currentRows = activeTab === "year"
+ ? yearRows
+ : activeTab === "disaster"
+ ? disasterRows
+ : rainfallRows;
+
+ return (
+
+
+
+
+
+ {[
+ ["year", "년도탭"],
+ ["disaster", "재해명탭"],
+ ["rainfall", "침수발생강우탭"],
+ ].map(([tabId, label]) => {
+ const isActive = activeTab === tabId;
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {renderRows(currentRows)}
+
+
+
+ );
+}
diff --git a/apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage.tsx b/apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage.tsx
new file mode 100644
index 000000000..346ee33ec
--- /dev/null
+++ b/apps/sn/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage.tsx
@@ -0,0 +1,408 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import { Button, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
+import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
+import UiPageNations from "@common/business_components/ui/table/uiPageNations";
+import useStore from "@/store/useStore";
+import {
+ buildFloodHistoryYearOptions,
+ buildFloodHistoryTableRows,
+ FLOOD_HISTORY_DEFAULT_FILTERS,
+ FLOOD_HISTORY_DOWNLOAD_HEADERS,
+ FLOOD_HISTORY_ITEMS,
+ FLOOD_HISTORY_PAGE_SIZE,
+ FLOOD_HISTORY_ROW_SELECT_MESSAGE,
+ formatFloodArea,
+ formatFloodDepth,
+ normalizeFloodHistoryFilters,
+ type FloodHistoryFilters,
+ type FloodHistoryItem,
+} from "./floodHistoryTypes";
+
+const resolveOptionText = (
+ code: string,
+ options: Array<{ text: string; val: string | number }>,
+ prefixLength?: number,
+) => {
+ const normalized = String(code ?? "").trim();
+ if (!normalized || normalized === "0") return "";
+ const lookup = prefixLength ? normalized.slice(0, prefixLength) : normalized;
+ return options.find((item) => String(item.val).trim() === lookup)?.text ?? "";
+};
+
+const GRID_COLUMNS = [
+ { id: "rowNo", title: "NO", width: "64px", align: "center" },
+ { id: "ctpvNm", title: "시도", width: "90px", align: "center" },
+ { id: "sggNm", title: "시군구", width: "100px", align: "center" },
+ { id: "startDateDisplay", title: "침수시작일자", width: "118px", align: "center" },
+ { id: "endDateDisplay", title: "침수종료일자", width: "118px", align: "center" },
+ { id: "startTimeDisplay", title: "침수시작시간", width: "98px", align: "center" },
+ { id: "endTimeDisplay", title: "침수종료시간", width: "98px", align: "center" },
+ { id: "floodYearDisplay", title: "침수년도", width: "96px", align: "center" },
+ { id: "floodHourDisplay", title: "침수시간", width: "100px", align: "center" },
+ { id: "disasterName", title: "침수재해명", width: "190px", align: "left" },
+ { id: "districtNameDisplay", title: "침수지구명", width: "150px", align: "left" },
+ { id: "causeCodeDisplay", title: "침수원인상세코드", width: "132px", align: "center" },
+ { id: "causeDetailDisplay", title: "침수원인상세", width: "190px", align: "left" },
+ { id: "avgFloodDepthDisplay", title: "평균침수위", width: "104px", align: "right" },
+ { id: "floodAreaDisplay", title: "침수면적", width: "104px", align: "right" },
+ { id: "exmnYearDisplay", title: "조사년도", width: "96px", align: "center" },
+ { id: "damageAmountDisplay", title: "피해금액", width: "132px", align: "right" },
+] as const;
+
+const buildDownloadRows = (rows: FloodHistoryItem[]) =>
+ rows.map((row, index) => ({
+ rowNo: index + 1,
+ ctpvNm: row.ctpvNm,
+ sggNm: row.sggNm,
+ disasterName: row.disasterName,
+ exmnYearDisplay: `${row.exmnYear}년`,
+ avgFloodDepthDisplay: formatFloodDepth(row.avgFloodDepthM),
+ floodAreaDisplay: formatFloodArea(row.floodAreaHa),
+ causeDetailDisplay: row.causeDetailName,
+ startDateDisplay: row.startDate,
+ endDateDisplay: row.endDate,
+ startTimeDisplay: row.startTime,
+ endTimeDisplay: row.endTime,
+ causeCodeDisplay: row.causeDetailCode,
+ districtNameDisplay: row.districtName,
+ damageAmountDisplay: row.damageAmount,
+ floodHourDisplay: row.floodHour,
+ floodYearDisplay: `${row.floodYear}년`,
+ }));
+
+export default function FloodHistoryPopupPage() {
+ const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
+ // 조회조건 draft는 popup 내부에서 사용자가 편집 중인 값이다.
+ const [filters, setFilters] = useState(FLOOD_HISTORY_DEFAULT_FILTERS);
+ // 적용된 조회조건과 선택행은 다운로드/페이지 이동에도 같은 기준을 유지하기 위해 분리한다.
+ const [appliedFilters, setAppliedFilters] = useState(FLOOD_HISTORY_DEFAULT_FILTERS);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [selectedHistoryId, setSelectedHistoryId] = useState("");
+
+ useEffect(() => {
+ if (ctpv.length === 0) void fetchCtpv();
+ if (sgg.length === 0) void fetchSgg();
+ }, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
+
+ const floodYearOptions = useMemo(
+ () => buildFloodHistoryYearOptions(FLOOD_HISTORY_ITEMS),
+ [],
+ );
+
+ const sggOptions = useMemo(
+ () =>
+ filters.ctpvCd === "0"
+ ? sgg
+ : sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))),
+ [filters.ctpvCd, sgg],
+ );
+
+ const filteredRows = useMemo(() => {
+ const normalized = normalizeFloodHistoryFilters(appliedFilters);
+ const selectedCtpvName = resolveOptionText(normalized.ctpvCd, ctpv as any, 2);
+ const selectedSggName = resolveOptionText(normalized.sggCd, sgg as any, 5);
+ const disasterToken = normalized.disasterName.toLocaleLowerCase("ko-KR");
+
+ return FLOOD_HISTORY_ITEMS.filter((item) => {
+ if (selectedCtpvName && item.ctpvNm !== selectedCtpvName) return false;
+ if (selectedSggName && item.sggNm !== selectedSggName) return false;
+ if (normalized.floodYear && item.floodYear !== normalized.floodYear) return false;
+ if (
+ disasterToken &&
+ !item.disasterName.toLocaleLowerCase("ko-KR").includes(disasterToken) &&
+ !item.districtName.toLocaleLowerCase("ko-KR").includes(disasterToken)
+ ) {
+ return false;
+ }
+ return true;
+ }).sort((left, right) => {
+ if (left.floodYear !== right.floodYear) {
+ return Number(right.floodYear) - Number(left.floodYear);
+ }
+ return left.disasterName.localeCompare(right.disasterName, "ko");
+ });
+ }, [appliedFilters, ctpv, sgg]);
+
+ const pagedRows = useMemo(() => {
+ const start = (currentPage - 1) * FLOOD_HISTORY_PAGE_SIZE;
+ const end = start + FLOOD_HISTORY_PAGE_SIZE;
+ return buildFloodHistoryTableRows(
+ filteredRows.slice(start, end),
+ currentPage,
+ FLOOD_HISTORY_PAGE_SIZE,
+ );
+ }, [currentPage, filteredRows]);
+
+ const handleSearch = () => {
+ const normalized = normalizeFloodHistoryFilters(filters);
+
+ if (normalized.sggCd !== "0" && normalized.ctpvCd === "0") {
+ alert("시도를 먼저 선택하세요.");
+ return;
+ }
+ if (normalized.floodYear && normalized.floodYear.length !== 4) {
+ alert("침수발생년도는 4자리 연도로 입력하세요.");
+ return;
+ }
+
+ setAppliedFilters(normalized);
+ setCurrentPage(1);
+ setSelectedHistoryId("");
+ };
+
+ const handleDownload = async () => {
+ if (filteredRows.length === 0) {
+ alert("다운로드할 침수발생이력 데이터가 없습니다.");
+ return;
+ }
+
+ await downloadExcelWithKoreanHeaders({
+ data: buildDownloadRows(filteredRows) as unknown as JSON[],
+ fileName: `침수흔적도_${new Date().toISOString().slice(0, 10)}.xlsx`,
+ sheetNameArray: ["침수흔적도"],
+ headerRange: "A1:Q1",
+ headerMapping: FLOOD_HISTORY_DOWNLOAD_HEADERS,
+ });
+ };
+
+ const handleRowDoubleClick = (row: FloodHistoryItem) => {
+ window.opener?.postMessage(
+ {
+ type: FLOOD_HISTORY_ROW_SELECT_MESSAGE,
+ row,
+ openDetail: false,
+ },
+ window.location.origin,
+ );
+ window.close();
+ };
+
+ const handleRowClick = (row: FloodHistoryItem) => {
+ setSelectedHistoryId(row.historyId);
+ // popup을 유지한 채 메인 지도에 mock 침수흔적/침수심 preview를 먼저 보여준다.
+ window.opener?.postMessage(
+ {
+ type: FLOOD_HISTORY_ROW_SELECT_MESSAGE,
+ row,
+ openDetail: false,
+ },
+ window.location.origin,
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 침수흔적도 조회 결과 {filteredRows.length.toLocaleString("ko-KR")}건
+
+
+
+
+
+
+ {GRID_COLUMNS.map((head) => (
+
+ ))}
+
+
+
+ NO
+ 시도
+ 시군구
+ 강우 분석정보
+ 침수재해명
+ 침수년도
+ 침수시간
+ 침수지구명
+ 침수원인상세코드
+ 침수원인상세
+ 평균침수위
+ 침수면적
+ 조사년도
+ 피해금액
+
+
+ 침수시작일자
+ 침수종료일자
+ 침수시작시간
+ 침수종료시간
+
+
+
+ {pagedRows.length > 0 ? (
+ pagedRows.map((row) => {
+ const isSelected = selectedHistoryId === row.historyId;
+
+ return (
+ handleRowClick(row)}
+ onDoubleClick={() => handleRowDoubleClick(row)}
+ >
+ {row.rowNo}
+ {row.ctpvNm}
+ {row.sggNm}
+ {row.startDateDisplay}
+ {row.endDateDisplay}
+ {row.startTimeDisplay}
+ {row.endTimeDisplay}
+ {row.disasterName}
+ {row.floodYearDisplay}
+ {row.floodHourDisplay}
+ {row.districtNameDisplay}
+ {row.causeCodeDisplay}
+ {row.causeDetailDisplay}
+ {row.avgFloodDepthDisplay}
+ {row.floodAreaDisplay}
+ {row.exmnYearDisplay}
+ {row.damageAmountDisplay}
+
+ );
+ })
+ ) : (
+
+
+ 조회된 데이터가 없습니다.
+
+
+ )}
+
+
+
+
+ {filteredRows.length > 0 ? (
+
+ ) : null}
+
+
+ 행 클릭 시 메인 지도에 미리 표시되고, 더블클릭하면 팝업을 닫습니다. 상세정보는 지도 위 침수흔적 영역을 클릭해 확인합니다.
+
+
+
+
+ );
+}
diff --git a/apps/sn/app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts b/apps/sn/app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts
new file mode 100644
index 000000000..fd6ef484e
--- /dev/null
+++ b/apps/sn/app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts
@@ -0,0 +1,458 @@
+"use client";
+
+export const FLOOD_HISTORY_POPUP_KEY = "floodHistoryPopup";
+export const FLOOD_HISTORY_ROW_SELECT_MESSAGE = "SN_FLOOD_HISTORY_ROW_SELECT";
+export const FLOOD_HISTORY_LAYER_TYPE = "tb_sn_flood_trace_mock";
+export const FLOOD_HISTORY_LAYER_NAME = "침수흔적도";
+export const FLOOD_HISTORY_AREA_LAYER_ID = "SN_FLOOD_HISTORY_AREA";
+export const FLOOD_HISTORY_DEPTH_LAYER_ID = "SN_FLOOD_HISTORY_DEPTH";
+export const FLOOD_HISTORY_PAGE_SIZE = 10;
+
+export type FloodHistoryFilters = {
+ ctpvCd: string;
+ sggCd: string;
+ floodYear: string;
+ disasterName: string;
+};
+
+export type FloodHistoryDepthZone = {
+ zoneId: string;
+ depthLabel: string;
+ color: string;
+ ring: [number, number][];
+};
+
+export type FloodHistoryItem = {
+ historyId: string;
+ ctpvNm: string;
+ sggNm: string;
+ floodYear: string;
+ exmnYear: string;
+ disasterName: string;
+ districtName: string;
+ floodAreaHa: number;
+ avgFloodDepthM: number;
+ floodDepthM: number;
+ gradeLabel: string;
+ startDate: string;
+ endDate: string;
+ startTime: string;
+ endTime: string;
+ floodHour: string;
+ causeDetailCode: string;
+ causeDetailName: string;
+ damageAmount: string;
+ rainStartTime: string;
+ rainEndTime: string;
+ totalRainfallMm: number;
+ rainfallIntensityMmHr: number;
+ areaRing: [number, number][];
+ depthZones: FloodHistoryDepthZone[];
+};
+
+export type FloodHistoryTableRow = FloodHistoryItem & {
+ rowNo: number;
+ exmnYearDisplay: string;
+ avgFloodDepthDisplay: string;
+ floodAreaDisplay: string;
+ causeDetailDisplay: string;
+ startDateDisplay: string;
+ endDateDisplay: string;
+ startTimeDisplay: string;
+ endTimeDisplay: string;
+ causeCodeDisplay: string;
+ districtNameDisplay: string;
+ damageAmountDisplay: string;
+ floodHourDisplay: string;
+ floodYearDisplay: string;
+};
+
+export const FLOOD_HISTORY_DEFAULT_FILTERS: FloodHistoryFilters = {
+ ctpvCd: "0",
+ sggCd: "0",
+ floodYear: "",
+ disasterName: "",
+};
+
+export const FLOOD_HISTORY_DOWNLOAD_HEADERS: Record = {
+ rowNo: "NO",
+ ctpvNm: "시도",
+ sggNm: "시군구",
+ disasterName: "침수재해명",
+ exmnYearDisplay: "조사년도",
+ avgFloodDepthDisplay: "평균침수위",
+ floodAreaDisplay: "침수면적",
+ causeDetailDisplay: "침수원인상세",
+ startDateDisplay: "침수시작일자",
+ endDateDisplay: "침수종료일자",
+ startTimeDisplay: "침수시작시간",
+ endTimeDisplay: "침수종료시간",
+ causeCodeDisplay: "침수원인상세코드",
+ districtNameDisplay: "침수지구명",
+ damageAmountDisplay: "피해금액",
+ floodHourDisplay: "침수시간",
+ floodYearDisplay: "침수년도",
+};
+
+export const FLOOD_HISTORY_DEPTH_LEGEND_ITEMS = [
+ { color: "#fff27a", label: "0.0~0.5m" },
+ { color: "#c2f13d", label: "0.5~1.0m" },
+ { color: "#67d8ff", label: "1.0~2.0m" },
+ { color: "#8f8bff", label: "2.0~5.0m" },
+ { color: "#f2528b", label: "5.0m 이상" },
+] as const;
+
+const toRadian = (degree: number) => (degree * Math.PI) / 180;
+
+const createIrregularRing = ({
+ centerX,
+ centerY,
+ radiusX,
+ radiusY,
+ scales,
+ rotationDeg = 0,
+}: {
+ centerX: number;
+ centerY: number;
+ radiusX: number;
+ radiusY: number;
+ scales: number[];
+ rotationDeg?: number;
+}): [number, number][] => {
+ const rotation = toRadian(rotationDeg);
+ const points = scales.map((scale, index) => {
+ const angle = rotation + (Math.PI * 2 * index) / scales.length;
+ const jitterX = radiusX * scale * Math.cos(angle);
+ const jitterY = radiusY * scale * Math.sin(angle);
+ return [centerX + jitterX, centerY + jitterY] as [number, number];
+ });
+
+ return [...points, points[0]];
+};
+
+const createDepthZones = (
+ historyId: string,
+ centerX: number,
+ centerY: number,
+): FloodHistoryDepthZone[] => {
+ return [
+ {
+ zoneId: `${historyId}-depth-1`,
+ depthLabel: "0.0~0.5m",
+ color: "#fff27a",
+ ring: createIrregularRing({
+ centerX: centerX - 5200,
+ centerY: centerY - 2600,
+ radiusX: 3600,
+ radiusY: 2900,
+ rotationDeg: -18,
+ scales: [1.02, 0.78, 0.9, 1.08, 0.86, 0.72, 0.94, 1.04],
+ }),
+ },
+ {
+ zoneId: `${historyId}-depth-2`,
+ depthLabel: "0.5~1.0m",
+ color: "#c2f13d",
+ ring: createIrregularRing({
+ centerX: centerX + 2600,
+ centerY: centerY - 1200,
+ radiusX: 4300,
+ radiusY: 3600,
+ rotationDeg: 12,
+ scales: [1.04, 0.88, 1.02, 0.8, 0.92, 1.08, 0.84, 0.95],
+ }),
+ },
+ {
+ zoneId: `${historyId}-depth-3`,
+ depthLabel: "1.0~2.0m",
+ color: "#67d8ff",
+ ring: createIrregularRing({
+ centerX: centerX - 600,
+ centerY: centerY + 2600,
+ radiusX: 5600,
+ radiusY: 4700,
+ rotationDeg: 8,
+ scales: [1.12, 0.86, 0.94, 1.06, 0.82, 0.97, 1.04, 0.9, 1.08, 0.84],
+ }),
+ },
+ {
+ zoneId: `${historyId}-depth-4`,
+ depthLabel: "2.0~5.0m",
+ color: "#8f8bff",
+ ring: createIrregularRing({
+ centerX: centerX + 4200,
+ centerY: centerY + 5200,
+ radiusX: 3200,
+ radiusY: 2500,
+ rotationDeg: 26,
+ scales: [1.05, 0.82, 1.08, 0.88, 0.96, 0.78, 1.02, 0.9],
+ }),
+ },
+ {
+ zoneId: `${historyId}-depth-5`,
+ depthLabel: "5.0m 이상",
+ color: "#f2528b",
+ ring: createIrregularRing({
+ centerX: centerX + 600,
+ centerY: centerY - 5200,
+ radiusX: 1500,
+ radiusY: 1350,
+ rotationDeg: -10,
+ scales: [1.0, 0.8, 1.06, 0.84, 0.94, 0.76, 0.98],
+ }),
+ },
+ ];
+};
+
+const createHistory = (
+ item: Omit & {
+ centerX: number;
+ centerY: number;
+ width: number;
+ height: number;
+ },
+): FloodHistoryItem => {
+ const { centerX, centerY, width, height, ...detail } = item;
+
+ return {
+ ...detail,
+ areaRing: createIrregularRing({
+ centerX,
+ centerY,
+ radiusX: width / 2,
+ radiusY: height / 2,
+ rotationDeg: 9,
+ scales: [1.08, 0.9, 1.04, 0.82, 0.98, 1.12, 0.88, 0.96, 1.03, 0.86],
+ }),
+ depthZones: createDepthZones(detail.historyId, centerX, centerY),
+ };
+};
+
+export const FLOOD_HISTORY_ITEMS: FloodHistoryItem[] = [
+ createHistory({
+ historyId: "FH-2024-001",
+ ctpvNm: "부산광역시",
+ sggNm: "중구",
+ floodYear: "2024",
+ exmnYear: "2025",
+ disasterName: "중앙동 저지대 침수",
+ districtName: "중앙동 저지대",
+ floodAreaHa: 1.8,
+ avgFloodDepthM: 0.62,
+ floodDepthM: 0.62,
+ gradeLabel: "1등급",
+ startDate: "2024-07-18",
+ endDate: "2024-07-18",
+ startTime: "14:10",
+ endTime: "18:20",
+ floodHour: "4.2시간",
+ causeDetailCode: "FC-01",
+ causeDetailName: "집중강우",
+ damageAmount: "120,000,000원",
+ rainStartTime: "13:50",
+ rainEndTime: "18:20",
+ totalRainfallMm: 91,
+ rainfallIntensityMmHr: 31,
+ centerX: 1122000,
+ centerY: 1705000,
+ width: 18000,
+ height: 14000,
+ }),
+ createHistory({
+ historyId: "FH-2023-002",
+ ctpvNm: "경상남도",
+ sggNm: "창원시",
+ floodYear: "2023",
+ exmnYear: "2024",
+ disasterName: "팔용동 공단 배수지연 침수",
+ districtName: "팔용동 공단",
+ floodAreaHa: 4.8,
+ avgFloodDepthM: 1.92,
+ floodDepthM: 1.92,
+ gradeLabel: "2등급",
+ startDate: "2023-08-11",
+ endDate: "2023-08-11",
+ startTime: "09:40",
+ endTime: "15:10",
+ floodHour: "5.5시간",
+ causeDetailCode: "FC-04",
+ causeDetailName: "배수시설 처리능력 초과",
+ damageAmount: "340,000,000원",
+ rainStartTime: "08:55",
+ rainEndTime: "15:05",
+ totalRainfallMm: 138,
+ rainfallIntensityMmHr: 42,
+ centerX: 1081000,
+ centerY: 1778000,
+ width: 25000,
+ height: 17000,
+ }),
+ createHistory({
+ historyId: "FH-2022-003",
+ ctpvNm: "울산광역시",
+ sggNm: "남구",
+ floodYear: "2022",
+ exmnYear: "2023",
+ disasterName: "태화강 제내지 도로 침수",
+ districtName: "여천동 하류부",
+ floodAreaHa: 3.1,
+ avgFloodDepthM: 1.15,
+ floodDepthM: 1.15,
+ gradeLabel: "2등급",
+ startDate: "2022-09-06",
+ endDate: "2022-09-06",
+ startTime: "07:20",
+ endTime: "12:40",
+ floodHour: "5.3시간",
+ causeDetailCode: "FC-02",
+ causeDetailName: "하천 수위 상승",
+ damageAmount: "210,000,000원",
+ rainStartTime: "06:30",
+ rainEndTime: "11:50",
+ totalRainfallMm: 117,
+ rainfallIntensityMmHr: 37,
+ centerX: 1168000,
+ centerY: 1811000,
+ width: 22000,
+ height: 16000,
+ }),
+ createHistory({
+ historyId: "FH-2021-004",
+ ctpvNm: "부산광역시",
+ sggNm: "서구",
+ floodYear: "2021",
+ exmnYear: "2022",
+ disasterName: "서대신동 급경사지 유출 침수",
+ districtName: "서대신동 일원",
+ floodAreaHa: 2.4,
+ avgFloodDepthM: 0.85,
+ floodDepthM: 0.85,
+ gradeLabel: "1등급",
+ startDate: "2021-07-07",
+ endDate: "2021-07-07",
+ startTime: "16:00",
+ endTime: "20:10",
+ floodHour: "4.1시간",
+ causeDetailCode: "FC-03",
+ causeDetailName: "우수관로 월류",
+ damageAmount: "175,000,000원",
+ rainStartTime: "15:20",
+ rainEndTime: "19:50",
+ totalRainfallMm: 104,
+ rainfallIntensityMmHr: 29,
+ centerX: 1111000,
+ centerY: 1709000,
+ width: 20000,
+ height: 14500,
+ }),
+ createHistory({
+ historyId: "FH-2020-005",
+ ctpvNm: "부산광역시",
+ sggNm: "영도구",
+ floodYear: "2020",
+ exmnYear: "2021",
+ disasterName: "청학동 해안도로 월파 침수",
+ districtName: "청학동 해안부",
+ floodAreaHa: 1.2,
+ avgFloodDepthM: 0.48,
+ floodDepthM: 0.48,
+ gradeLabel: "1등급",
+ startDate: "2020-10-22",
+ endDate: "2020-10-22",
+ startTime: "03:10",
+ endTime: "06:30",
+ floodHour: "3.3시간",
+ causeDetailCode: "FC-06",
+ causeDetailName: "해안 월파",
+ damageAmount: "82,000,000원",
+ rainStartTime: "02:40",
+ rainEndTime: "05:50",
+ totalRainfallMm: 55,
+ rainfallIntensityMmHr: 18,
+ centerX: 1129000,
+ centerY: 1695000,
+ width: 15000,
+ height: 12000,
+ }),
+ createHistory({
+ historyId: "FH-2019-006",
+ ctpvNm: "경상남도",
+ sggNm: "김해시",
+ floodYear: "2019",
+ exmnYear: "2020",
+ disasterName: "장유 배수분구 내수 침수",
+ districtName: "장유 배수분구",
+ floodAreaHa: 2.9,
+ avgFloodDepthM: 0.96,
+ floodDepthM: 0.96,
+ gradeLabel: "1등급",
+ startDate: "2019-09-21",
+ endDate: "2019-09-21",
+ startTime: "11:30",
+ endTime: "16:15",
+ floodHour: "4.8시간",
+ causeDetailCode: "FC-05",
+ causeDetailName: "배수펌프 지연 가동",
+ damageAmount: "143,000,000원",
+ rainStartTime: "10:45",
+ rainEndTime: "15:50",
+ totalRainfallMm: 88,
+ rainfallIntensityMmHr: 24,
+ centerX: 1098000,
+ centerY: 1734000,
+ width: 21000,
+ height: 15000,
+ }),
+];
+
+export const normalizeFloodHistoryFilters = (
+ filters: FloodHistoryFilters,
+): FloodHistoryFilters => ({
+ ctpvCd: String(filters.ctpvCd ?? "0") || "0",
+ sggCd: String(filters.sggCd ?? "0") || "0",
+ floodYear: String(filters.floodYear ?? "").replace(/[^0-9]/g, "").slice(0, 4),
+ disasterName: String(filters.disasterName ?? "").trim(),
+});
+
+export const formatFloodArea = (value: number) => `${value.toFixed(1)}ha`;
+
+export const formatFloodDepth = (value: number) => `${value.toFixed(2)}m`;
+
+export const buildFloodHistoryTableRows = (
+ items: FloodHistoryItem[],
+ page: number,
+ pageSize = FLOOD_HISTORY_PAGE_SIZE,
+): FloodHistoryTableRow[] =>
+ items.map((item, index) => ({
+ ...item,
+ rowNo: (page - 1) * pageSize + index + 1,
+ exmnYearDisplay: `${item.exmnYear}년`,
+ avgFloodDepthDisplay: formatFloodDepth(item.avgFloodDepthM),
+ floodAreaDisplay: formatFloodArea(item.floodAreaHa),
+ causeDetailDisplay: item.causeDetailName,
+ startDateDisplay: item.startDate,
+ endDateDisplay: item.endDate,
+ startTimeDisplay: item.startTime,
+ endTimeDisplay: item.endTime,
+ causeCodeDisplay: item.causeDetailCode,
+ districtNameDisplay: item.districtName,
+ damageAmountDisplay: item.damageAmount,
+ floodHourDisplay: item.floodHour,
+ floodYearDisplay: `${item.floodYear}년`,
+ }));
+
+export const buildFloodHistoryYearOptions = (items: FloodHistoryItem[]) =>
+ Array.from(new Set(items.map((item) => item.floodYear)))
+ .sort((left, right) => Number(right) - Number(left));
+
+const resolveFloodHistoryNodeName = (node: any) =>
+ String(node?.text ?? node?.data?.lyrNm ?? node?.data?.name ?? node?.lyrNm ?? node?.name ?? "").trim();
+
+const resolveFloodHistoryLayerType = (node: any) =>
+ String(node?.data?.lyrTyNm ?? node?.data?.lyrTypeNm ?? node?.lyrTyNm ?? node?.lyrTypeNm ?? "").trim();
+
+export const isFloodHistoryNode = (node: any) => {
+ const name = resolveFloodHistoryNodeName(node);
+ const layerType = resolveFloodHistoryLayerType(node);
+ return name === FLOOD_HISTORY_LAYER_NAME || layerType === FLOOD_HISTORY_LAYER_TYPE;
+};
diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx
index 7bdddc77f..95f1cbb8a 100644
--- a/apps/sn/app/(map-layout)/map/page.tsx
+++ b/apps/sn/app/(map-layout)/map/page.tsx
@@ -80,6 +80,7 @@ import {
import { downloadAgedPipeCsv } from "./components/agedPipe/agedPipeExport";
import SinkholeDetailPanel from "./components/sinkhole/SinkholeDetailPanel";
import SinkholeLayerSettingPanel from "./components/sinkhole/SinkholeLayerSettingPanel";
+import FloodHistoryDetailPanel from "./components/floodHistory/FloodHistoryDetailPanel";
import {
applySinkholeNodeFilters,
applySinkholeYearFilters,
@@ -109,6 +110,15 @@ import {
type SinkholeNearbyPipe,
type SinkholeSyncSummary,
} from "./components/sinkhole/sinkholeTypes";
+import {
+ FLOOD_HISTORY_AREA_LAYER_ID,
+ FLOOD_HISTORY_DEPTH_LAYER_ID,
+ FLOOD_HISTORY_LAYER_TYPE,
+ FLOOD_HISTORY_ROW_SELECT_MESSAGE,
+ FLOOD_HISTORY_POPUP_KEY,
+ isFloodHistoryNode,
+ type FloodHistoryItem,
+} from "./components/floodHistory/floodHistoryTypes";
import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup";
import SearchModal from "./searchModal/searchModal";
import { WATER_DEPTH_LAYER_TYPES, WATER_DEPTH_LEGEND_ITEMS } from "./waterDepth/waterDepth";
@@ -157,6 +167,7 @@ const GIS_EDIT_VERTEX_LAYER_ID = "SN_GIS_EDIT_VERTEX_HANDLES";
const WATER_RISK_AREA_LAYER_ID = "SN_WATER_RISK_AREA";
const WATER_RISK_GRID_LAYER_ID = "SN_WATER_RISK_GRID";
const WATER_RISK_PIPE_LAYER_ID = "SN_WATER_RISK_PIPE";
+const FLOOD_HISTORY_SELECTION_PADDING: [number, number, number, number] = [100, 380, 120, 120];
type FlowDirectionUsageStyle = {
color: string;
@@ -727,6 +738,44 @@ const getSelectedFeatureLayerTypeName = (data: any) => {
return String(data?.lyrTypeNm ?? data?.values_?.lyrTypeNm ?? "").trim();
};
+const createFloodHistoryAreaStyle = (label: string) =>
+ new Style({
+ fill: new Fill({
+ color: "rgba(30, 136, 229, 0.16)",
+ }),
+ stroke: new Stroke({
+ color: "rgba(30, 136, 229, 0.95)",
+ width: 3,
+ }),
+ text: new Text({
+ text: label,
+ font: "700 12px sans-serif",
+ fill: new Fill({ color: "#ffffff" }),
+ backgroundFill: new Fill({ color: "rgba(15, 23, 42, 0.88)" }),
+ padding: [4, 7, 4, 7],
+ overflow: true,
+ }),
+ zIndex: 9994,
+ });
+
+const createFloodHistoryDepthStyle = (label: string, color: string) =>
+ new Style({
+ fill: new Fill({
+ color,
+ }),
+ stroke: new Stroke({
+ color: "rgba(71, 85, 105, 0.72)",
+ width: 1.4,
+ }),
+ text: new Text({
+ text: label,
+ font: "700 11px sans-serif",
+ fill: new Fill({ color: "#0f172a" }),
+ overflow: true,
+ }),
+ zIndex: 9993,
+ });
+
const normalizeGisEditFid = (value: unknown) => {
const raw = String(value ?? "").trim();
if (!raw) return "";
@@ -2242,6 +2291,7 @@ const Map = () => {
const [mapReady, setMapReady] = useState(false);
const [isAgedPipeChecked, setIsAgedPipeChecked] = useState(false);
const [isSinkholeChecked, setIsSinkholeChecked] = useState(false);
+ const [isFloodHistoryChecked, setIsFloodHistoryChecked] = useState(false);
const [draftAgedPipeFilters, setDraftAgedPipeFilters] =
useState(AGED_PIPE_DEFAULT_FILTERS);
const [appliedAgedPipeFilters, setAppliedAgedPipeFilters] =
@@ -2251,6 +2301,8 @@ const Map = () => {
);
const [isSinkholePopupFilterActive, setIsSinkholePopupFilterActive] = useState(false);
const [sinkholeRadiusMeters, setSinkholeRadiusMeters] = useState(SINKHOLE_DEFAULT_RADIUS);
+ const [selectedFloodHistoryDetail, setSelectedFloodHistoryDetail] = useState(null);
+ const [floodHistoryDetailPopupPosition, setFloodHistoryDetailPopupPosition] = useState<{ left: number; top: number } | null>(null);
const [selectedSinkholeDetail, setSelectedSinkholeDetail] = useState(null);
const [selectedSinkholeNearbyPipes, setSelectedSinkholeNearbyPipes] = useState([]);
const [lastSinkholeSyncSummary, setLastSinkholeSyncSummary] = useState(null);
@@ -2269,6 +2321,9 @@ const Map = () => {
const shouldFocusAgedPipeOnNextSelectRef = useRef(false);
const agedPipePopupWindowRef = useRef(null);
const sinkholePopupWindowRef = useRef(null);
+ // 침수발생이력 overlay layer는 지도 드래그 중에도 유지돼야 하므로 React state 대신 ref로 보관한다.
+ const floodHistoryAreaLayerRef = useRef | null>(null);
+ const floodHistoryDepthLayerRef = useRef | null>(null);
const sinkholePopupCloseWatcherRef = useRef(null);
const pipeAnalysisPopupWindowRef = useRef(null);
const gisEditSessionPopupWindowRef = useRef(null);
@@ -3857,6 +3912,121 @@ const Map = () => {
});
}, []);
+ const ensureFloodHistoryAreaSource = useCallback(() => {
+ if (!mapReady || !gisMaps.current) return null;
+
+ if (!floodHistoryAreaLayerRef.current) {
+ const source = new VectorSource();
+ const layer = new VectorLayer({
+ source,
+ zIndex: 9993,
+ });
+ layer.set("id", FLOOD_HISTORY_AREA_LAYER_ID);
+ layer.set("lyrNm", "침수흔적도");
+ layer.set("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE);
+ layer.set("clickPsblYn", "N");
+ gisMaps.current.addLayer(layer);
+ floodHistoryAreaLayerRef.current = layer;
+ }
+
+ return floodHistoryAreaLayerRef.current.getSource();
+ }, [mapReady]);
+
+ const ensureFloodHistoryDepthSource = useCallback(() => {
+ if (!mapReady || !gisMaps.current) return null;
+
+ if (!floodHistoryDepthLayerRef.current) {
+ const source = new VectorSource();
+ const layer = new VectorLayer({
+ source,
+ zIndex: 9992,
+ });
+ layer.set("id", FLOOD_HISTORY_DEPTH_LAYER_ID);
+ layer.set("lyrNm", "침수흔적도");
+ layer.set("lyrTyNm", FLOOD_HISTORY_LAYER_TYPE);
+ layer.set("clickPsblYn", "N");
+ gisMaps.current.addLayer(layer);
+ floodHistoryDepthLayerRef.current = layer;
+ }
+
+ return floodHistoryDepthLayerRef.current.getSource();
+ }, [mapReady]);
+
+ const clearFloodHistoryLayers = useCallback(() => {
+ floodHistoryAreaLayerRef.current?.getSource?.()?.clear?.();
+ floodHistoryDepthLayerRef.current?.getSource?.()?.clear?.();
+ }, []);
+
+ const renderSelectedFloodHistory = useCallback(
+ (detail: FloodHistoryItem | null) => {
+ const areaSource = ensureFloodHistoryAreaSource();
+ const depthSource = ensureFloodHistoryDepthSource();
+ if (!areaSource || !depthSource) return;
+
+ areaSource.clear();
+ depthSource.clear();
+
+ if (!detail) return;
+
+ const areaFeature = new Feature({
+ geometry: new Polygon([detail.areaRing]),
+ });
+ areaFeature.setStyle(createFloodHistoryAreaStyle(`${detail.floodYear}년 ${detail.districtName}`));
+ areaSource.addFeature(areaFeature);
+
+ detail.depthZones.forEach((zone) => {
+ const zoneFeature = new Feature({
+ geometry: new Polygon([zone.ring]),
+ });
+ zoneFeature.setStyle(createFloodHistoryDepthStyle(zone.depthLabel, zone.color));
+ depthSource.addFeature(zoneFeature);
+ });
+ },
+ [ensureFloodHistoryAreaSource, ensureFloodHistoryDepthSource],
+ );
+
+ const focusFloodHistoryOnMap = useCallback((detail: FloodHistoryItem) => {
+ if (!gisMaps.current) return;
+
+ const geometry = new Polygon([detail.areaRing]);
+ gisMaps.current.getView?.().fit?.(geometry.getExtent(), {
+ duration: 350,
+ padding: FLOOD_HISTORY_SELECTION_PADDING,
+ maxZoom: 15,
+ constrainResolution: true,
+ nearest: true,
+ });
+ }, []);
+
+ const clearFloodHistorySelection = useCallback(() => {
+ setSelectedFloodHistoryDetail(null);
+ setFloodHistoryDetailPopupPosition(null);
+ clearFloodHistoryLayers();
+ }, [clearFloodHistoryLayers]);
+
+ const closeFloodHistoryDetail = useCallback(() => {
+ setFloodHistoryDetailPopupPosition(null);
+ }, []);
+
+ const handleFloodHistoryRowSelect = useCallback(
+ (detail: FloodHistoryItem) => {
+ setSelectedFloodHistoryDetail(detail);
+ renderSelectedFloodHistory(detail);
+ focusFloodHistoryOnMap(detail);
+ setIsSearchModalOpen(false);
+ },
+ [focusFloodHistoryOnMap, renderSelectedFloodHistory, setIsSearchModalOpen],
+ );
+
+ const openFloodHistoryPopupWindow = useCallback(() => {
+ const popup = openWindowPopup(FLOOD_HISTORY_POPUP_KEY as WindowPopupKey);
+ if (!popup) {
+ alert("브라우저 팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요.");
+ return null;
+ }
+ return popup;
+ }, []);
+
const buildSinkholeQueryString = useCallback(
(filters: SinkholeFilters, page = 0, size = SINKHOLE_DEFAULT_POINT_LIMIT, checkedNodesData = checkedNodesDataRef.current) =>
buildSinkholeListQueryString(applySinkholeYearFilters(filters, checkedNodesData), ctpv, sgg, page, size),
@@ -4329,6 +4499,18 @@ const Map = () => {
return () => window.removeEventListener("message", handleSinkholePopupMessage);
}, [loadSinkholeDetail, sinkholeScope]);
+ useEffect(() => {
+ const handleFloodHistoryPopupMessage = (event: MessageEvent) => {
+ if (event.origin !== window.location.origin) return;
+ if (event.data?.type !== FLOOD_HISTORY_ROW_SELECT_MESSAGE || !event.data?.row) return;
+
+ handleFloodHistoryRowSelect(event.data.row as FloodHistoryItem);
+ };
+
+ window.addEventListener("message", handleFloodHistoryPopupMessage);
+ return () => window.removeEventListener("message", handleFloodHistoryPopupMessage);
+ }, [handleFloodHistoryRowSelect]);
+
// WaterDepth 표시 여부를 결정하는 함수
const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
const waterDepthLayers = checkedNodesData.filter(node =>
@@ -4378,6 +4560,11 @@ const Map = () => {
handleWaterDepthVisibility(checkedIds, checkedNodesData);
setIsAgedPipeChecked(checkedNodesData.some((node) => isAgedPipeLayerNode(node)));
+ const floodHistoryChecked = checkedNodesData.some((node) => isFloodHistoryNode(node));
+ setIsFloodHistoryChecked(floodHistoryChecked);
+ if (!floodHistoryChecked) {
+ clearFloodHistorySelection();
+ }
const sinkholeChecked = checkedNodesData.some((node) => isSinkholeLayerNode(node));
setIsSinkholeChecked(sinkholeChecked);
if (sinkholeChecked) {
@@ -4387,7 +4574,7 @@ const Map = () => {
removeSinkholeLayer();
closeSinkholeDetail();
}
- }, [closeSinkholeDetail, handleWaterDepthVisibility, removeFlowDirectionLayer, removeSinkholeLayer, retainFlowDirectionUsageCodes]);
+ }, [clearFloodHistorySelection, closeSinkholeDetail, handleWaterDepthVisibility, removeFlowDirectionLayer, removeSinkholeLayer, retainFlowDirectionUsageCodes]);
const handleOpenLayerPopup = useCallback(
(_layerNm: string, node?: any) => {
@@ -4399,13 +4586,17 @@ const Map = () => {
openSinkholePopupWindow();
return false;
}
+ if (isFloodHistoryNode(node)) {
+ openFloodHistoryPopupWindow();
+ return false;
+ }
if (isSinkholeYearNode(node)) {
return false;
}
return true;
},
- [openAgedPipeList, openSinkholePopupWindow],
+ [openAgedPipeList, openFloodHistoryPopupWindow, openSinkholePopupWindow],
);
// 현재 체크된 특수 레이어만 하단 설정영역에 모아 렌더링하기 위한 패널 목록이다.
@@ -5191,9 +5382,11 @@ const Map = () => {
);
}, [
closeSinkholeDetail,
+ closeFloodHistoryDetail,
clearSinkholeFocusedPipeLayer,
focusSinkholeNearbyPipe,
handleApplyGisEditPipeBasicInfo,
+ selectedFloodHistoryDetail,
selectedSinkholeDetail,
selectedSinkholeNearbyPipes,
setIsSearchModalOpen,
@@ -6211,6 +6404,58 @@ const Map = () => {
renderSelectedSinkhole(selectedSinkholeDetail, sinkholeRadiusMeters);
}, [renderSelectedSinkhole, selectedSinkholeDetail, sinkholeRadiusMeters]);
+ useEffect(() => {
+ renderSelectedFloodHistory(selectedFloodHistoryDetail);
+ }, [renderSelectedFloodHistory, selectedFloodHistoryDetail]);
+
+ useEffect(() => {
+ if (!mapReady || !gisMaps.current) return;
+
+ const map = gisMaps.current;
+ const clickKey = map.on("singleclick", (event: any) => {
+ if (!selectedFloodHistoryDetail || !isFloodHistoryChecked) return;
+
+ let clickedFloodHistory = false;
+ map.forEachFeatureAtPixel(
+ event.pixel,
+ (feature: any, layer: any) => {
+ if (
+ layer === floodHistoryAreaLayerRef.current ||
+ layer === floodHistoryDepthLayerRef.current
+ ) {
+ clickedFloodHistory = true;
+ return feature;
+ }
+ return undefined;
+ },
+ { hitTolerance: 4 },
+ );
+
+ if (!clickedFloodHistory) return;
+
+ const size = map.getSize?.() ?? [1200, 800];
+ const popupWidth = 220;
+ const popupHeight = 210;
+ const nextLeft = Math.min(Math.max(event.pixel[0] + 12, 12), size[0] - popupWidth - 12);
+ const nextTop = Math.min(Math.max(event.pixel[1] - 12, 12), size[1] - popupHeight - 12);
+ setFloodHistoryDetailPopupPosition({
+ left: nextLeft,
+ top: nextTop,
+ });
+ setIsSearchModalOpen(false);
+ });
+
+ return () => {
+ unByKey(clickKey);
+ };
+ }, [isFloodHistoryChecked, mapReady, selectedFloodHistoryDetail, setIsSearchModalOpen]);
+
+ useEffect(() => {
+ return () => {
+ clearFloodHistoryLayers();
+ };
+ }, [clearFloodHistoryLayers]);
+
return (
<>
@@ -6337,6 +6582,13 @@ const Map = () => {
) : null}
+ {selectedFloodHistoryDetail && floodHistoryDetailPopupPosition ? (
+
+ ) : null}
setIsSearchModalOpen(true)}
@@ -6397,7 +6649,9 @@ const Map = () => {
showFloatingSearchButton={false}
treeTogglePosition="center"
zoomGuide={zoomGuide}
- layerIconVisible={(node: any) => isAgedPipeLayerNode(node) || isSinkholeRootNode(node)}
+ layerIconVisible={(node: any) =>
+ isAgedPipeLayerNode(node) || isSinkholeRootNode(node) || isFloodHistoryNode(node)
+ }
defaultZoom={7.5}
defaultLonLat={[129, 36.3]}
diff --git a/apps/sn/docs/작업검증/SFR-011-009-침수발생이력/001-qa-SFR-011-009-침수발생이력-통합검증계획.md b/apps/sn/docs/작업검증/SFR-011-009-침수발생이력/001-qa-SFR-011-009-침수발생이력-통합검증계획.md
new file mode 100644
index 000000000..e80066fa9
--- /dev/null
+++ b/apps/sn/docs/작업검증/SFR-011-009-침수발생이력/001-qa-SFR-011-009-침수발생이력-통합검증계획.md
@@ -0,0 +1,79 @@
+# SFR-011-009 침수발생이력 통합 검증 계획
+
+## 1. 문서 목적
+
+- `SFR-011-009 침수발생이력`의 FE 검증 계획을 기능당 1개 문서로 누적 관리한다.
+- 현재 문서는 docs-first 설계 완료 직후 기준으로, FE-only 범위 검증 항목과 후속 통합 검증 항목을 분리한다.
+
+## 2. 검증 기준 요약
+
+- 레이어 트리 `침수흔적도` 목록 popup이 정상적으로 열린다.
+- `침수흔적도` on/off가 지도 표시와 맞게 동작한다.
+- 조회조건 유효성 검증이 동작한다.
+- `침수발생년도` 선택이 동작한다.
+- 목록이 조건에 따라 필터링된다.
+- 목록 다운로드가 현재 조회 결과 기준으로 동작한다.
+- 행 클릭 시 메인 지도 preview가 뜬다.
+- 지도 위 큰 침수흔적 레이어 클릭 시 작은 상세 팝업이 열린다.
+
+## 3. FE-only 검증 계획
+
+### 3.1 popup 진입
+
+- 레이어 트리 `침수흔적도` 체크
+- 레이어 트리 `침수흔적도`의 목록 버튼 클릭
+- popup open 여부
+- popup 제목, 조회조건 영역, 목록 그리드 노출 여부
+
+### 3.2 조회조건 검증
+
+- 시도 미선택 상태에서 시군구 선택 후 조회 시 차단되는지 확인
+- 침수발생년도 선택 시 목록 필터가 반영되는지 확인
+- 침수재해명 키워드 입력 시 목록 필터가 적용되는지 확인
+
+### 3.3 목록/다운로드
+
+- 조회 결과 총 건수 표시
+- `강우 분석정보` 묶음 헤더와 하위 4개 컬럼 표시
+- 나머지 상세 필드가 가로 스크롤로 모두 노출되는지 확인
+- 10건 초과 시 페이지네이션 노출
+- 다운로드 버튼 클릭 시 현재 필터링 결과 기준 파일이 생성되는지 확인
+- 결과 없음 상태에서 다운로드가 차단되는지 확인
+
+### 3.4 지도 연동
+
+- 행 클릭 시 지도 preview 표시 여부
+- 행 더블클릭 시 popup close 여부
+- 메인 지도 영역으로 fit 이동되는지 확인
+- `침수흔적도` 체크 시 mock polygon과 침수심 overlay가 함께 표시되는지 확인
+- `침수흔적도`를 끄면 관련 표시가 함께 숨겨지는지 확인
+- 큰 침수흔적 polygon 클릭 시 상세 팝업이 뜨는지 확인
+
+### 3.5 지도 상세 팝업
+
+- `침수년도탭`, `재해명탭`, `침수발생강우탭` 전환
+- 기술 컬럼 제외 후 한글 업무 항목만 보이는지 확인
+- 각 탭의 테이블 구조와 값 표시
+- 닫기 시 팝업만 닫히는지 확인
+
+## 4. BE 연동 후 검증 계획
+
+- 실제 목록 조회 API 필드 매핑
+- 실제 geometry 및 지도 위치 정합성
+- 지도 click 상세조회
+- 실제 레이어 트리 `침수흔적도` 토글 연계
+- 다운로드 권한 및 파일 포맷 검증
+
+## 5. 차단 사유 분류 기준
+
+- `기능 결함`
+- `인증 차단`
+- `환경 차단`
+- `데이터 차단`
+- `자동화 한계`
+
+## 6. 현재 상태
+
+- 공통 설계서 작성 완료
+- FE 작업카드 작성 완료
+- 구현은 진행 중이며, 검증은 FE-only 기준으로 먼저 수행한다.
diff --git a/apps/sn/docs/작업검증/SFR-011-009-침수발생이력/002-qa-SFR-011-009-침수발생이력-통합검증결과.md b/apps/sn/docs/작업검증/SFR-011-009-침수발생이력/002-qa-SFR-011-009-침수발생이력-통합검증결과.md
new file mode 100644
index 000000000..ca7d05fd6
--- /dev/null
+++ b/apps/sn/docs/작업검증/SFR-011-009-침수발생이력/002-qa-SFR-011-009-침수발생이력-통합검증결과.md
@@ -0,0 +1,52 @@
+# SFR-011-009 침수발생이력 통합 검증 결과
+
+## 1. 문서 목적
+
+- `SFR-011-009 침수발생이력`의 현재 검증 결과와 남은 수동 확인 항목을 누적 관리한다.
+- 현재 문서는 FE draft 구현 직후 기준의 로컬 응답 확인과 자동화 한계 항목을 기록한다.
+
+## 2. 이번 라운드 결과 요약
+
+- 상태: 부분 확인
+- 확인 완료
+ - `localhost:3002` 기본 응답 200 확인
+ - `localhost:3002/map` 응답 200 확인
+ - 공통 설계서, FE 작업카드, FE 검증계획 작성 완료
+- 미완료
+ - popup 실제 진입 확인
+ - 목록 조회/행 클릭 preview/메인 지도 플로팅 상세팝업 수동 확인
+ - 다운로드 파일 수동 확인
+
+## 3. 세부 결과
+
+### 3.1 환경 확인
+
+- `pnpm --filter sn run dev`는 샌드박스 내에서 `spawn EPERM`으로 실패했다.
+- 권한 상승 후 로컬 서버를 열어 `http://127.0.0.1:3002`, `http://127.0.0.1:3002/map` 응답 200을 확인했다.
+
+### 3.2 자동/간접 확인
+
+| 항목 | 결과 | 메모 |
+| --- | --- | --- |
+| 로컬 서버 기본 응답 | 통과 | `/` 200 |
+| 지도 메인 경로 응답 | 통과 | `/map` 200 |
+| popup 경로 직접 요청 | 보류 | 초기 컴파일 지연 또는 자동 요청 timeout 가능성 |
+| 실제 popup 클릭 | 보류 | 사용자가 로컬 수동 검증 예정 |
+| 행 클릭 preview | 보류 | 사용자가 로컬 수동 검증 예정 |
+| 더블클릭 후 지도 이동 | 보류 | 사용자가 로컬 수동 검증 예정 |
+| 지도 클릭 플로팅 팝업 | 보류 | 사용자가 로컬 수동 검증 예정 |
+
+## 4. 남은 수동 검증 항목
+
+1. 레이어 트리 `침수흔적도` 목록 버튼으로 popup이 정상적으로 열리는지 확인
+2. 조회조건 입력과 목록 필터가 기대대로 동작하는지 확인
+3. 목록 행 클릭 시 메인 지도에 테스트 레이어가 보이는지 확인
+4. 목록 행 더블클릭 시 popup이 닫히고 메인 지도 이동이 유지되는지 확인
+5. 큰 침수흔적 polygon 클릭 시 작은 상세 팝업이 뜨는지 확인
+6. `침수흔적도` on/off에 따라 영역/침수심 overlay가 메인 지도에 맞게 보이는지 확인
+7. 상세 팝업 탭과 표 구성이 자연스러운지 확인
+
+## 5. 결론
+
+- FE draft 구현과 문서 선행 정리는 완료됐다.
+- 최종 상태는 사용자 로컬 수동 검증 결과를 반영한 뒤 `검증 통과` 또는 `추가 보완 필요`로 갱신한다.
diff --git a/apps/sn/docs/작업계획/SFR-011-009-침수발생이력/001-fe-SFR-011-009-침수발생이력-작업카드.md b/apps/sn/docs/작업계획/SFR-011-009-침수발생이력/001-fe-SFR-011-009-침수발생이력-작업카드.md
new file mode 100644
index 000000000..574acdb4d
--- /dev/null
+++ b/apps/sn/docs/작업계획/SFR-011-009-침수발생이력/001-fe-SFR-011-009-침수발생이력-작업카드.md
@@ -0,0 +1,117 @@
+# SFR-011-009 침수발생이력 작업카드
+
+## 1. 작업 개요
+
+- 작업 ID: `SFR-011-009-FE`
+- 작업명: 침수발생이력 프론트엔드 구현
+- 상태: 검증
+- 작성일: 2026-06-15
+- 담당 역할: FE
+- 관련 역할: DE, QA
+- 담당 범위: `apps/sn` 지도 화면에서 레이어 트리 `침수흔적도` 목록 popup, 목록 조회, 지도 preview, 지도 클릭 플로팅 상세 팝업, mock 침수심 overlay 구현
+- 근거 문서
+ - `COMMON_DOCS_ROOT/작업계획/SFR-011-009-침수발생이력/001-de-SFR-011-009-침수발생이력-설계.md`
+ - `COMMON_DOCS_ROOT/작업지시/SFR-011-009-침수발생이력/0.침수발생이력_요구사항명세서_v2.0.docx`
+ - `COMMON_DOCS_ROOT/작업지시/SFR-011-009-침수발생이력/5.침수발생이력_화면시나리오_V2.0.md.docx`
+ - `COMMON_DOCS_ROOT/작업지시/SFR-011-009-침수발생이력/침수발생이력 목록화면설계서.png`
+ - `COMMON_DOCS_ROOT/작업지시/SFR-011-009-침수발생이력/침수발생이력 지도상세정보화면설계서.png`
+ - `COMMON_DOCS_ROOT/작업지시/SFR-011-009-침수발생이력/침수발생이력_화면.html`
+
+## 1-1. 공통 동기화 확인
+
+- 공통 작업현황판 확인 여부: 확인
+- 파일점유현황 확인 여부: 확인
+- 결정로그 확인 여부: 확인
+- 기존 진행 중 작업과의 관계: `map/page.tsx`, `windowPopup/**`는 다른 지도형 기능과 최근 수정 이력이 있어 충돌 주의가 필요하다.
+
+## 2. 작업 목적
+
+- 레이어 트리 `침수흔적도` 목록 진입을 placeholder가 아닌 실제 popup UI로 연결한다.
+- popup 조회 결과를 메인 지도와 연동해 사용자가 선택한 침수발생이력을 즉시 확인할 수 있게 한다.
+- 설계 이미지의 `침수흔적도 on/off + 목록 popup + 지도 강조 + 지도 위 상세 팝업` 흐름을 현재 `sn` 지도형 구조 안에서 자연스럽게 재현한다.
+
+## 3. 수정 대상 파일
+
+| 구분 | 파일 경로 | 수정 여부 | 비고 |
+| --- | --- | --- | --- |
+| 메인 지도 | `FE_APP_ROOT/app/(map-layout)/map/page.tsx` | 진행 | popup 선택 결과 수신, 지도 overlay, 우측 상세패널 연결 |
+| popup 정의 | `FE_APP_ROOT/src/features/windowPopup/model/popupDefinitions.ts` | 진행 | tree 목록 진입 popup 키/크기 정의 |
+| popup 등록 | `FE_APP_ROOT/src/features/windowPopup/registry.tsx` | 진행 | placeholder에서 실제 popup content로 교체 |
+| 분석도구 메뉴 | `FE_APP_ROOT/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx` | 진행 | 분석도구에서 `침수발생이력` 제거 |
+| popup content | `FE_APP_ROOT/src/features/windowPopup/ui/FloodHistoryPopupContent.tsx` | 진행 | popup 본문 연결 |
+| 신규 popup 페이지 | `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage.tsx` | 진행 | 조회조건/목록/다운로드 |
+| 신규 상세패널 | `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/FloodHistoryDetailPanel.tsx` | 진행 | 기본정보/발생일시/원인·피해 탭 |
+| 신규 타입/샘플데이터 | `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts` | 진행 | mock 데이터, postMessage 타입, legend 정의 |
+
+## 4. 구현 범위
+
+### 4.1 이번 라운드 포함
+
+- 레이어 트리 `침수흔적도` 목록 popup 진입
+- 레이어 트리 `침수흔적도` on/off
+- 시도/시군구/침수발생년도 선택/침수재해명 조회조건
+- mock 목록 조회/페이징
+- 목록 다운로드
+- 목록 행 클릭 preview + 더블클릭 후 메인 지도 이동
+- 메인 지도 mock polygon/침수심 overlay
+- 지도 클릭 플로팅 상세 팝업
+- `강우 분석정보` 묶음 헤더 유지 + 상세 필드 가로 스크롤 그리드
+
+### 4.2 이번 라운드 제외
+
+- 실제 백엔드 API 연동
+- 실제 레이어 트리 토글 연계
+- 지도 click 상세조회
+- 실제 기준데이터 기반 GIS layer 렌더링
+
+## 5. 화면 흐름
+
+1. 사용자가 레이어 트리에서 `침수흔적도`를 체크한다.
+2. 사용자가 `침수흔적도` 목록 버튼을 클릭한다.
+3. popup이 열린다.
+4. 사용자가 조회조건을 입력하고 `조회`를 클릭한다.
+5. 목록이 popup 그리드에 표시된다.
+6. 사용자가 행을 클릭하면 메인 지도에 테스트 레이어가 먼저 표시된다.
+7. 사용자가 행을 더블클릭하면 popup이 닫히고 메인 지도는 해당 침수 이력 영역으로 이동한다.
+8. `침수흔적도` 체크 상태에 따라 영역/침수심 표현이 함께 보인다.
+9. 메인 지도 위 큰 침수흔적 영역을 클릭하면 작은 상세 팝업이 열린다.
+
+## 6. 구현 메모
+
+- popup은 기존 `windowPopup` 체계를 그대로 사용하되, 분석도구가 아니라 레이어 트리 `침수흔적도` 목록 버튼으로 연다.
+- 목록 선택 결과는 `window.opener.postMessage`로 메인 지도에 전달한다.
+- popup의 `침수발생년도`는 같은 필터 줄 안의 선택 컴포넌트로 처리한다.
+- 메인 지도는 `VectorLayer` 두 개를 사용해
+ - 침수발생이력상세 영역
+ - 침수심 표현 영역
+ 을 임시 렌더링한다.
+- 두 overlay는 `침수흔적도` 체크 상태를 따르도록 맞춘다.
+- 상세정보는 우측 패널이 아니라 지도 위 플로팅 팝업으로 노출하고, 탭은 `침수년도탭`, `재해명탭`, `침수발생강우탭` 기준으로 보여준다.
+
+## 7. 검증 방법
+
+- 레이어 트리 `침수흔적도` 목록 popup이 열린다.
+- `침수흔적도` 체크 on/off에 따라 영역/침수심 overlay가 함께 바뀐다.
+- 시도 없이 시군구를 선택하면 조회가 차단된다.
+- 침수발생년도 선택이 동작한다.
+- 조건 변경 시 목록 필터가 바뀐다.
+- 행 클릭 시 지도 preview가 표시된다.
+- 지도 위 큰 polygon 클릭 시 작은 상세 팝업이 열린다.
+- 상세 팝업 탭 전환이 동작한다.
+
+## 8. 현재 메모
+
+- 문서 우선 원칙에 맞춰 이번 라운드는 설계서/작업카드/검증계획을 먼저 정리한 뒤 구현을 이어간다.
+- 현재 코드에는 일부 draft 구현이 이미 들어가 있으므로, 이후 단계에서 이 작업카드 기준으로 정합성을 다시 맞춘다.
+
+## 9. 롤백 방법
+
+- 레이어 트리 목록 진입 연결 해제
+- `floodHistory` 신규 컴포넌트 삭제
+- `map/page.tsx`의 floodHistory overlay/상세패널 분기 제거
+
+## 10. 남은 리스크
+
+- 실제 API 필드명이 mock 데이터와 다를 수 있다.
+- 실제 지도 레이어 기준데이터가 준비되면 현재 mock overlay 방식과 상태 관리가 달라질 수 있다.
+- popup 상세정보를 우측 패널로 둘지 지도 내부 floating popup으로 둘지는 실제 사용성 피드백 후 재조정 가능성이 있다.
diff --git a/apps/sn/docs/작업내용/SFR-011-009-침수발생이력/001-fe-SFR-011-009-침수발생이력-통합구현기록.md b/apps/sn/docs/작업내용/SFR-011-009-침수발생이력/001-fe-SFR-011-009-침수발생이력-통합구현기록.md
new file mode 100644
index 000000000..db4e62129
--- /dev/null
+++ b/apps/sn/docs/작업내용/SFR-011-009-침수발생이력/001-fe-SFR-011-009-침수발생이력-통합구현기록.md
@@ -0,0 +1,73 @@
+# SFR-011-009 침수발생이력 통합 구현 기록
+
+## 1. 문서 목적
+
+- `SFR-011-009 침수발생이력`의 FE 구현 이력과 남은 리스크를 기능당 1개 문서로 누적 관리한다.
+- 현재 문서는 docs-first 재정렬 이후의 1차 FE draft 구현 상태를 기록한다.
+
+## 2. 입력 근거
+
+- `COMMON_DOCS_ROOT/작업계획/SFR-011-009-침수발생이력/001-de-SFR-011-009-침수발생이력-설계.md`
+- `COMMON_DOCS_ROOT/작업내용/SFR-011-009-침수발생이력/001-fe-SFR-011-009-침수발생이력-통합공유기록.md`
+- `FE_APP_DOCS_ROOT/작업계획/SFR-011-009-침수발생이력/001-fe-SFR-011-009-침수발생이력-작업카드.md`
+
+## 3. 구현 라운드 요약
+
+### 3.1 2026-06-15 docs-first 재정렬
+
+- 수행 내용
+ - 작업지시 DOCX, 시나리오 DOCX, 설계 PNG, HTML 시안을 먼저 읽고 기능 범위를 재정리했다.
+ - 공통 설계서, FE 작업카드, FE 통합검증계획을 먼저 작성했다.
+ - 공통 작업현황판/파일점유현황에 `SFR-011-009-DE-FE-PLAN` 진행 상태를 등록했다.
+
+### 3.2 2026-06-15 FE draft 구현
+
+- 반영 파일
+ - `FE_APP_ROOT/app/(map-layout)/map/page.tsx`
+ - `FE_APP_ROOT/app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx`
+ - `FE_APP_ROOT/src/features/windowPopup/model/popupDefinitions.ts`
+ - `FE_APP_ROOT/src/features/windowPopup/registry.tsx`
+ - `FE_APP_ROOT/src/features/windowPopup/ui/FloodHistoryPopupContent.tsx`
+ - `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage.tsx`
+ - `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/FloodHistoryDetailPanel.tsx`
+ - `FE_APP_ROOT/app/(map-layout)/map/components/floodHistory/floodHistoryTypes.ts`
+- 구현 내용
+ - 분석도구의 `침수발생이력` 항목은 제거하고, 레이어 트리 `침수흔적도` 목록 진입으로 popup을 열도록 바꿨다.
+ - `침수발생이력` popup placeholder를 실제 popup 페이지로 교체했다.
+ - 시도/시군구/침수발생년도 선택/침수재해명 조회조건과 mock 목록 조회/다운로드를 연결했다.
+ - 그리드는 현재 파란 톤을 유지하되, 사용안함 항목을 제외한 상세 필드를 모두 노출하도록 바꾸고 가로 스크롤을 허용했다. `강우 분석정보` 묶음은 유지하면서 뒤쪽 컬럼을 재배치했다.
+ - 행 클릭 시 popup을 유지한 채 `postMessage`로 메인 지도에 mock preview를 보내고, 더블클릭 시 popup을 닫도록 구성했다.
+ - 메인 지도는 mock polygon layer와 침수심 표현 layer를 임시 `VectorLayer`로 그리도록 구성했고, 표시 여부는 `침수흔적도` 체크 상태를 따르도록 보정했다.
+ - 메인 지도 우측 상세패널은 제거하고, 지도 위 큰 침수흔적 영역 클릭 시 작은 플로팅 팝업이 열리도록 바꿨다.
+ - 상세 팝업 탭은 `침수년도탭`, `재해명탭`, `침수발생강우탭` 구조에 맞춰 재배치했고, 탭별 표 항목도 캡처 기준에 가깝게 맞췄다.
+
+## 4. 기능별 현재 상태
+
+### 4.1 popup 조회
+
+- FE-only draft 구현 완료
+- 레이어 트리 `침수흔적도` 목록 진입 기준
+- 시도/시군구/년도/재해명 조건과 목록 필터, 페이지네이션, 다운로드를 포함한다.
+
+### 4.2 지도 이동 및 강조
+
+- FE-only draft 구현 완료
+- 더블클릭 시 popup close 후 메인 지도 fit 이동과 mock polygon/침수심 표현 overlay를 수행한다.
+- `침수흔적도` 체크 상태에 따라 영역과 침수심 overlay를 함께 표시/숨김한다.
+
+### 4.3 지도 상세 팝업
+
+- FE-only draft 구현 완료
+- 선택 이력 기준 3탭 플로팅 팝업을 제공한다.
+
+## 5. 현재 검증 메모
+
+- 로컬 `localhost:3002` 응답은 확인했다.
+- `next dev`는 샌드박스 내에서 `spawn EPERM`이 발생해 권한 상승 후 로컬 서버를 띄웠다.
+- 사용자가 직접 로컬 화면을 수동 검증할 예정이므로, popup 실제 진입/더블클릭/상세패널 체감 확인은 후속 결과를 받아 갱신한다.
+
+## 6. 남은 리스크
+
+- popup 라우트의 초기 컴파일 시간이 길어 자동 요청에서 timeout이 발생할 수 있다.
+- `침수흔적도` 기준데이터 이름/구조와 FE 판별식이 다를 수 있다.
+- 실제 API 필드가 확정되면 상세패널 항목명/정렬이 일부 바뀔 수 있다.
diff --git a/apps/sn/src/features/windowPopup/model/popupDefinitions.ts b/apps/sn/src/features/windowPopup/model/popupDefinitions.ts
index 5724edd3a..dcba0e396 100644
--- a/apps/sn/src/features/windowPopup/model/popupDefinitions.ts
+++ b/apps/sn/src/features/windowPopup/model/popupDefinitions.ts
@@ -104,8 +104,8 @@ export const windowPopupDefinitions = {
floodHistoryPopup: {
key: "floodHistoryPopup",
title: "침수발생이력",
- width: 1080,
- height: 760,
+ width: 1160,
+ height: 820,
},
gisEditPopup: {
key: "gisEditPopup",
diff --git a/apps/sn/src/features/windowPopup/registry.tsx b/apps/sn/src/features/windowPopup/registry.tsx
index aa65d06d2..f63b08a91 100644
--- a/apps/sn/src/features/windowPopup/registry.tsx
+++ b/apps/sn/src/features/windowPopup/registry.tsx
@@ -14,24 +14,19 @@ import SinkholePopupContent from "./ui/SinkholePopupContent";
import SscDetailPopupContent from "./ui/SscDetailPopupContent";
import type { WindowPopupContentProps } from "./model/types";
import FeaturePlaceholderPopupContent from "./ui/FeaturePlaceholderPopupContent";
+import FloodHistoryPopupContent from "./ui/FloodHistoryPopupContent";
import GisEditSessionPopupContent from "./ui/GisEditSessionPopupContent";
import GisManagementPopupContent from "./ui/GisManagementPopupContent";
import PipeAnalysisPopupContent from "./ui/PipeAnalysisPopupContent";
import PipeAnalysisDetailPopupContent from "./ui/PipeAnalysisDetailPopupContent";
import WaterRiskPopupContent from "./ui/WaterRiskPopupContent";
-const FloodPredictionMapPopupContent = (_props: WindowPopupContentProps) => (
+const FloodPredictionPlaceholderPopupContent = (_props: WindowPopupContentProps) => (
);
-const FloodHistoryPopupContent = (_props: WindowPopupContentProps) => (
-
-);
-
const GisEditPopupContent = (_props: WindowPopupContentProps) => (
);
@@ -95,7 +90,7 @@ export const windowPopupRegistry = {
},
floodPredictionMapPopup: {
...windowPopupDefinitions.floodPredictionMapPopup,
- component: FloodPredictionMapPopupContent,
+ component: FloodPredictionPlaceholderPopupContent,
},
floodHistoryPopup: {
...windowPopupDefinitions.floodHistoryPopup,
diff --git a/apps/sn/src/features/windowPopup/ui/FloodHistoryPopupContent.tsx b/apps/sn/src/features/windowPopup/ui/FloodHistoryPopupContent.tsx
new file mode 100644
index 000000000..d363c7ecf
--- /dev/null
+++ b/apps/sn/src/features/windowPopup/ui/FloodHistoryPopupContent.tsx
@@ -0,0 +1,5 @@
+import FloodHistoryPopupPage from "@/app/(map-layout)/map/components/floodHistory/FloodHistoryPopupPage";
+
+export default function FloodHistoryPopupContent() {
+ return ;
+}
diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js
index f931b06b7..6d9da766d 100644
--- a/packages/common/business_components/src/ui/map/GisTree.js
+++ b/packages/common/business_components/src/ui/map/GisTree.js
@@ -550,9 +550,13 @@ export default React.memo(function GisTree({
(child) =>
child.parent === node.id && child.data.exprsYn === "Y"
);
+ const nodeLabel = String(node?.data?.lyrNm ?? "").trim();
const isSinkholeRootTreeNode =
String(node.id) === "SN_SINKHOLE" ||
- String(node?.data?.lyrNm ?? "").trim() === "지반침하정보";
+ nodeLabel === "지반침하정보";
+ const isFloodHistoryRootTreeNode = nodeLabel === "침수흔적도";
+ const hideTreeLegendIcon =
+ isSinkholeRootTreeNode || isFloodHistoryRootTreeNode;
const showLayerActionButton = layerIconVisible?.(node);
const showLayerActionIcon = depth > 0 && imageVisible;
@@ -582,7 +586,7 @@ export default React.memo(function GisTree({
onChange={() => handleCheck(node.id)}
className="cursor-pointer"
/>
- {!isSinkholeRootTreeNode && node.icon && {node.icon}}
+ {!hideTreeLegendIcon && node.icon && {node.icon}}