diff --git a/apps/dm/.env.development b/apps/dm/.env.development index 08568f984..96f4da6f5 100644 --- a/apps/dm/.env.development +++ b/apps/dm/.env.development @@ -10,9 +10,7 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token - - - +NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1 NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API # NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 #NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 diff --git a/apps/dm/.env.local b/apps/dm/.env.local index 27a3b0e98..b60a1d86f 100644 --- a/apps/dm/.env.local +++ b/apps/dm/.env.local @@ -13,8 +13,9 @@ NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload -#NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API -NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 +NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 #NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 diff --git a/apps/dm/.env.production b/apps/dm/.env.production index 7576a7dfb..85e97b3ab 100644 --- a/apps/dm/.env.production +++ b/apps/dm/.env.production @@ -11,7 +11,7 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token - +NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1 NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api diff --git a/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page.tsx b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page.tsx index 670b2a612..71eaad57d 100644 --- a/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page.tsx +++ b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page.tsx @@ -74,7 +74,7 @@ export type FormData = z.infer; function SdmInfoInsertContent() { const searchParams = useSearchParams(); - const {map, insertSdmInfoData, Sdm ,setSubsInfo} = useDmStore((state) => state); + const {insertSdmInfoData, Sdm ,setSubsInfo} = useDmStore((state) => state); const { successLogin } = Sdm; const [ismoneyDisabled, setIsmoneyDisabled] = useState(false); const formSchema = sdmInfoFormSchema; @@ -129,7 +129,7 @@ function SdmInfoInsertContent() { } setSubsInfo([]); // subsInfo 초기화 후보지 처음 화면 들어올때 초기화(스토어내 객체만) - }, []); + }, [setSubsInfo]); const form = useForm({ resolver: zodResolver(formSchema), @@ -137,15 +137,27 @@ function SdmInfoInsertContent() { mode: "onChange", }); - + const subsInfo = useDmStore((state) => state.map.subsInfo); useEffect(()=>{ //map.subsInfo 스토어 객체 값이 변경되면 해당 값이 폼에 자동으로 들어가도록 수정 대신 값이 있어야함 - if(map.subsInfo.length>0){ - form.setValue('candidateTitle', map.subsInfo) + if(subsInfo.length>0){ + form.setValue('candidateTitle', subsInfo) } -},[map.subsInfo, form]) +},[subsInfo, form]) + +//후보지 팝업 값 셋팅 +useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.data?.type === 'SET_SUBS_INFO') { + setSubsInfo(event.data.payload); // zustand로 값 설정 + } + }; + + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); +}, [setSubsInfo]); // 로그인 성공 시 모달 닫기 및 폼 업데이트 useEffect(() => { diff --git a/apps/dm/app/(base-layout)/pages/wrAbout/page.tsx b/apps/dm/app/(base-layout)/pages/wrAbout/page.tsx index 8d5bdd0ba..04dfa58df 100644 --- a/apps/dm/app/(base-layout)/pages/wrAbout/page.tsx +++ b/apps/dm/app/(base-layout)/pages/wrAbout/page.tsx @@ -53,14 +53,16 @@ export default function WrAboutPage() { }, observerOptions); // 각 카드 요소를 관찰 - cardRefs.current.forEach((card) => { + const cardsSnapshot = [...cardRefs.current]; + + cardsSnapshot.forEach((card) => { if (card) { observer.observe(card); } }); - + return () => { - cardRefs.current.forEach((card) => { + cardsSnapshot.forEach((card) => { if (card) { observer.unobserve(card); } diff --git a/apps/dm/app/(popup-layout)/map/popup/page.tsx b/apps/dm/app/(popup-layout)/map/popup/page.tsx index 908f2f09c..dc4e404ce 100644 --- a/apps/dm/app/(popup-layout)/map/popup/page.tsx +++ b/apps/dm/app/(popup-layout)/map/popup/page.tsx @@ -1,42 +1,46 @@ "use client"; import Gis from "@common/business_components/ui/map/Gis"; -import { useEffect, useRef, useState, useCallback } from "react"; +import { useEffect, useRef, useState } from "react"; import { mapService } from "../service"; import GisApp from "@common/business_components/ui/map/lib/gis.core"; import { SearchBar } from "@/components/ui/map/searchBar/searchBar"; -import { useDmStore } from "@/app/store"; +import SelectedFcltList from "./selectedFcltList"; +import type { SubsInfo } from "@/app/store/map/type"; -// export type SubsInfo={ -// ruseUtztnFcltNm: string; // 재이용시설명 -// ruseUtztnFcltCd: string; // 재이용시설 코드 -// } const Map = () => { - - //수요처 저장하는 함수 setSubsInfo - //setSubsInfo(subsInfo:SubsInfo[]):void - const { map,setSubsInfo } = useDmStore( - (state) => state - ); - //저장된 수요처 리스트 - const { subsInfo } = map; - const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 const gisMaps = useRef(null); const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 - const [checkedLayers, setCheckedLayers] = useState([]); - const [checkedLayersData, setCheckedLayersData] = useState([]); - const [selectDataPk, setSelectDataPk] = useState(""); - const [subs, setSubs] = useState([]); - - //적용 버튼 클릭 이벤트 + const [selectDataPk, setSelectDataPk] = useState>({}); + const [selectedFclts, setSelectedFclts] = useState([]); + + //적용 버튼 클릭 이벤트 const onApply = () => { - //setSubs([{'41110PB001R1':'수원제1,2 재이용1'},{'41110PB001R2':'수원제1,2 재이용2'}]); - console.log(subs) + if (selectedFclts.length > 3) { + alert("재이용시설 후보지는 최대 3개까지만 선택할 수 있습니다."); + return; + } + + const subsInfoList: SubsInfo[] = selectedFclts.map((fclt) => ({ + ruseUtztnFcltNm: fclt.ruseUtztnFcltNm, + ruseUtztnFcltCd: fclt.pbnstRuseFcltId, + })); + + window.opener.postMessage( + { + type: "SET_SUBS_INFO", + payload: subsInfoList, // 또는 필요한 데이터 + }, + "*" + ); + + window.close(); }; + //트리 레이어 데이터 조회 const fetchData = async () => { try { const response = await mapService.getLyrStngs(); @@ -46,76 +50,167 @@ const Map = () => { } }; - // 팝업 닫기 핸들러를 useCallback으로 메모이제이션 - const handlePopupClose = useCallback(() => { - setIsOpen(false); - setFeatureData(null); - }, []); + //하수처리시설코드로 재이용시설 목록 조회 + const reuseData = async (ruseFcltPk: any) => { + try { + const response = await mapService.getPbnstRuseFclts(ruseFcltPk); + const data = response.data || []; + setSelectedFclts((prev) => { + const newItems = data.filter( + (item: any) => + item && + Object.keys(item).length > 0 && + !prev.some((f) => f.pbnstRuseFcltId === item.pbnstRuseFcltId) + ); + console.log(newItems) + return [...prev, ...newItems]; + }); + + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; // 컴포넌트 마운트 시 1회 실행 useEffect(() => { fetchData(); }, []); - useEffect(() => { - console.log("Selected Data Pk: ", selectDataPk); - }, [selectDataPk]); + // 레이어 생성 후 초기화 처리 + const onCheckedLayersChange = () => { + //수요처 ID + const urlParams = new URLSearchParams(window.location.search); + const dmdInfoId = urlParams.get("dmdInfoId"); - // 체크된 레이어 정보를 받는 핸들러 - const handleCheckedLayersChange = ( - checkedIds: string[], - checkedNodesData: any[] - ) => { - setCheckedLayers(checkedIds); - setCheckedLayersData(checkedNodesData); - }; - - // SearchModal, GisContent 등에서 호출 - const onFeatureSelect = (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 인스턴스인지 확인 + //수요처 위치로 화면 이동 + const maps = gisMaps.current; + const layer = GisApp.Module.layer as any; + const layerNm = "tb_wr_ruse_fclt_dmd_co"; + const featureId = `${layerNm}.${dmdInfoId}`; let foundFeature: any = null; - if ( - data && - typeof data.getGeometry === "function" && - typeof data.setStyle === "function" - ) { - // 이미 Feature 인스턴스라면 그대로 사용 - foundFeature = data; - } else { - // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 - const featureId = data.fid || data.ftrIdn || data.id; - map - .getLayers() - .getArray() - .forEach((layer: any) => { + + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") == layerNm) { const source = layer.getSource?.(); if (source?.getFeatureById) { const feature = source.getFeatureById(String(featureId).trim()); - if (feature) foundFeature = feature; + if (feature) { + foundFeature = feature; + return; + } } - }); + } + }); + + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + + if (geometry) { + const extent = geometry.getExtent(); + + if (geometry.getType() === "Point") { + maps.getView().setCenter(geometry.getCoordinates()); + maps.getView().setZoom(11); + } else { + maps.getView().fit(extent, { + duration: 300, + padding: [30, 30, 30, 30], + maxZoom: 16, + }); + } + } + + layer.highLightFeature( + [foundFeature], + { + id: GisApp.hlLayerId, + zIndex: 10, + opacity: 1, + selectable: true, + }, + { width: 30 } + ); } - // 3. 피처 선택 + const formatNumber = (val: any) => { + const num = Number(val); + return isNaN(num) ? "-" : num.toLocaleString(); + }; + + // 재이용시설 오버레이 + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") === "tb_wr_bdst_ruse_fclt") { + const source = layer.getSource?.(); + if (source?.getFeatures) { + const features = source.getFeatures(); + + features.forEach((feature: any) => { + // 필요한 고유 ID 또는 속성 가져오기 (예: ftrIdn 또는 feature.getId()) + const featureId = + feature.getId() || feature.get("ftrIdn") || Math.random(); + + // HTML 또는 React 오버레이 컴포넌트 구성 + const overlayContent = ` +
+ +
+ ${feature.get("swtrmt_fclt_nm") || "-"} +
+ + + + + + + + + +
용량(m³/일)${formatNumber(feature.get("fclt_cpct"))}
+
+ `; + feature.set("html", overlayContent); + feature.set("overLayId", `overLay.${featureId}`); + }); + } + } + }); + }; + + // 검색해서 재이용시설 선택하면 해당 시설로 화면 이동 처리 + useEffect(() => { + if (!selectDataPk || Object.keys(selectDataPk).length === 0) return; + const maps = gisMaps.current; + let foundFeature: any = null; + const layerNm = "tb_wr_bdst_ruse_fclt"; + const featureId = `${layerNm}.${selectDataPk.bscFctCd}`; + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") == layerNm) { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) { + foundFeature = feature; + return; + } + } + } + }); + if (foundFeature) { const geometry = foundFeature.getGeometry(); if (geometry) { const extent = geometry.getExtent(); - map.getView().fit(extent, { + maps.getView().fit(extent, { duration: 300, padding: [50, 50, 50, 50], maxZoom: 16, @@ -123,21 +218,23 @@ const Map = () => { nearest: true, }); } - select.selectFeature(foundFeature, true); } else { - console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + console.warn("OpenLayers Feature를 찾을 수 없습니다:", selectDataPk); } + }, [selectDataPk]); - setFeatureData(data); - setIsOpen(true); - setIsSearchModalOpen(false); + // 재이용시설 클릭 시 선택 시설목록에 추가 + const onFeatureSelect = (data: any) => { + // 처리시설코드 + const swtrmtFcltCd = data.get("swtrmt_fclt_cd"); + reuseData(swtrmtFcltCd); }; return ( <> { - if (hasuPk !== "") { + setHasuPk={(hasuPk: any) => { + if (hasuPk) { setSelectDataPk(hasuPk); } }} @@ -150,7 +247,7 @@ const Map = () => { gisMaps={gisMaps} isMiniMap={false} searchView={undefined} - detailView={null} + detailView={undefined} onFeatureSelect={onFeatureSelect} setIsOpen={setIsOpen} setFeatureData={setFeatureData} @@ -158,11 +255,14 @@ const Map = () => { setIsSearchModalOpen={setIsSearchModalOpen} isOpen={isOpen} isSearchModalOpen={isSearchModalOpen} - onCheckedLayersChange={handleCheckedLayersChange} + onCheckedLayersChange={onCheckedLayersChange} imgVisble={false} - setData={setSubs} /> + setSelectedFclts(updatedList)} + /> ); }; diff --git a/apps/dm/app/(popup-layout)/map/popup/sdmInfoMap.tsx b/apps/dm/app/(popup-layout)/map/popup/sdmInfoMap.tsx deleted file mode 100644 index 84dc49119..000000000 --- a/apps/dm/app/(popup-layout)/map/popup/sdmInfoMap.tsx +++ /dev/null @@ -1,147 +0,0 @@ -"use client"; - -import Gis from "@common/business_components/ui/map/Gis"; -import { useEffect, useRef, useState, useCallback } from "react"; -import { mapService } from "../service"; -import GisApp from "@common/business_components/ui/map/lib/gis.core"; -import { SearchBar } from "@/components/ui/map/searchBar/searchBar"; - -const Map = () => { - const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 - const gisMaps = useRef(null); - const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 - const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 - const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 - const [checkedLayers, setCheckedLayers] = useState([]); - const [checkedLayersData, setCheckedLayersData] = useState([]); - const [selectDataPk, setSelectDataPk] = useState(""); - - const fetchData = async () => { - try { - const response = await mapService.getLyrStngs(); - setTreeData(response); // API 응답 저장 - } catch (error) { - console.error("데이터 불러오기 실패:", error); - } - }; - - // 팝업 닫기 핸들러를 useCallback으로 메모이제이션 - const handlePopupClose = useCallback(() => { - setIsOpen(false); - setFeatureData(null); - }, []); - - // 컴포넌트 마운트 시 1회 실행 - useEffect(() => { - fetchData(); - }, []); - - useEffect(() => { - console.log("Selected Data Pk: ", selectDataPk); - }, [selectDataPk]); - - // 체크된 레이어 정보를 받는 핸들러 - const handleCheckedLayersChange = ( - checkedIds: string[], - checkedNodesData: any[] - ) => { - setCheckedLayers(checkedIds); - setCheckedLayersData(checkedNodesData); - }; - - // SearchModal, GisContent 등에서 호출 - const onFeatureSelect = (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; - if ( - data && - typeof data.getGeometry === "function" && - typeof data.setStyle === "function" - ) { - // 이미 Feature 인스턴스라면 그대로 사용 - foundFeature = data; - } else { - // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 - const featureId = data.fid || data.ftrIdn || data.id; - 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, - }); - } - select.selectFeature(foundFeature, true); - } else { - console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); - } - - setFeatureData(data); - setIsOpen(true); - setIsSearchModalOpen(false); - }; - - return ( - <> - { - if (hasuPk !== "") { - setSelectDataPk(hasuPk); - } - }} - /> -
- -
- - ); -}; - -export default Map; diff --git a/apps/dm/app/(popup-layout)/map/popup/selectedFcltList.tsx b/apps/dm/app/(popup-layout)/map/popup/selectedFcltList.tsx new file mode 100644 index 000000000..bdf4d7e2c --- /dev/null +++ b/apps/dm/app/(popup-layout)/map/popup/selectedFcltList.tsx @@ -0,0 +1,81 @@ +import React from "react"; + +type FcltItem = { + pbnstRuseFcltId: string; + ruseUtztnFcltNm: string; + splyExtraCpct?: number; + ruseCpct?: number; + [key: string]: any; +}; + +export default function SelectedFcltList({ + data = [], + onRemove, +}: { + data: FcltItem[]; + onRemove: (updatedList: FcltItem[]) => void; +}) { + const handleDelete = (code: string) => { + const updatedList = data.filter((item) => item.pbnstRuseFcltId !== code); + onRemove(updatedList); + }; + + const TableContent = ( + <> + + + 시설명 + + 하수처리량 +
+ (㎥/일) + + + 재이용량 +
+ (㎥/일) + + 삭제 + + + + {data.map((item) => ( + + {item.ruseUtztnFcltNm} + + {Number(item.splyExtraCpct || 0).toLocaleString()} + + + {Number(item.ruseCpct || 0).toLocaleString()} + + + + + + ))} + + + ); + + if (data.length === 0) return null; + + return ( +
+

+ 선택된 시설 +

+
+ + {TableContent} +
+
+
+ ); +} diff --git a/apps/dm/app/(popup-layout)/map/popup/windowPotal.tsx b/apps/dm/app/(popup-layout)/map/popup/windowPotal.tsx deleted file mode 100644 index 38b8fc7db..000000000 --- a/apps/dm/app/(popup-layout)/map/popup/windowPotal.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { createPortal } from 'react-dom'; - -// 현재 문서의 스타일(CSS)을 새 창으로 복사하는 함수 -const copyStyles = (sourceDoc: Document, targetDoc: Document) => { - Array.from(sourceDoc.styleSheets).forEach(styleSheet => { - try { - if (styleSheet.cssRules) { //