From f685884ed75eaa469de195913846fe260438a10a Mon Sep 17 00:00:00 2001 From: swcho Date: Sat, 9 Aug 2025 22:38:02 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20WR(=EB=AC=BC=EC=9E=AC=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9)=20=EA=B0=80=EB=AD=84=EC=A7=80=EB=8F=84=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=EA=B5=AC=ED=98=84=20=EC=9D=84=20=EC=9C=84=ED=95=9C?= =?UTF-8?q?=20Gis=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/map/Gis.js | 13 + .../src/ui/map/lib/gis.core.js | 2 + .../src/ui/map/lib/gis.layer.js | 6 +- .../src/ui/map/searchBar/comboSearchBar.tsx | 302 ++++++++++++++++++ .../src/ui/map/searchBar/procSearchBar.tsx | 302 ++++++++++++++++++ 5 files changed, 624 insertions(+), 1 deletion(-) create mode 100644 packages/common/business_components/src/ui/map/searchBar/comboSearchBar.tsx create mode 100644 packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 579380ad5..bce7b2ef0 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -123,6 +123,8 @@ function Gis({ orderedLayers = [], checkInterval = 3000, height: height = "h-[calc(100vh-5rem)]", + onOpenPopup = (data) => { }, + droughtDates = { startYmd: '', endYmd: '' } }) { const mapRef = useRef(); const [mapObject, setMapObject] = useState({}); @@ -245,6 +247,17 @@ function Gis({ //setEditLayerItems(prev => [...layerItems]) }, [treeData]); + // droughtDates를 전역(GisApp)에 반영 + useEffect(() => { + if (typeof window === "undefined") return; + if (droughtDates?.startYmd && droughtDates?.endYmd) { + GisApp.DROUGHT_DATES = { + startYmd: String(droughtDates.startYmd), + endYmd: String(droughtDates.endYmd), + }; + } + }, [droughtDates?.startYmd, droughtDates?.endYmd]); + const getLayerItems = (layerCode) => { const layerItems = Object.entries(layerCode) .filter(([key, obj], idx) => obj?.updtPsblYn === "Y") diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js index 1a6b74c96..a5268ac44 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -30,6 +30,8 @@ const GisApp = {}; */ //레이어 정보 GisApp.layerInfos = []; +// 가뭄 레이어 날짜 전역 보관소 추가 +GisApp.DROUGHT_DATES = { startYmd: '', endYmd: '' }; //기본정보 GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows'; GisApp.SN_GEOSERVER_URL = '/sewerage-service/api/v1/map/geometry'; diff --git a/packages/common/business_components/src/ui/map/lib/gis.layer.js b/packages/common/business_components/src/ui/map/lib/gis.layer.js index 2d436ed12..6cbc054f8 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.layer.js +++ b/packages/common/business_components/src/ui/map/lib/gis.layer.js @@ -112,7 +112,7 @@ GisOlLayer.prototype = { let toProj = GisApp.TO_PROJ; let lyrTyNm = options.lyrTyNm; let typename = this.storageNm + lyrTyNm; - let filter = options?.flterSql && options.flterSql && options.flterSql.trim() !== '' + let filter = options?.flterSql && options.flterSql && options.flterSql.trim() !== ''; let params = {}; params['TYPENAME'] = typename; @@ -122,6 +122,10 @@ GisOlLayer.prototype = { params['SRS'] = toProj; params['OUTPUTFORMAT'] = 'application/json'; if (filter) params['CQL_FILTER'] = filter; + else if (GisApp.DROUGHT_DATES) { + params['START_YMD'] = GisApp.DROUGHT_DATES.startYmd; + params['END_YMD'] = GisApp.DROUGHT_DATES.endYmd; + } let strParams = new URLSearchParams(params).toString(); let source = new VectorSource({ diff --git a/packages/common/business_components/src/ui/map/searchBar/comboSearchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/comboSearchBar.tsx new file mode 100644 index 000000000..3289ae53f --- /dev/null +++ b/packages/common/business_components/src/ui/map/searchBar/comboSearchBar.tsx @@ -0,0 +1,302 @@ +"use client"; +import { SelectBox, type SelectDataType } from "../../../ui"; +import { Button } from "@common/components/ui"; +import { useCallback, useEffect, useState } from "react"; + +const geoserverUrl = + "/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&outputFormat=application/json"; + +const FACILITY_TYPE_URL_MAP: Record = { + tb_il_swage_prcs_fclt: geoserverUrl, //하수처리시설 + tb_wr_bdst_ruse_fclt: + "/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000", //재이용시설 post + tb_wr_rnwtr_utztn_fclt: "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}", //빗물이용시설 get + tb_wr_rswtr_fclt: "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}", //중수도시설 get + tb_wr_htdfl_fclt: "/reuse-service/drain/facilityList/1/1000/{sggCd}", //온배수시설 get + tb_wr_ruse_fclt_dmd_co: "/reuse-service/reuseFc/request/1/1000/{sggCd}", //수요처 get + tb_wr_tmpr_dmd_co: + "/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}", //잠재수요처 get +}; + +interface SearchBarProps { + onFeatureSelect: (feature: any) => void; + gisMaps: React.RefObject; + facilityTypes: { text: string; val: string }[]; +} + +export const ComboSearchBar = ({ + onFeatureSelect, + gisMaps, + facilityTypes, +}: SearchBarProps) => { + const [sidoValue, setSidoValue] = useState("0"); + const [sigunValue, setSigunValue] = useState("0"); + const [sidoData, setSidoData] = useState([ + { text: "선택", val: "0" }, + ]); + const [sigunData, setSigunData] = useState([ + { text: "선택", val: "0" }, + ]); + const [filteredSigunData, setFilteredSigunData] = useState([ + { text: "선택", val: "0" }, + ]); + + const [facilityData, setFacilityData] = useState([ + { text: "선택", val: "0" }, + ]); + const [facilityValue, setFacilityValue] = useState("0"); + + const [facilityType, setFacilityType] = useState( + facilityTypes?.length === 1 && facilityTypes[0] + ? facilityTypes[0].val + : "0", + ); + + //시도, 시군구 조회 + const fetchSido = async () => { + const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_ctpv&outputFormat=application/json&propertyName=mctpv_korn_nm,mctpv_cd`; + const res = await fetch(url).then(res => res.json()); + + const list = res.features.map((f: any) => ({ + text: f.properties.mctpv_korn_nm, + val: f.properties.mctpv_cd, + })); + setSidoData([{ text: "선택", val: "0" }, ...list]); + }; + + const fetchSigungu = async () => { + const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_sgg&outputFormat=application/json&propertyName=sgg_cd,sgg_korn_nm`; + const res = await fetch(url).then(res => res.json()); + + const list = res.features.map((f: any) => ({ + text: f.properties.sgg_korn_nm, + val: f.properties.sgg_cd, + })); + setSigunData(list); + }; + + //시설명 콤보박스 조회 + const fetchFacilitiesBySggNm = async ( + sggCd: string, + sggNm: string, + sidoNm: string, + currentFacilityType?: string, + ) => { + const type = currentFacilityType ?? facilityType; + if (!type || type === "0") return; + + const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type]; + if (!baseUrlTemplate) return; + + const year = new Date().getFullYear().toString(); + let url = baseUrlTemplate + .replace("{year}", year) + .replace("{sggCd}", sggCd) + .replace("{sidoNm}", encodeURIComponent(sidoNm)) + .replace("{sggNm}", encodeURIComponent(sggNm)); + + let list: SelectDataType[] = []; + + if ( + type === "tb_il_swage_prcs_fclt" //하수처리시설 + ) { + const cqlFilter = `ctpv_nm='${encodeURIComponent(sidoNm)}' AND sgg_nm='${encodeURIComponent(sggNm)}'`; + url = `${url}&typeName=hasudo:${type}&cql_filter=${cqlFilter}`; + const res = await fetch(url); + const json = await res.json(); + list = json.features.map((f: any) => ({ + text: f.properties.swtrmt_fclt_nm ?? "(이름없음)", + val: f.id, + })); + } else if ( + type === "tb_wr_bdst_ruse_fclt" // 재이용시설 + ) { + //재용에서 참조하는 하수처리시설 목록 + const paramsRes = await fetch("/reuse-service/reuseFc/pubFc"); + const paramsJson = await paramsRes.json(); + const params = paramsJson.data; + + const res = await fetch(url, { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + 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 ?? "(이름없음)", + val: item.dmdInfoId, + })); + } else if ( + type === "tb_wr_tmpr_dmd_co" //잠재수요처 + ) { + // GET 방식 + const res = await fetch(url); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + text: item.dmdCoNm ?? "(이름없음)", + val: item.fctryMngSn, + })); + } else { + // GET 방식 + const res = await fetch(url); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + text: item.bdstNm ?? "(이름없음)", + val: item.bdstInfoSn, + })); + } + + setFacilityData([{ text: "선택", val: "0" }, ...list]); + setFacilityValue("0"); + }; + + //시도, 시군구 초기 데이터 조회 + useEffect(() => { + fetchSido(); + fetchSigungu(); + }, []); + + //시군구 데이터 초기화 + const initializeSigunData = useCallback( + (sidoCd: string) => { + if (sidoCd === "0") { + setFilteredSigunData([{ text: "선택", val: "0" }]); + setSigunValue("0"); + return; + } + const prefix = sidoCd.substring(0, 2); + const filtered = sigunData.filter((item) => item.val.startsWith(prefix)); + setFilteredSigunData([{ text: "선택", val: "0" }, ...filtered]); + setSigunValue("0"); + }, + [sigunData], + ); + + //콤보박스 선택시 연관 콤보박스 처리 + const onSelectValue = useCallback( + (val: string, type: string) => { + if (type === "ctpvCd") { + setSidoValue(val); + initializeSigunData(val); + setFacilityData([{ text: "선택", val: "0" }]); + setFacilityValue("0"); + } else if (type === "sggCd") { + setSigunValue(val); + const sidoNm = + sidoData.find((item) => item.val === sidoValue)?.text || ""; + const sggNm = sigunData.find((item) => item.val === val)?.text || ""; + const sggCd = sigunData.find((item) => item.val === val)?.val || ""; + if (sggNm && sidoNm) + fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, facilityType); + } else if (type === "facilityType") { + setFacilityType(val); + setFacilityValue("0"); + setFacilityData([{ text: "선택", val: "0" }]); + const sidoNm = + sidoData.find((item) => item.val === sidoValue)?.text || ""; + const sggNm = + sigunData.find((item) => item.val === sigunValue)?.text || ""; + const sggCd = + sigunData.find((item) => item.val === sigunValue)?.val || ""; + if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, val); + } else if (type === "fcltCd") { + setFacilityValue(val); + } + }, + [initializeSigunData, sigunData, sigunValue, sidoData, sidoValue], + ); + + //조회 버튼 클릭 + const handleSearch = () => { + if (!gisMaps.current || facilityValue === "0") return; + const layers = gisMaps.current.getLayers().getArray(); + for (const layer of layers) { + const source = layer.getSource?.(); + const feature = source?.getFeatureById?.(facilityValue); + if (feature) { + onFeatureSelect(feature); + return; + } + } + alert("해당 처리시설을 지도에서 찾을 수 없습니다."); + }; + + return ( +
+
+
+
+ {facilityTypes.length > 1 && ( + + onSelectValue(val.toString(), "facilityType") + } + value={facilityType} + /> + )} + + onSelectValue(val.toString(), "ctpvCd") + } + value={sidoValue} + /> + + onSelectValue(val.toString(), "sggCd") + } + value={sigunValue} + /> + + onSelectValue(val.toString(), "fcltCd") + } + value={facilityValue} + /> +
+
+ +
+
+
+
+ ); +}; diff --git a/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx new file mode 100644 index 000000000..dbded59af --- /dev/null +++ b/packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx @@ -0,0 +1,302 @@ +"use client"; +import { SelectBox, type SelectDataType } from "../../../ui"; +import { useCallback, useEffect, useState } from "react"; +import GisApp from "../lib/gis.core"; + +const geoserverUrl = + "/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&outputFormat=application/json"; + +const FACILITY_TYPE_URL_MAP: Record = { + tb_il_swage_prcs_fclt: geoserverUrl, //하수처리시설 +}; + +interface SearchBarProps { + gisMaps: React.RefObject; + facilityTypes: { text: string; val: string }[]; +} + +export const ProcSearchBar = ({ gisMaps, facilityTypes }: SearchBarProps) => { + const [sidoValue, setSidoValue] = useState("0"); + const [sigunValue, setSigunValue] = useState("0"); + const [sidoData, setSidoData] = useState([ + { text: "시도", val: "0" }, + ]); + const [sigunData, setSigunData] = useState([ + { text: "시군구", val: "0" }, + ]); + const [filteredSigunData, setFilteredSigunData] = useState([ + { text: "시군구", val: "0" }, + ]); + + const [facilityData, setFacilityData] = useState([ + { text: "하수처리시설", val: "0" }, + ]); + const [facilityValue, setFacilityValue] = useState("0"); + + const [facilityType, setFacilityType] = useState( + facilityTypes?.length === 1 && facilityTypes[0] + ? facilityTypes[0].val + : "0", + ); + + const fetchSido = async () => { + const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_ctpv&outputFormat=application/json&propertyName=mctpv_korn_nm,mctpv_cd`; + const res = await fetch(url).then(res => res.json()); + + const list = res.features.map((f: any) => ({ + text: f.properties.mctpv_korn_nm, + val: f.properties.mctpv_cd, + })); + setSidoData([{ text: "선택", val: "0" }, ...list]); + }; + + const fetchSigungu = async () => { + const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_sgg&outputFormat=application/json&propertyName=sgg_cd,sgg_korn_nm`; + const res = await fetch(url).then(res => res.json()); + + const list = res.features.map((f: any) => ({ + text: f.properties.sgg_korn_nm, + val: f.properties.sgg_cd, + })); + setSigunData(list); + }; + + //시설명 콤보박스 조회 + const fetchFacilitiesBySggNm = async ( + sggCd: string, + sggNm: string, + sidoNm: string, + currentFacilityType?: string, + ) => { + const type = currentFacilityType ?? facilityType; + if (!type || type === "0") return; + + const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type]; + if (!baseUrlTemplate) return; + + const year = new Date().getFullYear().toString(); + let url = baseUrlTemplate + .replace("{year}", year) + .replace("{sggCd}", sggCd) + .replace("{sidoNm}", encodeURIComponent(sidoNm)) + .replace("{sggNm}", encodeURIComponent(sggNm)); + + let list: SelectDataType[] = []; + + if ( + type === "tb_il_swage_prcs_fclt" //하수처리시설 + ) { + const cqlFilter = `ctpv_nm='${encodeURIComponent(sidoNm)}' AND sgg_nm='${encodeURIComponent(sggNm)}'`; + url = `${url}&typeName=hasudo:${type}&cql_filter=${cqlFilter}`; + const res = await fetch(url); + const json = await res.json(); + list = json.features.map((f: any) => ({ + text: f.properties.swtrmt_fclt_nm ?? "(이름없음)", + val: f.id, + })); + } else if ( + type === "tb_wr_bdst_ruse_fclt" // 재이용시설 + ) { + //재용에서 참조하는 하수처리시설 목록 + const paramsRes = await fetch("/reuse-service/reuseFc/pubFc"); + const paramsJson = await paramsRes.json(); + const params = paramsJson.data; + + const res = await fetch(url, { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + 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 ?? "(이름없음)", + val: item.dmdInfoId, + })); + } else if ( + type === "tb_wr_tmpr_dmd_co" //잠재수요처 + ) { + // GET 방식 + const res = await fetch(url); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + text: item.dmdCoNm ?? "(이름없음)", + val: item.fctryMngSn, + })); + } else { + // GET 방식 + const res = await fetch(url); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + text: item.bdstNm ?? "(이름없음)", + val: item.bdstInfoSn, + })); + } + + setFacilityData([{ text: "시도", val: "0" }, ...list]); + setFacilityValue("0"); + }; + + //시도, 시군구 초기 데이터 조회 + useEffect(() => { + fetchSido(); + fetchSigungu(); + }, []); + + //시군구 데이터 초기화 + const initializeSigunData = useCallback( + (sidoCd: string) => { + if (sidoCd === "0") { + setFilteredSigunData([{ text: "시군구", val: "0" }]); + setSigunValue("0"); + return; + } + const prefix = sidoCd.substring(0, 2); + const filtered = sigunData.filter((item) => item.val.startsWith(prefix)); + setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]); + setSigunValue("0"); + }, + [sigunData], + ); + + //콤보박스 선택시 연관 콤보박스 처리 + const onSelectValue = useCallback( + (val: string, type: string) => { + if (type === "ctpvCd") { + setSidoValue(val); + initializeSigunData(val); + setFacilityData([{ text: "하수처리시설", val: "0" }]); + setFacilityValue("0"); + } else if (type === "sggCd") { + setSigunValue(val); + const sidoNm = + sidoData.find((item) => item.val === sidoValue)?.text || ""; + const sggNm = sigunData.find((item) => item.val === val)?.text || ""; + const sggCd = sigunData.find((item) => item.val === val)?.val || ""; + if (sggNm && sidoNm) + fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, facilityType); + } else if (type === "facilityType") { + setFacilityType(val); + setFacilityValue("0"); + setFacilityData([{ text: "하수처리시설", val: "0" }]); + const sidoNm = + sidoData.find((item) => item.val === sidoValue)?.text || ""; + const sggNm = + sigunData.find((item) => item.val === sigunValue)?.text || ""; + const sggCd = + sigunData.find((item) => item.val === sigunValue)?.val || ""; + if (sggNm && sidoNm) fetchFacilitiesBySggNm(sggCd, sggNm, sidoNm, val); + } else if (type === "fcltCd") { + setFacilityValue(val); + + // 하수처리시설일 때 바로 handleFeatureFocus 호출 + if (val !== "0") { + handleFeatureFocus({ + fid: val, + layerNm: "tb_il_swage_prcs_fclt", // 레이어 이름 + }); + } + } + }, + [initializeSigunData, sigunData, sigunValue, sidoData, sidoValue], + ); + + const handleFeatureFocus = (data: any) => { + const map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select as any; + if (!select) return; + // 1. 기존 선택 초기화 + //select.resetSelectedFeature(); + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature: any = null; + let featureId = data.fid; + map + .getLayers() + .getArray() + .forEach((layer: any) => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + // 3. 피처 선택 + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } else { + console.warn("geometry를 찾을 수 없습니다:", data); + } + } else { + console.warn("Feature를 찾을 수 없습니다:", data); + } + }; + + return ( +
+
+
+
+ + onSelectValue(val.toString(), "ctpvCd") + } + value={sidoValue} + /> + + onSelectValue(val.toString(), "sggCd") + } + value={sigunValue} + /> + + onSelectValue(val.toString(), "fcltCd") + } + value={facilityValue} + /> +
+
+
+
+ ); +}; From f644bd6b7e657ceda1afdf693e7ddf1d2a861b15 Mon Sep 17 00:00:00 2001 From: swcho Date: Sat, 9 Aug 2025 23:43:37 +0900 Subject: [PATCH 2/8] =?UTF-8?q?refactor,=20remove,=20add:=20GIS=20main=20?= =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EC=B9=98=EC=99=80=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/map/Gis.js | 118 ++- .../src/ui/map/GisContent.js | 121 ++- .../src/ui/map/GisSelect.js | 70 +- .../business_components/src/ui/map/GisTab.js | 84 -- .../business_components/src/ui/map/GisTree.js | 871 +++++++++--------- .../src/ui/map/lib/gis.core.js | 21 +- .../src/ui/map/lib/gis.style.js | 22 +- .../src/ui/map/searchBar/SearchInpuText.tsx | 122 +++ .../src/ui/map/searchBar/index.tsx | 3 + .../src/ui/map/searchBar/searchBar.tsx | 344 +++++++ 10 files changed, 1115 insertions(+), 661 deletions(-) delete mode 100644 packages/common/business_components/src/ui/map/GisTab.js create mode 100644 packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx create mode 100644 packages/common/business_components/src/ui/map/searchBar/index.tsx create mode 100644 packages/common/business_components/src/ui/map/searchBar/searchBar.tsx diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index bce7b2ef0..3d810b48d 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -156,18 +156,30 @@ function Gis({ setFeatureData(null); }; - // Feature Click Event + // 피처 클릭시 const handleFeatureSelect = useCallback( (data) => { + console.log("handleFeatureSelect"); onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출 }, [mapInstanceRef], ); + //피처로 포커스 이동 + const handleFeatureFocus = useCallback( + (layerName, data) => { + console.log("handleFeatureFocus"); + console.log(layerName); + console.log(data); + // onFeatureFocus?.(data); + }, + [mapInstanceRef], + ); + // 지도 생성 및 초기화 useEffect(() => { if (!mapRef.current) return; - if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 + //if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 // EPSG:5179 등록 proj4.defs([ @@ -178,33 +190,53 @@ function Gis({ ]); register(proj4); - // OpenLayers 지도 객체 생성 - const map = new OlMap({ - layers: [gisCore.baseLayers["base"], gisCore.baseLayers["satellite"]], - renderer: "webgl", - target: mapRef.current, - interactions: defaults({ doubleClickZoom: false }), - controls: defaultControls({ - zoom: false, - rotate: false, - attributionOptions: { - collapsible: false, - }, - }).extend([]), - view: new View({ - projection: GisApp.TO_PROJ, - zoom: gisCore.defaults.zoom, - center: olProj.transform( - [127.4875, 37.4916], - GisApp.FROM_PROJ, - GisApp.TO_PROJ, - ), - minZoom: gisCore.defaults.minZoom, - maxZoom: gisCore.defaults.maxZoom, - }), - }); + let map = mapInstanceRef.current; - gisMaps.current = map; + // OpenLayers 지도 객체 생성 + if (!map) { + console.log("지도 객체 생성"); + map = new OlMap({ + layers: [gisCore.baseLayers["base"], gisCore.baseLayers["satellite"]], + renderer: "webgl", + target: mapRef.current, + interactions: defaults({ doubleClickZoom: false }), + controls: defaultControls({ + zoom: false, + rotate: false, + attributionOptions: { + collapsible: false, + }, + }).extend([]), + view: new View({ + projection: GisApp.TO_PROJ, + zoom: gisCore.defaults.zoom, + center: olProj.transform( + [127.4875, 37.4916], + GisApp.FROM_PROJ, + GisApp.TO_PROJ, + ), + minZoom: gisCore.defaults.minZoom, + maxZoom: gisCore.defaults.maxZoom, + }), + }); + } else { + // 📌 재마운트 시 target 없으면 다시 지정 + if (!map.getTarget()) { + console.log("재마운트"); + map.setTarget(mapRef.current); + } + } + + const select = GisApp.Module.select; + if (select) { + console.log("select 바인딩"); + if (select.selectInteraction) { + map.removeInteraction(select.selectInteraction); + } + select.map = map; // map 바인딩 + select.setInteraction(); + select.pointerMoveEventHandler(); + } // 미니맵 추가 if (isMiniMap) { @@ -221,12 +253,21 @@ function Gis({ map.addControl(overviewMap); } + gisMaps.current = map; + // 스케일 컨트롤 및 베이스 레이어 설정 gisCore.__proto__.map = map; gisCore.scaleControl(); gisCore.changeBaseLayer("base"); setMapObject({ map: map }); mapInstanceRef.current = map; // 생성 완료 플래그 설정 + + // 📌 언마운트 시 완전 제거 + return () => { + console.log("지도 제거"); + map.setTarget(null); // DOM 연결 해제 + mapInstanceRef.current = null; // 참조 초기화 + }; }, []); // treeData 변경 시 레이어 코드 생성 @@ -240,11 +281,6 @@ function Gis({ // 트리 데이터를 레이어 코드 객체로 변환 const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, "lyrSn"); GisApp.layerCode = layerCode; // 전역 레이어 코드 설정 - - //setTreeData(data); - // 편집 가능한 레이어 목록 생성 - //const layerItems = getLayerItems(layerCode); - //setEditLayerItems(prev => [...layerItems]) }, [treeData]); // droughtDates를 전역(GisApp)에 반영 @@ -287,9 +323,9 @@ function Gis({ const handleCheckedLayersChange = useCallback( (checkedIds, checkedNodesData) => { setTimeout(() => { - setCheckedLayers(checkedIds); - setCheckedLayersData(checkedNodesData); - onCheckedLayersChange?.(checkedIds, checkedNodesData); + setCheckedLayers(checkedIds); + setCheckedLayersData(checkedNodesData); + onCheckedLayersChange?.(checkedIds, checkedNodesData); }, 1000); }, [onCheckedLayersChange], @@ -328,7 +364,9 @@ function Gis({ const dummyFeature = { getProperties: () => ({}), // 최소 구조 }; - onFeatureSelect(dummyFeature); + console.log(layerNm) + onOpenPopup(layerNm) + // onFeatureSelect(dummyFeature); }} imageVisible={imgVisble} isPlaying={isPlaying} @@ -370,10 +408,8 @@ function Gis({ )} {/* 시도/시군구 조회 영역 */} {SearchBar && ( -
- +
+ {SearchBar}
)} diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js index d2594ba95..45b44c882 100644 --- a/packages/common/business_components/src/ui/map/GisContent.js +++ b/packages/common/business_components/src/ui/map/GisContent.js @@ -1,74 +1,61 @@ -import { Search } from 'lucide-react'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useGisContext } from './Gis'; -import './lib/css/gis.edit.css'; -import GisApp from './lib/gis.core'; +import { Search } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useGisContext } from "./Gis"; +import "./lib/css/gis.edit.css"; +import GisApp from "./lib/gis.core"; -export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) { - const { gisMap, gisCore } = useGisContext(); - const [visibleComponents, setVisibleComponents] = useState({}); - const addressResultLayer = useRef(null); - const [playShow, setPlayShow] = useState({ show: false }); +export default function GisContent({ + edit = false, + onFeatureSelect, + isSearch, + setIsSearchModalOpen, +}) { + const { gisMap, gisCore } = useGisContext(); - //셀렉트 버튼 이벤트 - // const clickFeature = useCallback(feature => { - // if (feature) { - // const layer = feature.getLayer(); - // setVisibleComponents(prevState => ({ - // ...prevState, - // })); - // } - // }, []); + //셀렉트 버튼 이벤트 + const selectRslt = useCallback((feature) => { + GisApp.Module.layer.highLightFeature( + [feature], + { + id: GisApp.hlLayerId, + zIndex: 10, + opacity: 1, + selectable: true, + }, + { width: 30 }, + ); + clickFeature(feature); + }, []); - const selectRslt = useCallback(feature => { - GisApp.Module.layer.highLightFeature( - [feature], - { - id: GisApp.hlLayerId, - zIndex: 10, - opacity: 1, - selectable: true, - }, - { width: 30 } - ); - clickFeature(feature); - }, []); + useEffect(() => { + if (gisMap) { + setVisibleComponents((prevState) => ({ + ...prevState, + })); + } + }, [gisMap]); - useEffect(() => { - if (gisMap) { - setVisibleComponents(prevState => ({ - ...prevState, - })); - } - }, [gisMap]); + if (!gisMap) return null; - if (!gisMap) return null; + return ( + <> + + + + + - return ( - <> - {/**/} - {/* - - */} - {/* */} - - {/* 검색 모달 */} - {isSearch && ( -
- -
- )} - - ); + {/* 검색 모달 */} + {isSearch && ( +
+ +
+ )} + + ); } diff --git a/packages/common/business_components/src/ui/map/GisSelect.js b/packages/common/business_components/src/ui/map/GisSelect.js index e9deb72f7..d7d5b5b72 100644 --- a/packages/common/business_components/src/ui/map/GisSelect.js +++ b/packages/common/business_components/src/ui/map/GisSelect.js @@ -1,46 +1,42 @@ -import React, { useContext, useEffect } from 'react'; -import { GisContext } from './Gis'; -import GisApp from './lib/gis.core'; -import GisOlSelect from './lib/gis.select'; +import React, { useContext, useEffect } from "react"; +import { GisContext } from "./Gis"; +import GisApp from "./lib/gis.core"; +import GisOlSelect from "./lib/gis.select"; export default React.memo(function GisSelect({ onFeatureSelect }) { + const { gisMap } = useContext(GisContext); - const { gisMap } = useContext(GisContext); + useEffect(() => { + if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함 + // 이미 select가 있으면 재생성하지 않음 + const select = new GisOlSelect(gisMap); + GisApp.Module.select = select; - useEffect(() => { - if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함 + // 피처 선택 시 콜백 등록 + select.getSelectedInfo(function (features) { + if (features?.[0]) { + console.log("GisSelect features", features); + onFeatureSelect(features[0]); + } + }); + }, [gisMap, onFeatureSelect]); - // 이미 select가 있으면 재생성하지 않음 - if (!GisApp.Module.select) { - const select = new GisOlSelect(gisMap); - GisApp.Module.select = select; + // const select = new GisOlSelect(gisMap); + // GisApp.Module.select = select; - // 피처 선택 시 콜백 등록 - select.getSelectedInfo(function (features) { - if (features?.[0]) { - console.log('GisSelect features', features); - onFeatureSelect(features[0]); - } - }); - } - }, [gisMap, onFeatureSelect]); + // select.getSelectedInfo(function (features) { + // //clickFeature(features[0]); + // if(features.length > 0) { + // const layer = features[0]; + // // console.log('GisSelect layer', layer); + // onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달 + // } + // }); - // const select = new GisOlSelect(gisMap); - // GisApp.Module.select = select; - - // select.getSelectedInfo(function (features) { - // //clickFeature(features[0]); - // if(features.length > 0) { - // const layer = features[0]; - // // console.log('GisSelect layer', layer); - // onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달 - // } - // }); - - return ( - <> - {/*
+ return ( + <> + {/*

선,면,다각형을 이용한 시설물 선택 영역

  • @@ -55,6 +51,6 @@ export default React.memo(function GisSelect({ onFeatureSelect }) {
*/} - - ); + + ); }); diff --git a/packages/common/business_components/src/ui/map/GisTab.js b/packages/common/business_components/src/ui/map/GisTab.js deleted file mode 100644 index 57d8a48e8..000000000 --- a/packages/common/business_components/src/ui/map/GisTab.js +++ /dev/null @@ -1,84 +0,0 @@ -// import React, { useState } from 'react'; -// import { Tabs, TabsList, TabsTrigger } from '@common/components/ui/tabs'; -// import GisTree from './GisTree'; -// import GisSearch from './GisSearch'; -// import { ChevronLeft, ChevronRight, Search } from 'lucide-react'; -// import SearchModal from '@/map/SearchModal'; - - -// function GisTab({ visible, setVisible, isSearch, isSearchModalOpen, setIsSearchModalOpen, onFeatureSelect }) { -// const [activeTab, setActiveTab] = useState('layers'); - -// return ( -// <> -//
-// -// -// 레이어 -// {/* {isSearch && ( -// -// // -// // -// // -// )} */} -// - -// {/* GisTree는 항상 마운트되며, 탭 상태에 따라 보여주기만 함 */} -//
-// -//
- -// {/* GisSearch도 동일하게 처리 */} -// {/* {isSearch && ( -//
-// -//
-// )} */} - - -// {/* 🔍 검색 모달 */} -// {/* */} -//
- -// {/* 접기 버튼 */} -// -//
- -// {/* 열기 버튼 */} -// {!visible && ( -// -// )} -// -// ); -// } - -// export default React.memo(GisTab); diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index ed6bb00fc..7285caa07 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -1,446 +1,493 @@ import { MultiBackend, Tree, - getBackendOptions -} from "@minoru/react-dnd-treeview"; -import { ChevronLeft, ChevronRight } from 'lucide-react'; -import React, { useEffect, useState, useRef, useCallback } from 'react'; - -import { useGisContext } from './Gis'; -import GisApp from './lib/gis.core'; - -export default React.memo(function GisTree({ + getBackendOptions, + DndProvider, + } from "@minoru/react-dnd-treeview"; + import { ChevronLeft, ChevronRight } from "lucide-react"; + import React, { useEffect, useState, useRef, useCallback } from "react"; + + import { useGisContext } from "./Gis"; + import GisApp from "./lib/gis.core"; + + export default React.memo(function GisTree({ checkedNodes = [], - setCheckedNodes = () => { }, + setCheckedNodes = () => {}, visible, setVisible, onCheckedChange, onLayerIconClick, imageVisible = false, isPlaying = false, - setIsPlaying = () => { }, + setIsPlaying = () => {}, targetLayerTypes = [], orderedLayers = [], checkInterval = 3000, - onProgressChange = () => { }, -}) { + onProgressChange = () => {}, + }) { const { gisCore, treeData } = useGisContext(); const [checked, setChecked] = useState([]); const [expanded, setExpanded] = useState([]); const [nodes, setNodes] = useState([]); - const [activeTab, setActiveTab] = useState('layers'); - + const [activeTab, setActiveTab] = useState("layers"); + const currentAnimationIndexRef = useRef(0); - const targetNodesRef = useRef([]); // 대상 노드들을 저장할 ref - const isPausedRef = useRef(false); // 일시정지 상태를 저장할 ref - const timeoutIdRef = useRef(null); // setTimeout ID를 저장할 ref - - useEffect(() => { - if (treeData && treeData.length > 0) { - const nodeData = setNodeValues(treeData); - setNodes(nodeData); - gisCore.setAllLayersOnMap(false); - } - }, [treeData]); - - // 트리 노드 체크 시 레이어 표시/숨김 - useEffect(() => { - if (nodes.length > 0) { - // 기본으로 체크된 노드들 찾기 - const defaultChecked = getDefaultCheckedNodes(nodes); - setChecked(defaultChecked); - console.log('defaultChecked : ', defaultChecked); - - // 기본으로 확장된 노드들 설정 - const defaultExpanded = Array.from(new Set([ - ...defaultChecked, - ...defaultChecked.flatMap((id) => getAncestors(id, nodes)) - ])); - setExpanded(defaultExpanded); - - // 기본 체크된 레이어들을 지도에 표시 - gisCore.setLayerOnMap(defaultChecked, true); - - // 초기 체크된 레이어 정보를 상위로 전달 - const defaultCheckedNodesData = nodes.filter(node => defaultChecked.includes(node.id)); - onCheckedChange?.(defaultChecked, defaultCheckedNodesData); - } - }, [nodes]); // onCheckedChange를 의존성에 추가 - - // 초기 표시 여부가 'Y'인 레이어들 찾기 - const getDefaultCheckedNodes = (nodes, values = []) => { - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - if (node.data.initEprssYn === 'Y') { - values.push(node.data.lyrSn); - } - } - return values; - }; - + const targetNodesRef = useRef([]); + const isPausedRef = useRef(false); + const timeoutIdRef = useRef(null); + + // ===== 유틸 ===== const getDescendants = (id, all) => { - const children = all.filter((n) => n.parent === id); - return children.reduce( - (acc, child) => acc.concat(child.id, ...getDescendants(child.id, all)), - [] - ); + const children = all.filter((n) => n.parent === id); + return children.reduce( + (acc, child) => acc.concat(child.id, ...getDescendants(child.id, all)), + [] + ); }; - + const getAncestors = (id, all) => { - const node = all.find((n) => n.id === id); - if (!node || node.parent == null) return []; - return [node.parent, ...getAncestors(node.parent, all)]; + const node = all.find((n) => n.id === id); + if (!node || node.parent == null) return []; + return [node.parent, ...getAncestors(node.parent, all)]; }; - + + const getChildren = (parentId, all) => + all.filter((n) => n.parent === parentId); + + const areAllChildrenChecked = (parentId, all, checkedIds) => { + const children = getChildren(parentId, all); + return children.length > 0 && children.every((c) => checkedIds.includes(c.id)); + }; + + const updateParentCheckState = (nodeId, all, checkedIds) => { + const parent = all.find((n) => n.id === nodeId)?.parent; + if (parent == null) return checkedIds; + + if (areAllChildrenChecked(parent, all, checkedIds)) { + if (!checkedIds.includes(parent)) { + checkedIds.push(parent); + } + } else { + checkedIds = checkedIds.filter((id) => id !== parent); + } + return updateParentCheckState(parent, all, checkedIds); + }; + const isEveryChildChecked = (parentId, all, checked) => { - const children = all.filter((n) => n.parent === parentId); - return children.every((c) => checked.includes(c.id)); + const children = getChildren(parentId, all); + return children.length > 0 && children.every((c) => checked.includes(c.id)); }; - - const handleCheck = (id) => { - setChecked((prevChecked) => { - const isChecked = prevChecked.includes(id); - let newChecked = [...prevChecked]; - if (isChecked) { - const descendants = getDescendants(id, nodes); - newChecked = newChecked.filter((cid) => cid !== id && !descendants.includes(cid)); - const ancestors = getAncestors(id, nodes); - ancestors.forEach((p) => { - if (newChecked.includes(p)) { - newChecked = newChecked.filter((cid) => cid !== p); - } - }); - } else { - const descendants = getDescendants(id, nodes); - newChecked = Array.from(new Set([...newChecked, id, ...descendants])); - const ancestors = getAncestors(id, nodes); - ancestors.forEach((p) => { - if (isEveryChildChecked(p, nodes, newChecked)) { - newChecked.push(p); - } - }); - // setExpanded((prev) => Array.from(new Set([...prev, ...ancestors]))); - } - const allIds = nodes.map((n) => n.id); - const unchecked = allIds.filter((nid) => !newChecked.includes(nid)); - gisCore.setLayerOnMap(newChecked, true); - gisCore.setLayerOnMap(unchecked, false); - - // 체크된 노드 정보와 함께 노드 데이터를 상위로 전달 - const checkedNodesData = nodes.filter(node => newChecked.includes(node.id)); - onCheckedChange?.(newChecked, checkedNodesData); - - return newChecked; - }); + + // ===== 기본 체크 노드 ===== + const getDefaultCheckedNodes = (nodes) => { + let checkedIds = nodes + .filter((n) => n.data.initEprssYn === "Y") + .map((n) => n.id); + + nodes.forEach((node) => { + checkedIds = updateParentCheckState(node.id, nodes, checkedIds); + }); + + return Array.from(new Set(checkedIds)); }; - - const setNodeValues = (result) => { - return result.map((item) => { - const src = makeLegendSrc(item); - let label = item.lyrNm; - return { - id: item.lyrSn, - text: label, - parent: item.upperLyrSn, - droppable: !!item.children, - data: item, - icon: src ? {item.lyrNm} : '', - }; - }); - }; - - const makeLegendSrc = layerinfo => { - if (!layerinfo.gobjTy) return null; - - let src; - if (layerinfo.iconNm) { - src = GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm; - } else { - const ss = makeLegendImage(layerinfo); - src = ss.src; - } - return src; - }; - - const makeLegendImage = layerinfo => { - let image = document.createElement('img'); - let canvas = document.createElement('canvas'); - canvas.width = 16; - canvas.height = 16; - let ctx = canvas.getContext('2d'); - ctx.beginPath(); - - if (layerinfo.gobjTy === 'lineLabelPoligon') { - ctx.moveTo(1, 1); - ctx.lineTo(15, 1); - ctx.lineTo(15, 15); - ctx.lineTo(1, 15); - ctx.lineTo(1, 1); - } else if (layerinfo.gobjTy === 'line') { - ctx.moveTo(1, 15); - ctx.lineTo(5, 1); - ctx.lineTo(9, 15); - ctx.lineTo(15, 1); - } else { - ctx.moveTo(1, 1); - ctx.lineTo(15, 1); - ctx.lineTo(15, 15); - ctx.lineTo(1, 15); - ctx.lineTo(1, 1); - } - - if (layerinfo.gobjTy === 'lineLabelPoligon') { - ctx.fillStyle = layerinfo.polgnColor; - ctx.fill(); - ctx.lineWidth = 1; - ctx.strokeStyle = layerinfo.lnColor; - ctx.stroke(); - } - - if (layerinfo.gobjTy === 'line') { - ctx.lineWidth = 1; - ctx.strokeStyle = layerinfo.lnColor; - ctx.stroke(); - } - - image.src = canvas.toDataURL('image/png'); - return image; - }; - const handleLayerImgClick = (nodeNm, nodeId, layerNm) => { - if (!checked.includes(nodeId)) alert('시설을 선택해주세요') - else onLayerIconClick(nodeNm) - } - - // 특정 노드를 체크/언체크하는 함수 - const checkSpecificNode = useCallback((nodeId, shouldCheck = true) => { - setChecked((prevChecked) => { - let newChecked = [...prevChecked]; - const isCurrentlyChecked = prevChecked.includes(nodeId); - - if (shouldCheck && !isCurrentlyChecked) { - // 체크하기 - const descendants = getDescendants(nodeId, nodes); - newChecked = Array.from(new Set([...newChecked, nodeId, ...descendants])); - const ancestors = getAncestors(nodeId, nodes); - ancestors.forEach((p) => { - if (isEveryChildChecked(p, nodes, newChecked)) { - newChecked.push(p); - } - }); - } else if (!shouldCheck && isCurrentlyChecked) { - // 언체크하기 - const descendants = getDescendants(nodeId, nodes); - newChecked = newChecked.filter((cid) => cid !== nodeId && !descendants.includes(cid)); - const ancestors = getAncestors(nodeId, nodes); - ancestors.forEach((p) => { - if (newChecked.includes(p)) { - newChecked = newChecked.filter((cid) => cid !== p); - } - }); - } - - // 지도에 레이어 표시/숨김 적용 - const allIds = nodes.map((n) => n.id); - const unchecked = allIds.filter((nid) => !newChecked.includes(nid)); - gisCore.setLayerOnMap(newChecked, true); - gisCore.setLayerOnMap(unchecked, false); - - // 상위로 변경 사항 전달 - const checkedNodesData = nodes.filter(node => newChecked.includes(node.id)); - onCheckedChange?.(newChecked, checkedNodesData); - - return newChecked; - }); + + // ===== 초기 로딩 ===== + useEffect(() => { + if (treeData && treeData.length > 0) { + const nodeData = setNodeValues(treeData); + setNodes(nodeData); + gisCore.setAllLayersOnMap(false); + } + }, [treeData]); + + useEffect(() => { + if (nodes.length > 0) { + const defaultChecked = getDefaultCheckedNodes(nodes); + setChecked(defaultChecked); + + const defaultExpanded = Array.from( + new Set([ + ...defaultChecked, + ...defaultChecked.flatMap((id) => getAncestors(id, nodes)), + ]) + ); + setExpanded(defaultExpanded); + + gisCore.setLayerOnMap(defaultChecked, true); + + const defaultCheckedNodesData = nodes.filter((node) => + defaultChecked.includes(node.id) + ); + onCheckedChange?.(defaultChecked, defaultCheckedNodesData); + } }, [nodes]); - - // 침수위험지도 실행 함수 - const handleAutoCheckAnimation = useCallback((isEnabled, layerTypes = [], interval = 3000) => { - // 기존 타임아웃 정리 - if (timeoutIdRef.current) { - clearTimeout(timeoutIdRef.current); - timeoutIdRef.current = null; + + // ===== 체크 이벤트 ===== + const handleCheck = (id) => { + setChecked((prevChecked) => { + let newChecked = [...prevChecked]; + const isChecked = prevChecked.includes(id); + + if (isChecked) { + const descendants = getDescendants(id, nodes); + newChecked = newChecked.filter( + (cid) => cid !== id && !descendants.includes(cid) + ); + } else { + const descendants = getDescendants(id, nodes); + newChecked = Array.from(new Set([...newChecked, id, ...descendants])); } - + + newChecked = updateParentCheckState(id, nodes, newChecked); + + const allIds = nodes.map((n) => n.id); + const unchecked = allIds.filter((nid) => !newChecked.includes(nid)); + gisCore.setLayerOnMap(newChecked, true); + gisCore.setLayerOnMap(unchecked, false); + + const checkedNodesData = nodes.filter((node) => + newChecked.includes(node.id) + ); + onCheckedChange?.(newChecked, checkedNodesData); + + return newChecked; + }); + }; + + // ===== 특정 노드 체크/해제 ===== + const checkSpecificNode = useCallback( + (nodeId, shouldCheck = true) => { + setChecked((prevChecked) => { + let newChecked = [...prevChecked]; + const isCurrentlyChecked = prevChecked.includes(nodeId); + + if (shouldCheck && !isCurrentlyChecked) { + const descendants = getDescendants(nodeId, nodes); + newChecked = Array.from( + new Set([...newChecked, nodeId, ...descendants]) + ); + const ancestors = getAncestors(nodeId, nodes); + ancestors.forEach((p) => { + if (isEveryChildChecked(p, nodes, newChecked)) { + newChecked.push(p); + } + }); + } else if (!shouldCheck && isCurrentlyChecked) { + const descendants = getDescendants(nodeId, nodes); + newChecked = newChecked.filter( + (cid) => cid !== nodeId && !descendants.includes(cid) + ); + const ancestors = getAncestors(nodeId, nodes); + ancestors.forEach((p) => { + if (newChecked.includes(p)) { + newChecked = newChecked.filter((cid) => cid !== p); + } + }); + } + + const allIds = nodes.map((n) => n.id); + const unchecked = allIds.filter((nid) => !newChecked.includes(nid)); + gisCore.setLayerOnMap(newChecked, true); + gisCore.setLayerOnMap(unchecked, false); + + const checkedNodesData = nodes.filter((node) => + newChecked.includes(node.id) + ); + onCheckedChange?.(newChecked, checkedNodesData); + + return newChecked; + }); + }, + [nodes] + ); + + // ===== 자동 애니메이션 ===== + const handleAutoCheckAnimation = useCallback( + (isEnabled, layerTypes = [], interval = 3000) => { + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current); + timeoutIdRef.current = null; + } + if (!isEnabled || layerTypes.length === 0) { - // 애니메이션 비활성화 시 상태 초기화 + isPausedRef.current = false; + targetNodesRef.current = []; + currentAnimationIndexRef.current = 0; + onProgressChange(0, 0); + return; + } + + const getTargetNodesInOrder = () => { + const orderedNodes = []; + orderedLayers.forEach((lyrSn) => { + const node = nodes.find( + (n) => n.id === lyrSn && layerTypes.includes(n.data?.lyrTyNm) + ); + if (node) { + orderedNodes.push(node); + } + }); + return orderedNodes; + }; + + const targetNodes = getTargetNodesInOrder(); + + if (targetNodes.length === 0) return; + + if (targetNodesRef.current.length === 0) { + targetNodesRef.current = targetNodes; + currentAnimationIndexRef.current = 0; + onProgressChange(targetNodes.length, 0); + targetNodes.forEach((node) => { + checkSpecificNode(node.id, false); + }); + } + + isPausedRef.current = false; + + const processNextLayer = () => { + if (isPausedRef.current || !isPlaying) return; + + const currentTargetNodes = targetNodesRef.current; + const currentIndex = currentAnimationIndexRef.current; + + if (currentIndex < currentTargetNodes.length) { + checkSpecificNode(currentTargetNodes[currentIndex].id, true); + if (currentIndex > 0) { + checkSpecificNode(currentTargetNodes[currentIndex - 1].id, false); + } + + currentAnimationIndexRef.current++; + onProgressChange( + currentTargetNodes.length, + currentAnimationIndexRef.current + ); + timeoutIdRef.current = setTimeout(processNextLayer, interval); + } else { isPausedRef.current = false; targetNodesRef.current = []; currentAnimationIndexRef.current = 0; - onProgressChange(0, 0); - return; - } - - // 대상 노드들을 찾아서 특정 순서로 정렬 후 배열로 반환 - const getTargetNodesInOrder = () => { - const orderedNodes = []; - orderedLayers.forEach(lyrSn => { - const node = nodes.find(n => n.id === lyrSn && layerTypes.includes(n.data?.lyrTyNm)); - if (node) { - orderedNodes.push(node); - } - }); - - return orderedNodes; - }; - - const targetNodes = getTargetNodesInOrder(); - - if (targetNodes.length === 0) { - console.warn('대상 레이어를 찾을 수 없습니다:', layerTypes); - return; - } - - // 처음 시작하는 경우에만 초기화 - if (targetNodesRef.current.length === 0) { - targetNodesRef.current = targetNodes; - currentAnimationIndexRef.current = 0; - - console.log('자동 체크 대상 노드들 (순서대로):', targetNodes.map(n => ({ id: n.id, name: n.text, type: n.data?.lyrTyNm }))); - - // 진행률 초기화 (전체 단계 수 설정) - onProgressChange(targetNodes.length, 0); - - // 모든 대상 레이어를 먼저 언체크 - targetNodes.forEach(node => { - checkSpecificNode(node.id, false); - }); - } - - // 애니메이션 재개/시작 - isPausedRef.current = false; - - const processNextLayer = () => { - // 일시정지 상태이거나 애니메이션이 비활성화되면 중단 - if (isPausedRef.current || !isPlaying) { - return; - } - - const currentTargetNodes = targetNodesRef.current; - const currentIndex = currentAnimationIndexRef.current; - - if (currentIndex < currentTargetNodes.length) { - checkSpecificNode(currentTargetNodes[currentIndex].id, true); - if (currentIndex > 0) { - checkSpecificNode(currentTargetNodes[currentIndex - 1].id, false); - } - - currentAnimationIndexRef.current++; - - // 진행률 업데이트 - onProgressChange(currentTargetNodes.length, currentAnimationIndexRef.current); - - // 다음 레이어 처리를 위한 타이머 설정 - timeoutIdRef.current = setTimeout(processNextLayer, interval); - } else { - // 애니메이션 완료 - 상태 초기화 및 완료 신호 전송 - console.log('애니메이션 완료'); - isPausedRef.current = false; - targetNodesRef.current = []; - currentAnimationIndexRef.current = 0; - timeoutIdRef.current = null; - - // 진행률 100% 완료 - onProgressChange(currentTargetNodes.length, currentTargetNodes.length); - - // 잠시 후 진행률 초기화 - setTimeout(() => { - onProgressChange(0, 0); - }, 1000); - - setIsPlaying?.(false); - } - }; - - processNextLayer(); - - }, [nodes, checkSpecificNode, setIsPlaying, isPlaying, onProgressChange]); - - // 애니메이션 일시정지 함수 - const pauseAnimation = useCallback(() => { - if (timeoutIdRef.current) { - clearTimeout(timeoutIdRef.current); timeoutIdRef.current = null; - } - isPausedRef.current = true; - }, []); - - // 외부에서 전달받은 props로 자동 체크 제어 - useEffect(() => { - if (isPlaying) handleAutoCheckAnimation(true, targetLayerTypes, checkInterval); // 재생 시작/재개 - else pauseAnimation(); // 일시정지 - }, [isPlaying, handleAutoCheckAnimation, targetLayerTypes, checkInterval, pauseAnimation]); - - // 컴포넌트 언마운트 시 정리 - useEffect(() => { - return () => { - if (timeoutIdRef.current) { - clearTimeout(timeoutIdRef.current); - } + onProgressChange( + currentTargetNodes.length, + currentTargetNodes.length + ); + setTimeout(() => { + onProgressChange(0, 0); + }, 1000); + setIsPlaying?.(false); + } }; - }, []); - - - if (!treeData || treeData.length === 0) return null; - - - return ( - <> -
-
-

레이어

-
- - ( -
- {node.droppable && ( - - - )} - handleCheck(node.id)} className="cursor-pointer" /> - {node.icon && {node.icon}} - {node.text} - {imageVisible ? depth > 0 && { handleLayerImgClick(node.text, node.id, node.data.lyrTyNm) }} alt={node.text}> : null} -
- )} - sort={(a, b) => { - const aOrder = parseInt(a.data?.sortOrdr ?? '0'); - const bOrder = parseInt(b.data?.sortOrdr ?? '0'); - return aOrder - bOrder; - }} - /> -
-
- {/* 접기 버튼 */} - -
- {/* 열기 버튼 */} - {!visible && ( - - )} - + + processNextLayer(); + }, + [nodes, checkSpecificNode, setIsPlaying, isPlaying, onProgressChange] ); -}); + + const pauseAnimation = useCallback(() => { + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current); + timeoutIdRef.current = null; + } + isPausedRef.current = true; + }, []); + + useEffect(() => { + if (isPlaying) + handleAutoCheckAnimation(true, targetLayerTypes, checkInterval); + else pauseAnimation(); + }, [ + isPlaying, + handleAutoCheckAnimation, + targetLayerTypes, + checkInterval, + pauseAnimation, + ]); + + useEffect(() => { + return () => { + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current); + } + }; + }, []); + + // ===== 노드 세팅 ===== + const setNodeValues = (result) => { + return result.map((item) => { + const src = makeLegendSrc(item); + return { + id: item.lyrSn, // 반드시 고유값 + text: item.lyrNm, + parent: item.upperLyrSn || null, // 루트는 null + droppable: true, // children 여부 모를 땐 true로 둬야 트리 확장 가능 + data: item, + icon: src ? {item.lyrNm} : "", + }; + }); + }; + + + const makeLegendSrc = (layerinfo) => { + if (!layerinfo.gobjTy) return null; + if (layerinfo.iconNm) { + return GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm; + } else { + return makeLegendImage(layerinfo).src; + } + }; + + const makeLegendImage = (layerinfo) => { + let image = document.createElement("img"); + let canvas = document.createElement("canvas"); + canvas.width = 16; + canvas.height = 16; + let ctx = canvas.getContext("2d"); + ctx.beginPath(); + + if (layerinfo.gobjTy === "lineLabelPoligon") { + ctx.moveTo(1, 1); + ctx.lineTo(15, 1); + ctx.lineTo(15, 15); + ctx.lineTo(1, 15); + ctx.lineTo(1, 1); + } else if (layerinfo.gobjTy === "line") { + ctx.moveTo(1, 15); + ctx.lineTo(5, 1); + ctx.lineTo(9, 15); + ctx.lineTo(15, 1); + } else { + ctx.moveTo(1, 1); + ctx.lineTo(15, 1); + ctx.lineTo(15, 15); + ctx.lineTo(1, 15); + ctx.lineTo(1, 1); + } + + if (layerinfo.gobjTy === "lineLabelPoligon") { + ctx.fillStyle = layerinfo.polgnColor; + ctx.fill(); + ctx.lineWidth = 1; + ctx.strokeStyle = layerinfo.lnColor; + ctx.stroke(); + } + if (layerinfo.gobjTy === "line") { + ctx.lineWidth = 1; + ctx.strokeStyle = layerinfo.lnColor; + ctx.stroke(); + } + + image.src = canvas.toDataURL("image/png"); + return image; + }; + + const handleLayerImgClick = (nodeNm, nodeId) => { + if (!checked.includes(nodeId)) alert("시설을 선택해주세요"); + else onLayerIconClick(nodeNm); + }; + + if (!treeData || treeData.length === 0) return null; + + return ( + <> +
+
+

레이어

+
+ + { + const isVisible = node.data.exprsYn === "Y"; + const hasVisibleChildren = nodes.some( + (child) => + child.parent === node.id && child.data.exprsYn === "Y" + ); + + return ( +
+ {hasVisibleChildren ? ( + + ) : ( + + )} + handleCheck(node.id)} + className="cursor-pointer" + /> + {node.icon && {node.icon}} + + {node.text} + + {imageVisible && depth > 0 && ( + + + handleLayerImgClick( + node.data.lyrTyNm, + node.id + ) + } + alt={node.text} + /> + + )} +
+ ); + }} + sort={(a, b) => { + const aOrder = parseInt(a.data?.sortOrdr ?? "0"); + const bOrder = parseInt(b.data?.sortOrdr ?? "0"); + return aOrder - bOrder; + }} + /> +
+
+ +
+ {!visible && ( + + )} + + ); + }); + \ No newline at end of file diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js index a5268ac44..5a6e705da 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -16,11 +16,8 @@ import { Point } from 'ol/geom'; import { getTopLeft } from 'ol/extent'; import TileWMTS from 'ol/source/WMTS'; import WMTSTileGrid from 'ol/tilegrid/WMTS'; -// import { get as getProjection } from 'ol/proj'; import proj4 from 'proj4'; import { register } from 'ol/proj/proj4'; -import olSourceImageWMS from 'ol/source/ImageWMS'; -import { GeoJSON } from 'ol/format'; import GisOlEditLayer from './gis.edit'; import GisOlLayer from './gis.layer'; @@ -34,7 +31,6 @@ GisApp.layerInfos = []; GisApp.DROUGHT_DATES = { startYmd: '', endYmd: '' }; //기본정보 GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows'; -GisApp.SN_GEOSERVER_URL = '/sewerage-service/api/v1/map/geometry'; GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms'; //맵 정보 @@ -44,9 +40,8 @@ GisApp.GOOGLE_SATELLITE_URL = `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`; GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`; -// TODO: NGII 정보 -GisApp.NGII_API_KEY = '04trYP9_xwLAfALjwZ-B8g'; // NGII에서 발급받은 API 키 -GisApp.NGII_WMTS_URL = `https://map.ngii.go.kr/openapi/Gettile.do?apikey=${GisApp.NGII_API_KEY}`; +// NGII 정보(배경지도) +GisApp.NGII_WMTS_URL = process.env.NEXT_PUBLIC_NGII_URL; GisApp.EMAP_URL = '/emap/'; GisApp.EMAP_SATELLITE_URL = '/emap/satellite/'; @@ -563,18 +558,24 @@ GisApp.core.prototype = { */ setLayerOnMap: function (layerIds, visible, addQurryFlter) { for (let i in layerIds) { - + let id = layerIds[i]; let layer = this.getLayerById(id); if (layer) { layer.setVisible(visible); } else if (visible) { const layerCode = GisApp.layerCode[id]; + const actlLyrYn = layerCode.actlLyrYn; + if(actlLyrYn == 'N') continue; if(!layerCode.lyrTyNm) continue; const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || ''); layerCode['flterSql'] = queryFlter; - //layer = this.addWmsLayer(layerCode); - layer = this.addWfsLayer(layerCode); + + if(layerCode.srvcNm == 'wms'){ + layer = this.addWmsLayer(layerCode); + }else { + layer = this.addWfsLayer(layerCode); + } } } if (GisApp.Module.select) { diff --git a/packages/common/business_components/src/ui/map/lib/gis.style.js b/packages/common/business_components/src/ui/map/lib/gis.style.js index 8cff08dca..f38853962 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.style.js +++ b/packages/common/business_components/src/ui/map/lib/gis.style.js @@ -88,7 +88,7 @@ GisOlStyle.prototype = { } let baseResolution = 2; - let minScale = 0.4; + let minScale = 0.8; let maxScale = 1.5; let scale = baseResolution / resolution > maxScale ? maxScale : baseResolution / resolution; scale = Math.max(minScale, Math.min(maxScale, scale)); @@ -176,7 +176,7 @@ GisOlStyle.prototype = { const properties = feature.getProperties(); const overLayId = feature?.get('overLayId'); - const overLayMinZoom = layer.get('overLayMinZoom') || '12'; + const overLayMinZoom = layer.get('overLayMinZoom') || '10'; const overLayOffset = layer?.get('overLayOffset'); let findoOverlay = this.overlays[overLayId]; if (findoOverlay) { @@ -193,7 +193,8 @@ GisOlStyle.prototype = { if (typeof overlayHtml === 'string') { //const sanitizedHtml = DOMPurify.sanitize(overlayHtml); - overlayContainer.innerHTML = sanitizedHtml; + //overlayContainer.innerHTML = sanitizedHtml; + overlayContainer.innerHTML = overlayHtml; } // React 컴포넌트일 경우 createPortal() 사용 else if (React.isValidElement(overlayHtml)) { @@ -205,7 +206,7 @@ GisOlStyle.prototype = { element: overlayContainer, positioning: 'bottom-center', stopEvent: false, - offset: overLayOffset ? overLayOffset : [0, 0], // 오프셋 설정 (위치 조정) + offset: overLayOffset ? overLayOffset : [80, 0], // 오프셋 설정 (위치 조정) }); this.overlays[overLayId] = overlay; @@ -274,7 +275,8 @@ GisOlStyle.prototype = { let fontExtnColor = layer.get('fontExtnColor'); let fontExtnWdth = Number(layer.get('fontExtnWdth') || '1'); let fontIntnColor = layer.get('fontIntnColor'); - let minViewSize = layer.get('minViewSize'); + //let minViewSize = layer.get('minViewSize') || 13; + let minViewSize = 14; if (feature == null) { return; } @@ -282,14 +284,14 @@ GisOlStyle.prototype = { let label = prop[layer.get('lblAtrbNm')]; let scaleFactor = 1 / resolution; // 해상도가 낮아질수록 글자 크기 증가 const zoom = GisApp.Module.core.map.getView().getZoom(); - if (zoom < minViewSize - 3) { + if (zoom <= minViewSize) { label = ''; } let text = new Text({ - stroke: new Stroke({ - color: '#FFFFFF', // 흰색 테두리 - width: fontExtnWdth, - }), + // stroke: new Stroke({ + // color: '#FFFFFF', // 흰색 테두리 + // width: fontExtnWdth, + // }), fill: new Fill({ color: fontIntnColor, width: fontExtnWdth, diff --git a/packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx b/packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx new file mode 100644 index 000000000..97ba51fb7 --- /dev/null +++ b/packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx @@ -0,0 +1,122 @@ +// SearchInputText.tsx +"use client"; +import { Input } from "@common/components/ui"; +import { useEffect, useRef, useState } from "react"; + +interface SearchInputTextProps { + initValue?: string; + setValue: (val: string) => void; + setRtnData: (val: { fnm: string; fid: string }) => void; + searchList: { fnm: string; fid: string }[]; + onEnter?: () => void; + className?: string; +} + +export default function SearchInputText({ + initValue, + setValue, + setRtnData, + searchList, + onEnter, + className, +}: SearchInputTextProps) { + const [showResults, setShowResults] = useState(false); + const [text, setText] = useState(initValue ?? ""); + const [highlightIndex, setHighlightIndex] = useState(-1); + const wrapperRef = useRef(null); + + useEffect(() => { + setText(initValue ?? ""); + }, [initValue]); + + useEffect(() => { + if (searchList.length > 0) { + setShowResults(true); + } else { + setShowResults(false); + } + setHighlightIndex(-1); + }, [searchList]); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + wrapperRef.current && + !(wrapperRef.current as any).contains(event.target) + ) { + setShowResults(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [wrapperRef]); + + const handleChange = (e: React.ChangeEvent) => { + setText(e.target.value); + setValue(e.target.value); + setHighlightIndex(-1); + }; + + const handleSelect = (item: { fnm: string; fid: string }) => { + setText(item.fnm); + setValue(item.fnm); + setRtnData(item); + setShowResults(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + if (highlightIndex >= 0 && highlightIndex < searchList.length) { + const selected = searchList[highlightIndex]; + if (selected) handleSelect(selected); + } else if (onEnter) { + onEnter(); + } + } else if (e.key === "ArrowDown") { + e.preventDefault(); + setHighlightIndex((prev) => + prev < searchList.length - 1 ? prev + 1 : prev + ); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setHighlightIndex((prev) => (prev > 0 ? prev - 1 : -1)); + } + }; + + return ( +
+ + {showResults && ( +
+ {searchList.length > 0 ? ( + searchList.map((item, index) => ( +
handleSelect(item)} + > + {item.fnm} +
+ )) + ) : ( +
+ 검색 결과가 없습니다. +
+ )} +
+ )} +
+ ); +} diff --git a/packages/common/business_components/src/ui/map/searchBar/index.tsx b/packages/common/business_components/src/ui/map/searchBar/index.tsx new file mode 100644 index 000000000..4ba24de3a --- /dev/null +++ b/packages/common/business_components/src/ui/map/searchBar/index.tsx @@ -0,0 +1,3 @@ +export * from "./comboSearchBar"; +export * from "./procSearchBar"; +export * from './searchBar' diff --git a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx new file mode 100644 index 000000000..edd2853f2 --- /dev/null +++ b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx @@ -0,0 +1,344 @@ +"use client"; +import { SelectBox, type SelectDataType } from "../../../ui"; +import { Button } from "@common/components/ui"; +import { useCallback, useEffect, useState } from "react"; +import SearchInputText from "./SearchInpuText"; +import GisApp from "../lib/gis.core"; + +const FACILITY_TYPE_URL_MAP: Record = { + tb_il_swage_prcs_fclt: + "/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_il_swage_prcs_fclt&outputFormat=application/json&propertyName=swtrmt_fclt_cd,swtrmt_fclt_nm&cql_filter=", + tb_wr_bdst_ruse_fclt: + "/reuse-service/reuseFc/pubDetailList/{year}/{sggCd}/1/1000/", + tb_wr_rnwtr_utztn_fclt: + "/reuse-service/reuseFc/rainyInfoList/1/1000/{sggCd}/{fcltyNm}", + tb_wr_rswtr_fclt: + "/reuse-service/reuseFc/waterreuseFc/1/1000/{sggCd}/{fcltyNm}", + tb_wr_htdfl_fclt: + "/reuse-service/drain/facilityList/1/1000/{sggCd}/{fcltyNm}", + tb_wr_ruse_fclt_dmd_co: + "/reuse-service/reuseFc/request/1/1000/{sggCd}/{fcltyNm}", + tb_wr_tmpr_dmd_co: + "/reuse-service/reuseFc/request/temp/1/1000/{sidoNm} {sggNm}/{fcltyNm}", +}; + +interface SearchBarProps { + gisMaps: React.RefObject; + facilityTypes: { text: string; val: string }[]; + onFeatureFocus: any; +} + +export const SearchBar = ({ + gisMaps, + facilityTypes, + onFeatureFocus, +}: SearchBarProps) => { + const [sidoValue, setSidoValue] = useState("0"); + const [sigunValue, setSigunValue] = useState("0"); + const [facilityType, setFacilityType] = useState( + facilityTypes?.length === 1 && facilityTypes[0] ? facilityTypes[0].val : "0" + ); + const [fcltyNm, setFcltyNm] = useState(""); + const [searchList, setSearchList] = useState<{ fnm: string; fid: string }[]>( + [] + ); + + const [sidoData, setSidoData] = useState([ + { text: "시도", val: "0" }, + ]); + const [sigunData, setSigunData] = useState([ + { text: "시군구", val: "0" }, + ]); + const [filteredSigunData, setFilteredSigunData] = useState([ + { text: "시군구", val: "0" }, + ]); + + const fetchSido = async () => { + const res = await fetch("/cms-service/api/v1/code/cache/ADSTDSTRCD/Y"); + const json = await res.json(); + const sidoList = json + .filter((item: any) => item.lwpoCd?.length === 2) + .map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm })); + const sigunList = json + .filter((item: any) => item.lwpoCd?.length > 2) + .map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm })); + setSidoData([{ text: "시도", val: "0" }, ...sidoList]); + setSigunData(sigunList); + }; + + const initializeSigunData = useCallback( + (sidoCd: string) => { + if (sidoCd === "0") { + setFilteredSigunData([{ text: "시도", val: "0" }]); + setSigunValue("0"); + return; + } + const prefix = sidoCd.substring(0, 2); + const filtered = sigunData.filter((item) => item.val.startsWith(prefix)); + setFilteredSigunData([{ text: "시군구", val: "0" }, ...filtered]); + setSigunValue("0"); + }, + [sigunData] + ); + + useEffect(() => { + fetchSido(); + }, []); + + const onSelectValue = useCallback( + (val: string, type: string) => { + if (type === "ctpvCd") { + setSidoValue(val); + initializeSigunData(val); + setSearchList([]); + setFcltyNm(""); + } else if (type === "sggCd") { + setSigunValue(val); + setSearchList([]); + setFcltyNm(""); + } else if (type === "facilityType") { + setFacilityType(val); + setSearchList([]); + setFcltyNm(""); + } + }, + [initializeSigunData] + ); + + const handleSearch = async () => { + if (!fcltyNm) return alert("시설명을 입력하세요."); + + const sidoNm = sidoData.find((item) => item.val === sidoValue)?.text || ""; + const sggNm = sigunData.find((item) => item.val === sigunValue)?.text || ""; + + const type = facilityType; + const baseUrlTemplate = FACILITY_TYPE_URL_MAP[type]; + if (!baseUrlTemplate) return; + + let url = baseUrlTemplate; + const year = new Date().getFullYear().toString(); + url = url + .replace("{year}", year) + .replace("{sggCd}", sigunValue) + .replace("{sidoNm}", encodeURIComponent(sidoNm)) + .replace("{sggNm}", encodeURIComponent(sggNm)) + .replace("{fcltyNm}", encodeURIComponent(fcltyNm)); + + let list: SelectDataType[] = []; + + if (type === "tb_il_swage_prcs_fclt") { + const cqlFilter = `ctpv_nm='${sidoNm}' and sgg_nm='${sggNm}' and swtrmt_fclt_nm like '%${fcltyNm}%'`; + url += encodeURIComponent(cqlFilter); + const res = await fetch(url); + const json = await res.json(); + list = json.features.map((f: any) => ({ + text: f.properties.swtrmt_fclt_nm ?? "(이름없음)", + val: f.id, + })); + } else if ( + type === "tb_wr_bdst_ruse_fclt" // 재이용시설 + ) { + //재이용에서 참조하는 하수처리시설 목록 + const procRes = await fetch("/reuse-service/reuseFc/pubFc"); + const procJson = await procRes.json(); + const procData = procJson.data; + const procResult = procData + .filter((code: string) => code.slice(0, 5) === sigunValue) + .map((code: string) => { + return code; + }); + + //처리시설명으로 처리시설코드 조회 + let procUrl2 = + "/operation-service/prcsFclt/ruseInfo/1/1000/A/{sggCd}/0/1/SW/{fcltyNm}"; + procUrl2 = procUrl2 + .replace("{sggCd}", sigunValue) + .replace("{fcltyNm}", encodeURIComponent(fcltyNm)); + const procRes2 = await fetch(procUrl2, { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(procResult), + }); + const procJson2 = await procRes2.json(); + const procResult2 = (procJson2.data || []).map((item: any) => item.fid); + + //최종 처리시설코드로 재이용시설조회 + const res = await fetch(url, { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(procResult2), + }); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + 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 ?? "(이름없음)", + val: item.dmdInfoId, + })); + } else if ( + type === "tb_wr_tmpr_dmd_co" //잠재수요처 + ) { + // GET 방식 + const res = await fetch(url); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + text: item.dmdCoNm ?? "(이름없음)", + val: item.fctryMngSn, + })); + } else { + // GET 방식 + const res = await fetch(url); + const json = await res.json(); + list = (json.data || []).map((item: any) => ({ + text: item.bdstNm ?? "(이름없음)", + val: item.bdstInfoSn, + })); + } + + if (list.length === 0) { + alert("검색 결과가 없습니다."); + setSearchList([]); + setFcltyNm(""); + return; + } + + const newList = list.map((item: any) => { + const code = item.val; + const name = item.text; + return { fnm: name, fid: code }; + }); + setSearchList(newList); + }; + + //해당 시설물로 이동 + const handleFeatureFocus = (data: any) => { + const map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select as any; + if (!select) return; + // 1. 기존 선택 초기화 + //select.resetSelectedFeature(); + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature: any = null; + let featureId = data.fid; + map + .getLayers() + .getArray() + .forEach((layer: any) => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + // 3. 피처 선택 + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } else { + console.warn("geometry를 찾을 수 없습니다:", data); + } + } else { + console.warn("Feature를 찾을 수 없습니다:", data); + } + + onFeatureFocus(foundFeature); + }; + + return ( +
+
+
+
+ {facilityTypes.length > 1 && ( + + onSelectValue(val.toString(), "facilityType") + } + value={facilityType} + /> + )} + onSelectValue(val.toString(), "ctpvCd")} + value={sidoValue} + /> + onSelectValue(val.toString(), "sggCd")} + value={sigunValue} + /> + { + setFcltyNm(val); + }} + setRtnData={(selected: { fnm: string; fid: string }) => { + // 선택된 항목이 있을 경우 onFeatureSelect 실행 + const type = facilityType; + handleFeatureFocus({ + layerNm: type, + ...selected, + }); + }} + searchList={searchList} + onEnter={handleSearch} + className="mr-5 w-[150px]" + /> +
+
+ +
+
+
+
+ ); +} From 3cc8fcb2eb5bfa4d7a0665054b562854a80dffdf Mon Sep 17 00:00:00 2001 From: swcho Date: Sat, 9 Aug 2025 23:46:20 +0900 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/searchBar/{SearchInpuText.tsx => SearchInputText.tsx} | 0 .../business_components/src/ui/map/searchBar/searchBar.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/common/business_components/src/ui/map/searchBar/{SearchInpuText.tsx => SearchInputText.tsx} (100%) diff --git a/packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx b/packages/common/business_components/src/ui/map/searchBar/SearchInputText.tsx similarity index 100% rename from packages/common/business_components/src/ui/map/searchBar/SearchInpuText.tsx rename to packages/common/business_components/src/ui/map/searchBar/SearchInputText.tsx diff --git a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx index edd2853f2..e750ab1a7 100644 --- a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx +++ b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx @@ -2,7 +2,7 @@ import { SelectBox, type SelectDataType } from "../../../ui"; import { Button } from "@common/components/ui"; import { useCallback, useEffect, useState } from "react"; -import SearchInputText from "./SearchInpuText"; +import SearchInputText from "./SearchInputText"; import GisApp from "../lib/gis.core"; const FACILITY_TYPE_URL_MAP: Record = { From 57fc624fbc03fa77661eb41f0682793604e5e369 Mon Sep 17 00:00:00 2001 From: swcho Date: Sun, 10 Aug 2025 00:29:50 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20WR(=EB=AC=BC=EC=9E=AC=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9)=20=EA=B0=80=EB=AD=84=EC=A7=80=EB=8F=84=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=EA=B5=AC=ED=98=84=20(=EC=9E=A0=EC=9E=AC=EC=88=98?= =?UTF-8?q?=EC=9A=94=EC=B2=98,=20=EC=88=98=EC=9A=94=EC=B2=98=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=ED=95=84=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(map-layout)/map/mainMap/page.tsx | 2 +- apps/wr/app/(map-layout)/service/map.ts | 4 +- .../mapDrgtPopUp/drgtLegend.tsx | 70 +++++++ .../app/(popUp-layout)/mapDrgtPopUp/median.ts | 24 +++ .../app/(popUp-layout)/mapDrgtPopUp/page.tsx | 88 +++++++++ .../(popUp-layout)/mapDrgtPopUp/searchBar.tsx | 173 ++++++++++++++++++ .../matMng/coMng/drgtSttn/page.tsx | 28 ++- apps/wr/middleware.ts | 7 +- 8 files changed, 385 insertions(+), 11 deletions(-) create mode 100644 apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx create mode 100644 apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts create mode 100644 apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx create mode 100644 apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index 331e487bd..7451ae4b5 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -24,7 +24,7 @@ const Map = () => { const fetchData = async () => { try { - const response = await mapService.getLyrStngs(); + const response = await mapService.getLyrStngs('WR_NETWORK'); setTreeData(response); // API 응답 저장 } catch (error) { console.error("데이터 불러오기 실패:", error); diff --git a/apps/wr/app/(map-layout)/service/map.ts b/apps/wr/app/(map-layout)/service/map.ts index 5002afef7..79ba7fe9e 100644 --- a/apps/wr/app/(map-layout)/service/map.ts +++ b/apps/wr/app/(map-layout)/service/map.ts @@ -30,8 +30,8 @@ export type LyrStngProps = { export const mapService = { //레이어 설정 조회 - getLyrStngs: async () => { - const response = await fetch(`${API_URL}/lyrStngs/WR_NETWORK`, { + getLyrStngs: async (lyrGrp : string) => { + const response = await fetch(`${API_URL}/lyrStngs/${lyrGrp}`, { method: 'GET', headers: { 'Accept': 'application/json', diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx new file mode 100644 index 000000000..9c558851c --- /dev/null +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx @@ -0,0 +1,70 @@ +"use client"; + +import React from "react"; +import { Label } from "@common/components/ui/label"; + +export const DRGT_LAYER_TYPES: string[] = [ + // 수정 필요 + 'drought1', + 'drought2', + 'drought3', + 'drought4', + 'drought5', + 'drought6', +]; +// 지정된 순서에 따른 lyr_sn 배열 +export const EXPRS_ORDERED_LAYERS: string[] = [ + // 수정 필요 + '106', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 30년 빈도 + '107', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 50년 빈도 + '108', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 80년 빈도 + '109', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 100년 빈도 + '110', // 도시침수지도(tb_ms_flood_dngr_map) - 30년 빈도 + '111', // 도시침수지도(tb_ms_flood_dngr_map) - 50년 빈도 +]; + +export function DrgtLegend() { + + + return ( + <> +
+
+ +
+
+
+
+
+
+
+
가 뭄 정 보
+
+ + {/* 관심 - 1회 미만 */} + {/* 관심 - 1회 이상 */} + + {/* 주의 - 1회 미만 */} + {/* 주의 - 1회 이상 */} + + {/* 관심+주의 - 1회 미만 */} + {/* 관심+주의 - 1회 이상 */} +
+
+
+
+ + ); +} + +function LegendItem({ color, label }: { color: string; label: string }) { + return ( +
+ + {label} +
+ ); +} \ No newline at end of file diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts new file mode 100644 index 000000000..ab63e7c91 --- /dev/null +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts @@ -0,0 +1,24 @@ +import { boundingExtent, getCenter } from 'ol/extent'; +import Feature from 'ol/Feature'; +import Geometry from 'ol/geom/Geometry'; + +export function medianCoordinate2D(feature: Feature) { + // Feature를 입력 받아 중심 좌표를 계산하는 함수 + if (!feature) { + console.warn("유효한 피처가 없습니다."); + return; + } + + console.log(feature) + + const geometry = feature.getGeometry(); + if (!geometry) { + console.warn("피처에 geometry가 없습니다."); + return; + } + + const extent = geometry.getExtent(); + const center = getCenter(extent); + const centerExtent = boundingExtent([center]); + return centerExtent; +} \ No newline at end of file diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx new file mode 100644 index 000000000..a2c843351 --- /dev/null +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/page.tsx @@ -0,0 +1,88 @@ +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import { useEffect, useRef, useState, useCallback } from "react"; +import { useSearchParams } from "next/navigation"; +import { mapService } from "../../(map-layout)/service"; +import { DrgtLegend, DRGT_LAYER_TYPES, EXPRS_ORDERED_LAYERS } from "./drgtLegend"; +import { SearchBar } from "./searchBar"; + + +export default function MapDrgtPopUp() { + const searchParams = useSearchParams(); + const ctpvCd = searchParams.get('ctpvCd') || ''; + const sggCd = searchParams.get('sggCd') || ''; + const fromDate = searchParams.get('fromDate') || ''; + const toDate = searchParams.get('toDate') || ''; + + const gisMaps = useRef(null); + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [showDrgtLegend, setShowDrgtLegend] = useState(false); // DrgtLegend 표시 여부 + const [isPlaying, setIsPlaying] = useState(false); + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchData(); + }, []); + + function fetchData() { + (async () => { + try { + const response = await mapService.getLyrStngs('WR_DROUGHT'); + setTreeData(response); // API 응답 저장 + console.log("Drought layers fetched:", response); + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + })(); + } + + // WaterDepth 표시 여부를 결정하는 함수 + const handleDrgtLegndVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => { + const drgtLegndLayers = checkedNodesData.filter(node => + DRGT_LAYER_TYPES.includes(node.data?.lyrTyNm) + ); + + const hasDrgtLegndLayer = drgtLegndLayers.length > 0; + setShowDrgtLegend(hasDrgtLegndLayer); + }, []); + + // 체크된 레이어 변경 처리 함수 + const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => { + handleDrgtLegndVisibility(checkedIds, checkedNodesData); + }, [handleDrgtLegndVisibility]); + + + + return ( +
+ } + waterDepth={showDrgtLegend ? DrgtLegend : undefined} + isPlaying={isPlaying} + setIsPlaying={setIsPlaying as any} + targetLayerTypes={DRGT_LAYER_TYPES as any} + orderedLayers={EXPRS_ORDERED_LAYERS as any} + checkInterval={2500} + height={`h-[100vh]`} + droughtDates={{ startYmd: fromDate, endYmd: toDate }} + /> +
+ ); +} diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx new file mode 100644 index 000000000..21a98d696 --- /dev/null +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx @@ -0,0 +1,173 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { SelectBox } from '@common/business_components/ui' +import { Label, Button } from '@common/components'; +import { type SelectDataType } from "@common/business_components/ui"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import GeoJSON from "ol/format/GeoJSON"; +import Feature from "ol/Feature"; +import Geometry from "ol/geom/Geometry"; +import { medianCoordinate2D } from "./median"; + +interface CtpvSggPrcsFcltSelectBoxProps { + gisMaps: React.RefObject; + ctpvCode: string; + sggCode: string; + fromDate: string; + toDate: string; +} + +const DEFAULT_OPTION: SelectDataType = { val: '0', text: '전체' }; + +export function SearchBar({ gisMaps, ctpvCode, sggCode }: CtpvSggPrcsFcltSelectBoxProps) { + const [ctpvCd, setCtpvCd] = useState(ctpvCode); + const [sggCd, setSggCd] = useState(sggCode); + const [ctpv, setCtpv] = useState([]); + const [sgg, setSgg] = useState([]); + + useEffect(() => { + (async () => { + await fetchSido(); + await fetchSigungu(); + setCtpvCd(ctpvCode); + setSggCd(sggCode); + })(); + }, []); + + function onViewMapClick() { + if (sggCd && sggCd !== '0') { + fetchSggData(); + return; + } + if (ctpvCd && ctpvCd !== '0') fetchCtpvData(); + } + + async function fetchCtpvData() { + try { + const ctpvData = await fetch(`/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo%3Atb_ms_ctpv&outputFormat=application%2Fjson&cql_filter=mctpv_cd=%27${ctpvCd.trim()}%27&srsName=EPSG:5179`) + .then(res => res.json()); + console.log('시도 데이터', ctpvData.features[0]); + + const format = new GeoJSON(); + const feature: Feature = format.readFeature(ctpvData.features[0], { + dataProjection: "EPSG:5179", // GeoJSON 좌표계 + featureProjection: GisApp.TO_PROJ // 지도 좌표계 + }); + + console.log('feature', feature); + + const extent = medianCoordinate2D(feature); + console.log('GisApp.TO_PROJ', GisApp.TO_PROJ); + console.log('extent', extent) + + handleFeatureFocus(extent); + } + catch (error) { + console.error("시도 data fetching Error:", error); + } + } + + async function fetchSggData() { + try { + const sggData = await fetch(`/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo%3Atb_ms_sgg&outputFormat=application%2Fjson&cql_filter=sgg_cd=%27${sggCd}%27&srsName=EPSG:5179`) + .then(res => res.json()); + console.log('시군구 데이터', sggData.features[0]); + + + const format = new GeoJSON(); + const feature: Feature = format.readFeature(sggData.features[0], { + dataProjection: "EPSG:5179", // GeoJSON 좌표계 + featureProjection: GisApp.TO_PROJ // 지도 좌표계 + }); + + const extent = medianCoordinate2D(feature); + + handleFeatureFocus(extent); + } + catch (error) { + console.error("시군구 data fetching Error:", error); + } + } + + function handleFeatureFocus(extent: any) { + console.log('ctpv', ctpv); + console.log('sgg', sgg); + const map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 11 + }); + }; + + const fetchSido = async () => { + const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_ctpv&outputFormat=application/json&propertyName=mctpv_korn_nm,mctpv_cd`; + const res = await fetch(url).then(res => res.json()); + + const list = res.features.map((f: any) => ({ + text: f.properties.mctpv_korn_nm, + val: f.properties.mctpv_cd, + })); + + console.log('시도 데이터', list); + setCtpv(list); + }; + + const fetchSigungu = async () => { + const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_sgg&outputFormat=application/json&propertyName=sgg_cd,sgg_korn_nm`; + const res = await fetch(url).then(res => res.json()); + + const list = res.features.map((f: any) => ({ + text: f.properties.sgg_korn_nm, + val: f.properties.sgg_cd, + })); + + console.log('시군구 데이터', list); + setSgg(list); + }; + + + + return ( +
+
+ + { + setCtpvCd(String(val)); + setSggCd(String(0)); + }} + selectContentClass="z-[101]" + /> + + String(opt.val).slice(0, 2) === String(ctpvCd).trim()) + ] + } + value={sggCd} + onSelectValue={val => { setSggCd(String(val)) }} + selectContentClass="z-[101]" + /> +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx b/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx index be7210f3f..f6881688a 100644 --- a/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx @@ -9,7 +9,7 @@ import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/b import { ContentsDiv, DateMonthPicker, downloadExcel, SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, Title, UiModal, UiTable, type SelectDataType } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; import { Button, Label, ScrollArea } from "@common/components/ui"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { toast, Toaster } from "react-hot-toast"; export default function DrgtSttn() { @@ -26,6 +26,7 @@ export default function DrgtSttn() { isOpen: boolean; data: customDrgt[] | undefined; }>({ data: undefined, isOpen: false }); + const popupRef = useRef(null); const cellClick = (index: number) => { if (fromDate && toDate && drgtRes[index]) { @@ -121,6 +122,19 @@ export default function DrgtSttn() { setToDate(formatted); } }, [fromDate, toDate]) + + function onViewMapClick() { + const params = new URLSearchParams({ + ctpvCd: String(objSearch.city), + sggCd: String(objSearch.sgg), + fromDate: fromDate || '', + toDate: toDate || '' + }); + const popupOptions = `width=${window.screen.availWidth},height=${window.screen.availHeight},left=0,top=0,resizable=yes,scrollbars=yes`; + popupRef.current = window.open(`/mapDrgtPopUp?${params.toString()}`, "가뭄 지도 보기 팝업", popupOptions); + } + + return ( <> @@ -136,7 +150,7 @@ export default function DrgtSttn() { selectClass={""} selectArray={code.cityCd || []} onSelectValue={getSelCity} - value={objSearch?.city ||code.cityCd[0]?.val ||""} + value={objSearch?.city || code.cityCd[0]?.val || ""} />
@@ -156,7 +170,7 @@ export default function DrgtSttn() { onChange: (date: string | undefined) => { const year = date ? date.slice(0, 4) : ''; let month = date?.slice(4); - if (month&&month?.length < 2) { + if (month && month?.length < 2) { month = month?.padStart(2, "0"); } const formatted = `${year}${month}`; @@ -172,7 +186,7 @@ export default function DrgtSttn() { onChange: (date: string | undefined) => { const year = date ? date.slice(0, 4) : ''; let month = date?.slice(4); - if (month&&month.length < 2) { + if (month && month.length < 2) { month = month?.padStart(2, "0"); } const formatted = `${year}${month}`; @@ -211,7 +225,7 @@ export default function DrgtSttn() { sggText = sidoText; } - const searchArr:string[] = []; + const searchArr: string[] = []; if (sidoText) searchArr.push(sidoText); if (Array.isArray(sggText)) { searchArr.push(...sggText); @@ -234,7 +248,7 @@ export default function DrgtSttn() { diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index d068f6aa5..c5c7f8c01 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -91,6 +91,8 @@ export function middleware(req: NextRequest) { let queryParams = searchParams.toString(); const typename = searchParams.get("TYPENAME"); console.log("typename ", typename); + const startYmd = searchParams.get("START_YMD") ?? null; + const endYmd = searchParams.get("END_YMD") ?? null; let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; @@ -116,10 +118,13 @@ export function middleware(req: NextRequest) { typename == "hasudo:tb_wr_rswtr_fclt" || //중수도시설 typename == "hasudo:tb_wr_htdfl_fclt" || //온배수시설 typename == "hasudo:tb_wr_ruse_fclt_dmd_co" || //수요처 - typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처 + typename == "hasudo:tb_wr_tmpr_dmd_co" || //잠재수요처 + typename?.startsWith("hasudo:drought") //가뭄 지도 ) { const url = req.nextUrl.clone(); url.searchParams.set("TYPENAME", typename.replace("hasudo:", "")); + if (startYmd) url.searchParams.set("startYmd", startYmd); + if (endYmd) url.searchParams.set("endYmd", endYmd); queryParams = url.searchParams.toString(); newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`; } From bd3015dec56d7b747b0a91319cd033a7640dc6e1 Mon Sep 17 00:00:00 2001 From: swcho Date: Sun, 10 Aug 2025 00:40:23 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20=EA=B0=9C=EB=B0=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts | 6 +++--- apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts index ab63e7c91..6a216813e 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts @@ -1,6 +1,6 @@ -import { boundingExtent, getCenter } from 'ol/extent'; -import Feature from 'ol/Feature'; -import Geometry from 'ol/geom/Geometry'; +import { boundingExtent, getCenter } from 'ol/extent.js'; +import Feature from 'ol/Feature.js'; +import Geometry from 'ol/geom/Geometry.js'; export function medianCoordinate2D(feature: Feature) { // Feature를 입력 받아 중심 좌표를 계산하는 함수 diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx index 21a98d696..110dc377a 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx @@ -5,9 +5,9 @@ import { SelectBox } from '@common/business_components/ui' import { Label, Button } from '@common/components'; import { type SelectDataType } from "@common/business_components/ui"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; -import GeoJSON from "ol/format/GeoJSON"; -import Feature from "ol/Feature"; -import Geometry from "ol/geom/Geometry"; +import GeoJSON from "ol/format/GeoJSON.js"; +import Feature from "ol/Feature.js"; +import Geometry from "ol/geom/Geometry.js"; import { medianCoordinate2D } from "./median"; interface CtpvSggPrcsFcltSelectBoxProps { From 5b4b8fbd80a9804b9def7ff1580327e602f047f5 Mon Sep 17 00:00:00 2001 From: swcho Date: Sun, 10 Aug 2025 00:48:54 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20=EA=B0=9C=EB=B0=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=ED=83=80=EC=9E=85=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts | 7 ++++--- apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts index 6a216813e..6bb5f78c9 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts @@ -1,6 +1,7 @@ -import { boundingExtent, getCenter } from 'ol/extent.js'; -import Feature from 'ol/Feature.js'; -import Geometry from 'ol/geom/Geometry.js'; +import boundingExtent from 'ol/extent'; +import getCenter from 'ol/extent'; +import Feature from 'ol/Feature'; +import Geometry from 'ol/geom/Geometry'; export function medianCoordinate2D(feature: Feature) { // Feature를 입력 받아 중심 좌표를 계산하는 함수 diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx index 110dc377a..21a98d696 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx @@ -5,9 +5,9 @@ import { SelectBox } from '@common/business_components/ui' import { Label, Button } from '@common/components'; import { type SelectDataType } from "@common/business_components/ui"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; -import GeoJSON from "ol/format/GeoJSON.js"; -import Feature from "ol/Feature.js"; -import Geometry from "ol/geom/Geometry.js"; +import GeoJSON from "ol/format/GeoJSON"; +import Feature from "ol/Feature"; +import Geometry from "ol/geom/Geometry"; import { medianCoordinate2D } from "./median"; interface CtpvSggPrcsFcltSelectBoxProps { From b5c111fef07a3a8083331d2b411456673f405e11 Mon Sep 17 00:00:00 2001 From: swcho Date: Sun, 10 Aug 2025 00:52:35 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=EA=B0=9C=EB=B0=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20import=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts index 6bb5f78c9..ab63e7c91 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/median.ts @@ -1,5 +1,4 @@ -import boundingExtent from 'ol/extent'; -import getCenter from 'ol/extent'; +import { boundingExtent, getCenter } from 'ol/extent'; import Feature from 'ol/Feature'; import Geometry from 'ol/geom/Geometry'; From fe524f98b40547d7bb3ea51c284006be46e514a8 Mon Sep 17 00:00:00 2001 From: swcho Date: Sun, 10 Aug 2025 00:59:52 +0900 Subject: [PATCH 8/8] =?UTF-8?q?test:=20=EA=B0=9C=EB=B0=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=B0=B0=ED=8F=AC=20=EC=97=90=EB=9F=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20wr=EC=97=90=20ol=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/wr/package.json b/apps/wr/package.json index a29dcd66b..bb170c820 100644 --- a/apps/wr/package.json +++ b/apps/wr/package.json @@ -21,6 +21,8 @@ "lucide-react": "^0.474.0", "moment": "^2.30.1", "next": "15.1.7", + "ol": "^7.3.0", + "ol-ext": "^4.0.11", "react-hook-form": "^7.54.2", "react-hot-toast": "^2.5.2", "react-i18next": "^15.6.0",