diff --git a/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts b/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts index a7f5088d6..11984c8e1 100644 --- a/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts +++ b/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts @@ -1,4 +1,4 @@ -"use client"; +"use client"; import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react"; import GeoJSON from "ol/format/GeoJSON.js"; @@ -161,6 +161,33 @@ const getFirstFeatureId = (payload: any) => { return String(firstFeature?.id ?? "").trim(); }; +const getOilqPipeExtent = (features: any[]) => { + const oilqFeatures = features.filter((feature) => Boolean(feature?.get?.("hasOilq"))); + if (oilqFeatures.length === 0) return null; + + let nextExtent: [number, number, number, number] | null = null; + oilqFeatures.forEach((feature) => { + const geometryExtent = feature?.getGeometry?.()?.getExtent?.(); + if (!Array.isArray(geometryExtent) || geometryExtent.some((value: number) => !Number.isFinite(value))) { + return; + } + + if (!nextExtent) { + nextExtent = [...geometryExtent] as [number, number, number, number]; + return; + } + + nextExtent = [ + Math.min(nextExtent[0], geometryExtent[0]), + Math.min(nextExtent[1], geometryExtent[1]), + Math.max(nextExtent[2], geometryExtent[2]), + Math.max(nextExtent[3], geometryExtent[3]), + ]; + }); + + return nextExtent; +}; + export const useInftwtResultMap = ({ gisMaps, setFeatureData, @@ -198,7 +225,7 @@ export const useInftwtResultMap = ({ }); layer.set("id", INFTWT_ZONE_LAYER_ID); - layer.set("lyrNm", "침입수분석-처리구역"); + layer.set("lyrNm", "移⑥엯?섎텇??泥섎━援ъ뿭"); layer.set("lyrTyNm", "tb_ms_swage_prcs_zone"); layer.set("clickPsblYn", "N"); layer.setVisible?.(true); @@ -224,7 +251,7 @@ export const useInftwtResultMap = ({ }); layer.set("id", INFTWT_PIPE_LAYER_ID); - layer.set("lyrNm", "침입수분석-결과관로"); + layer.set("lyrNm", "침입수분석 결과관로"); layer.set("lyrTyNm", "tb_ms_swage_ppl"); layer.set("clickPsblYn", "Y"); layer.setVisible?.(true); @@ -241,6 +268,7 @@ export const useInftwtResultMap = ({ const zoneExtent = zoneSourceRef.current?.getExtent?.(); const pipeExtent = pipeSourceRef.current?.getExtent?.(); const pipeFeatures = pipeSourceRef.current?.getFeatures?.() ?? []; + const oilqPipeExtent = getOilqPipeExtent(pipeFeatures); const hasPipeFeatures = pipeFeatures.length > 0; const hasZoneFeatures = Boolean(zoneSourceRef.current?.getFeatures?.().length); const preferZone = mapView?.focusTargetType === "ZONE" && hasZoneFeatures; @@ -257,13 +285,15 @@ export const useInftwtResultMap = ({ : false; const shouldUseZoneExtent = preferZone && (hasPipeInsideZone || !hasPipeFeatures); - const extent = shouldUseZoneExtent - ? zoneExtent - : hasPipeFeatures - ? pipeExtent - : hasZoneFeatures - ? zoneExtent - : null; + const extent = oilqPipeExtent + ? oilqPipeExtent + : shouldUseZoneExtent + ? zoneExtent + : hasPipeFeatures + ? pipeExtent + : hasZoneFeatures + ? zoneExtent + : null; if (!map || !extent || extent.some((value: number) => !Number.isFinite(value))) { return; @@ -272,7 +302,7 @@ export const useInftwtResultMap = ({ map.getView?.().fit?.(extent, { duration: 350, padding: DEFAULT_RESULT_FIT_PADDING, - maxZoom: shouldUseZoneExtent ? 15 : 16, + maxZoom: oilqPipeExtent ? 17 : shouldUseZoneExtent ? 15 : 16, constrainResolution: true, nearest: true, }); @@ -307,6 +337,11 @@ export const useInftwtResultMap = ({ const moveToStableFocus = useCallback( async (mapView: InftwtResultMapViewResponse) => { + if ((mapView.oilqPipeCount ?? 0) > 0) { + fitToRenderedFeatures(mapView); + return; + } + const activeRow = activeResultRowRef.current; const movedByRegion = moveByRegionCodes( activeRow?.ctpvCd ?? mapView.analysis?.ctpvCd, @@ -353,7 +388,7 @@ export const useInftwtResultMap = ({ pipeLayerRef.current?.changed?.(); gisMaps.current?.renderSync?.(); - console.info("침입수 FE overlay 렌더 결과", { + console.info("移⑥엯??FE overlay ?뚮뜑 寃곌낵", { focusTargetType: mapView.focusTargetType, zoneFeatureCount: zoneFeatures.length, pipeFeatureCount: pipeFeatures.length, @@ -372,6 +407,15 @@ export const useInftwtResultMap = ({ setIsOpen(false); setIsSearchModalOpen(false); + const cachedMapView = mapViewCacheRef.current.get(row.inftwtAnlsSn); + let mapView = cachedMapView; + + if (!mapView) { + const payload = await getRawJson(`/sewerage-service/api/v1/inftwtAnlsRslt/mapView/${row.inftwtAnlsSn}`); + mapView = (payload?.data ?? payload) as InftwtResultMapViewResponse; + mapViewCacheRef.current.set(row.inftwtAnlsSn, mapView); + } + if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent(INFTWT_PROC_SEARCH_EVENT, { @@ -379,26 +423,17 @@ export const useInftwtResultMap = ({ ctpvCd: row.ctpvCd ?? "0", sggCd: row.sggCd ?? "0", swtrmtFcltNm: row.swtrmtFcltNm ?? "", + suppressMapMove: (mapView.oilqPipeCount ?? 0) > 0, }, }), ); } - const cachedMapView = mapViewCacheRef.current.get(row.inftwtAnlsSn); - if (cachedMapView) { - setActiveMapView(cachedMapView); - void renderMapView(cachedMapView); - return; - } - - const payload = await getRawJson(`/sewerage-service/api/v1/inftwtAnlsRslt/mapView/${row.inftwtAnlsSn}`); - const mapView = (payload?.data ?? payload) as InftwtResultMapViewResponse; - mapViewCacheRef.current.set(row.inftwtAnlsSn, mapView); setActiveMapView(mapView); void renderMapView(mapView); } catch (error) { - console.error("침입수 분석결과 지도 연계 중 오류:", error); - alert("침입수 분석결과를 지도에 연계하지 못했습니다."); + console.error("移⑥엯??遺꾩꽍寃곌낵 吏€???곌퀎 以??ㅻ쪟:", error); + alert("移⑥엯??遺꾩꽍寃곌낵瑜?吏€?꾩뿉 ?곌퀎?섏? 紐삵뻽?듬땲??"); } }, [renderMapView, setFeatureData, setIsOpen, setIsSearchModalOpen], @@ -419,7 +454,7 @@ export const useInftwtResultMap = ({ if (!payload?.row) return; void handleRowSelect(payload.row); } catch (error) { - console.warn("침입수 분석결과 storage 동기화 값을 해석하지 못했습니다.", error); + console.warn("移⑥엯??遺꾩꽍寃곌낵 storage ?숆린??媛믪쓣 ?댁꽍?섏? 紐삵뻽?듬땲??", error); } }; @@ -459,7 +494,7 @@ export const useInftwtResultMap = ({ }); if (!popupWindow) { - alert("팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요."); + alert("?앹뾽??李⑤떒?섏뿀?듬땲?? 釉뚮씪?곗? ?앹뾽 ?덉슜 ?ㅼ젙???뺤씤?댁<?몄슂."); } return true; diff --git a/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md b/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md index e9916a137..2f18e01e1 100644 --- a/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md +++ b/apps/sn/docs/작업내용/SN-침입수분석결과-지도연계유지보수/001-fe-SN-침입수분석결과-지도연계유지보수-통합구현기록.md @@ -112,3 +112,15 @@ - 사용자 요청에 따라 침입수 분석 화면 액션 버튼 순서를 `양식 받기 -> 침입수분석 실행 -> 결과보기`로 조정했다. - 변경은 공통 `ii/BaseLayout.tsx` 렌더 순서만 수정해 RDII 분석 화면에도 같은 순서가 적용되도록 맞췄다. + +## 17. 2026-07-09 실측하수량 관로 우선 줌인 보정 + +- 침입수 분석결과 popup에서 row click 시 `oilqPipeCount > 0`이면 시도/시군구 중심 이동보다 `hasOilq` 관로 extent fit을 우선 적용하도록 `useInftwtResultMap`을 보정했다. +- 실측하수량 관로가 없는 row만 기존 지역 중심 이동과 일반 결과 관로 fallback을 유지한다. +- 이 보정으로 주황색으로 표시되는 실측하수량 관로가 있는 케이스는 지도 진입 직후 해당 관로 주변으로 바로 확대된다. + +## 18. 2026-07-09 지역 선이동 제거 보정 + +- `useInftwtResultMap`은 row click 직후 mapView를 먼저 확보한 뒤, `oilqPipeCount > 0`이면 검색바 동기화 이벤트에 `suppressMapMove`를 내려 지역 중심 이동을 건너뛰도록 보정했다. +- `procSearchBar`는 `suppressMapMove`가 내려오면 시도/시군구/시설 상태만 갱신하고 `moveMapToCenter`는 호출하지 않는다. +- 이 보정으로 실측하수량 관로가 있는 케이스는 처음부터 해당 관로 extent로 바로 확대된다. diff --git a/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx index 84016e5a8..b3f715086 100644 --- a/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx +++ b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx @@ -1,4 +1,4 @@ -"use client"; +"use client"; import { SelectBox, type SelectDataType } from "../../../ui"; import { Button } from "@common/components/ui"; import { useCallback, useEffect, useState } from "react"; @@ -29,7 +29,7 @@ interface SearchBarProps { } interface CenterSelectDataType extends SelectDataType { - center?: [number, number]; // 좌표 중심 위치 + center?: [number, number]; // 醫뚰몴 以묒떖 ?꾩튂 } export const ProcSearchBar = ({ @@ -74,7 +74,7 @@ export const ProcSearchBar = ({ center: sidoCenter[String(f.properties.mctpv_cd ?? "").trim()] //getGeometryCenter(f.geometry) ?? undefined, })) .sort((a: any, b: any) => { - // 커스텀 순서 정의 + // 而ㅼ뒪?€ ?쒖꽌 ?뺤쓽 const customOrder = [ "서울특별시", "부산광역시", @@ -98,19 +98,19 @@ export const ProcSearchBar = ({ const indexA = customOrder.indexOf(a.text); const indexB = customOrder.indexOf(b.text); - // 둘 다 커스텀 순서에 있으면 해당 순서로 + // ????而ㅼ뒪?€ ?쒖꽌???덉쑝硫??대떦 ?쒖꽌濡? if (indexA !== -1 && indexB !== -1) { return indexA - indexB; } - // 하나만 커스텀 순서에 있으면 그것을 앞으로 + // ?섎굹留?而ㅼ뒪?€ ?쒖꽌???덉쑝硫?洹멸쾬???욎쑝濡? if (indexA !== -1) return -1; if (indexB !== -1) return 1; - // 둘 다 커스텀 순서에 없으면 기본 알파벳 순서 + // ????而ㅼ뒪?€ ?쒖꽌???놁쑝硫?湲곕낯 ?뚰뙆踰??쒖꽌 return a.text.localeCompare(b.text, 'ko'); }); - setSidoData([{ text: "시도", val: "0" }, ...list]); + setSidoData([{ text: "?쒕룄", val: "0" }, ...list]); }; const fetchSigungu = async () => { @@ -174,7 +174,7 @@ export const ProcSearchBar = ({ const json = await res.json(); list = (json.features || []) .map((f: any) => ({ - text: f.properties.swtrmt_fclt_nm ?? "(이름없음)", + text: f.properties.swtrmt_fclt_nm ?? "(?대쫫?놁쓬)", val: f.id, })) .sort((a: any, b: any) => a.text.localeCompare(b.text, "ko")); @@ -193,28 +193,28 @@ export const ProcSearchBar = ({ }); const json = await res.json(); list = (json.data || []).map((item: any) => ({ - text: item.ruseUtztnFcltNm ?? "(이름없음)", + text: item.ruseUtztnFcltNm ?? "(?대쫫?놁쓬)", val: item.pbnstRuseFcltId, })); } else if (type === "tb_wr_ruse_fclt_dmd_co") { const res = await fetch(url); const json = await res.json(); list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", + text: item.dmdCoNm ?? "(?대쫫?놁쓬)", val: item.dmdInfoId, })); } else if (type === "tb_wr_tmpr_dmd_co") { const res = await fetch(url); const json = await res.json(); list = (json.data || []).map((item: any) => ({ - text: item.dmdCoNm ?? "(이름없음)", + text: item.dmdCoNm ?? "(?대쫫?놁쓬)", val: item.fctryMngSn, })); } else { const res = await fetch(url); const json = await res.json(); list = (json.data || []).map((item: any) => ({ - text: item.bdstNm ?? "(이름없음)", + text: item.bdstNm ?? "(?대쫫?놁쓬)", val: item.bdstInfoSn, })); } @@ -264,7 +264,7 @@ export const ProcSearchBar = ({ const prefix = nextSidoCd.substring(0, 2); const filtered = sigunData.filter((item) => item.val.startsWith(prefix)); - setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]); + setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]); setSigunValue("0"); }, [sigunData], @@ -315,7 +315,7 @@ export const ProcSearchBar = ({ const handleSearch = useCallback(() => { if (sidoValue === "0") { - alert("시도를 선택해 주세요"); + alert("시도를 선택해 주세요."); return; } @@ -344,10 +344,12 @@ export const ProcSearchBar = ({ ctpvCd?: string; sggCd?: string; swtrmtFcltNm?: string; + suppressMapMove?: boolean; }>; const nextCtpvCd = String(event.detail?.ctpvCd ?? "0").trim() || "0"; const nextSggCd = String(event.detail?.sggCd ?? "0").trim() || "0"; const nextSwtrmtFcltNm = String(event.detail?.swtrmtFcltNm ?? "").trim(); + const suppressMapMove = Boolean(event.detail?.suppressMapMove); const nextSidoCenter = sidoCenter[nextCtpvCd.slice(0, 2)]; const nextSigunguCenter = getSigunguCenter(nextSggCd); @@ -364,6 +366,10 @@ export const ProcSearchBar = ({ setFacilityValue("0"); setPendingExternalFacilityName(nextSwtrmtFcltNm); + if (suppressMapMove) { + return; + } + if (nextSggCd !== "0" && nextSigunguCenter) { moveMapToCenter(gisMaps, nextSigunguCenter, 12); return; @@ -430,7 +436,7 @@ export const ProcSearchBar = ({ />