From 9604e4591bef18bb97decce635985ce86af6c571 Mon Sep 17 00:00:00 2001 From: kiminchan Date: Wed, 13 May 2026 11:11:54 +0900 Subject: [PATCH] =?UTF-8?q?=EB=85=B8=ED=9B=84=EA=B4=80=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20-=20=EC=8B=9C=EB=8F=84=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EC=8B=9C=20=EC=A7=80=EB=8F=84=20=EC=A4=8C=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/agedPipe/AgedPipePopupPage.tsx | 21 +++-- .../map/components/agedPipe/agedPipeUtils.ts | 2 +- apps/sn/app/(map-layout)/map/page.tsx | 78 ++++++++++++++++++- 3 files changed, 94 insertions(+), 7 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx index d7fe9dc1f..a1c0c8d24 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx +++ b/apps/sn/app/(map-layout)/map/components/agedPipe/AgedPipePopupPage.tsx @@ -24,6 +24,7 @@ import { downloadAgedPipeCsv } from "./agedPipeExport"; const POPUP_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT"; const POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE"; const POPUP_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC"; +const POPUP_CTPV_FOCUS_MESSAGE = "SN_AGED_PIPE_CTPV_FOCUS"; export default function AgedPipePopupPage() { const searchParams = useSearchParams(); @@ -123,7 +124,7 @@ export default function AgedPipePopupPage() { return () => window.removeEventListener("message", handleFilterSyncMessage); }, [loadRows]); - const handleSearch = (page = 1) => { + const handleSearch = async (page = 1, shouldFocusCtpv = false) => { if (!validateElapsedYears(Number(filters.elapsedYears))) { alert("경과년도는 1년~100년 범위 내에서 입력해주세요."); return; @@ -131,7 +132,17 @@ export default function AgedPipePopupPage() { const normalized = normalizeAgedPipeFilters(filters); setFilters(normalized); - loadRows(page, normalized); + await loadRows(page, normalized); + + if (shouldFocusCtpv && normalized.ctpvCd !== "0") { + window.opener?.postMessage( + { + type: POPUP_CTPV_FOCUS_MESSAGE, + ctpvCd: normalized.ctpvCd, + }, + window.location.origin, + ); + } }; const handleRowClick = (index?: number) => { @@ -239,7 +250,7 @@ export default function AgedPipePopupPage() { }); }} onKeyDown={(event) => { - if (event.key === "Enter") handleSearch(1); + if (event.key === "Enter") void handleSearch(1, true); }} /> @@ -288,7 +299,7 @@ export default function AgedPipePopupPage() { size="sm" color="primary" className="h-10 px-5 !text-[1.1rem]" - onClick={() => handleSearch(1)} + onClick={() => void handleSearch(1, true)} > 조회 @@ -311,7 +322,7 @@ export default function AgedPipePopupPage() { pageSize={10} total={total} cellClick={(index) => handleRowClick(index)} - onPageChange={(page) => handleSearch(page)} + onPageChange={(page) => void handleSearch(page)} curPage={currentPage} index={selectedIndex} widthFixed={true} diff --git a/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts b/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts index bfe8d5023..2c51616b5 100644 --- a/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts +++ b/apps/sn/app/(map-layout)/map/components/agedPipe/agedPipeUtils.ts @@ -10,7 +10,7 @@ import { export const AGED_PIPE_LAYER_TYPE = "tb_ms_swage_ppl"; export const AGED_PIPE_DEFAULT_ELAPSED_YEARS = 30; export const AGED_PIPE_PAGE_SIZE = 10; -export const AGED_PIPE_MIN_ZOOM = 14; +export const AGED_PIPE_MIN_ZOOM = 13; export const AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES = ["19000101"]; export type AgedPipeFilters = { diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 7e9450b90..9eb592abc 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -68,6 +68,10 @@ const AGED_PIPE_HIGHLIGHT_LAYER_ID = "SN_AGED_PIPE_HIGHLIGHT"; const AGED_PIPE_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT"; const AGED_PIPE_POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE"; const AGED_PIPE_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC"; +// 노후관로 팝업에서 조회 버튼 클릭 시, 부모 지도를 선택 시도 범위로 이동시키는 메시지이다. +const AGED_PIPE_CTPV_FOCUS_MESSAGE = "SN_AGED_PIPE_CTPV_FOCUS"; +// 시도 기준 자동 이동은 조회 직후 노후관로가 바로 보이는 수준까지 한 단계 더 당겨 고정한다. +const AGED_PIPE_CTPV_FOCUS_ZOOM = 14; const SOUTH_KOREA_VIEW_EXTENT: [number, number, number, number] = [ 720000, 1445000, @@ -952,6 +956,72 @@ const Map = () => { } }, [draftAgedPipeFilters, isAgedPipeListOpen, loadAgedPipeList, syncAgedPipePopupFilters]); + // 노후관로 팝업에서 시도 기준 조회를 실행하면, 부모 지도창을 해당 시도 extent로 이동시킨다. + const focusMapToAgedPipeCtpv = useCallback(async (ctpvCd: string) => { + const normalizedCtpvCd = String(ctpvCd ?? "").replace(/[^0-9]/g, "").slice(0, 2); + if (!normalizedCtpvCd || normalizedCtpvCd === "0") return; + + const map = gisMaps.current; + if (!map) return; + + try { + const params = new URLSearchParams({ + service: "WFS", + version: "1.0.0", + request: "GetFeature", + typeName: "hasudo:tb_ms_ctpv", + outputFormat: "application/json", + CQL_FILTER: `mctpv_cd='${normalizedCtpvCd}'`, + }); + const payload = await getRawJson(`/geoserver/hasudo/ows?${params.toString()}`); + // 시도 WFS는 환경마다 3857/4326 등 다른 CRS로 내려올 수 있으므로 응답 crs를 우선 신뢰한다. + const responseCrsName = String(payload?.crs?.properties?.name ?? ""); + const resolvedDataProjection = responseCrsName.includes("EPSG::") + ? `EPSG:${responseCrsName.split("EPSG::")[1]}` + : responseCrsName.includes("EPSG:") + ? responseCrsName.slice(responseCrsName.lastIndexOf("EPSG:")) + : GisApp.FROM_PROJ; + const features = new GeoJSON().readFeatures(payload, { + dataProjection: resolvedDataProjection, + featureProjection: map.getView().getProjection(), + }); + + let extent: [number, number, number, number] | null = null; + features.forEach((feature) => { + const geometry = feature.getGeometry(); + if (!geometry) return; + const geometryExtent = geometry.getExtent(); + if (!extent) { + extent = [...geometryExtent] as [number, number, number, number]; + return; + } + extent = [ + Math.min(extent[0], geometryExtent[0]), + Math.min(extent[1], geometryExtent[1]), + Math.max(extent[2], geometryExtent[2]), + Math.max(extent[3], geometryExtent[3]), + ]; + }); + + if (!extent) return; + + const view = map.getView(); + view.fit(extent, { + padding: AGED_PIPE_FOCUS_PADDING, + maxZoom: 18, + constrainResolution: true, + nearest: true, + }); + + const fittedZoom = view.getZoom() ?? 0; + if (fittedZoom < AGED_PIPE_CTPV_FOCUS_ZOOM) { + view.setZoom(AGED_PIPE_CTPV_FOCUS_ZOOM); + } + } catch (error) { + console.error("노후관로 시도 이동 중 오류:", error); + } + }, []); + const openAgedPipePopupWindow = useCallback(() => { const popup = openWindowPopup("agedPipePopup", { elapsedYears: String(appliedAgedPipeFilters.elapsedYears), @@ -1067,6 +1137,12 @@ const Map = () => { return; } + if (event.data?.type === AGED_PIPE_CTPV_FOCUS_MESSAGE) { + if (!event.data?.ctpvCd) return; + void focusMapToAgedPipeCtpv(String(event.data.ctpvCd)); + return; + } + if (event.data?.type === PIPE_ANALYSIS_DETAIL_ROW_SELECT_MESSAGE) { if (!event.data?.row) return; handlePipeAnalysisRowSelect(event.data.row as PipeAnalysisDetailRow); @@ -1075,7 +1151,7 @@ const Map = () => { window.addEventListener("message", handleAgedPipePopupMessage); return () => window.removeEventListener("message", handleAgedPipePopupMessage); - }, [agedPipeTotalCount, handleAgedPipeRowDoubleClick, handlePipeAnalysisRowSelect]); + }, [agedPipeTotalCount, focusMapToAgedPipeCtpv, handleAgedPipeRowDoubleClick, handlePipeAnalysisRowSelect]); // WaterDepth 표시 여부를 결정하는 함수 const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => {