From 05b4a4dae547541197c9b9162aab9c3e74ec63bf Mon Sep 17 00:00:00 2001 From: leedj312 Date: Tue, 2 Sep 2025 17:30:05 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=EB=AC=BC=EC=9E=AC=EC=9D=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EB=B9=BC=EA=B3=A0=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(map-layout)/map/mainMap/page.tsx | 217 +++++++++++++++--- .../(popUp-layout)/mapFeaturePopup/page.tsx | 2 +- apps/wr/app/store/common/code/basicCode.tsx | 1 + apps/wr/app/store/map/pbnstMap/index.tsx | 9 +- apps/wr/app/store/map/ruseMap/index.tsx | 79 +++++-- .../ruseFcltMng/fcltReg/rswtReg/index.tsx | 4 +- apps/wr/components/lib/func/mapUtil.ts | 116 ++++++++-- .../wrOverLay/WrOverLayRuse/WrOverLayRuse.tsx | 88 +++++++ .../WrOverLayTable/WrOverLayTable.tsx | 33 +++ .../components/ui/map/wrOverLay/wrOverLay.tsx | 50 ++++ 10 files changed, 526 insertions(+), 73 deletions(-) create mode 100644 apps/wr/components/ui/map/wrOverLay/WrOverLayRuse/WrOverLayRuse.tsx create mode 100644 apps/wr/components/ui/map/wrOverLay/WrOverLayTable/WrOverLayTable.tsx create mode 100644 apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index f75604d86..f31263202 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -15,9 +15,14 @@ import { getWrLayerNm, } from "@/components/lib/func/mapUtil"; import toast, { Toaster } from "react-hot-toast"; +import createOverlayManager from "@common/business_components/ui/map/GisOverlay"; +import { createRoot } from "react-dom/client"; +import WrOverLay from "@/components/ui/map/wrOverLay/wrOverLay"; +import type { comCodeState } from "@/app/store/common/code"; const Map = () => { - const { comMap } = useWrStore((state) => state); + + const {code, comMap ,rainMapDetile ,rswtMapDetile,getRuseMaplListReturn,pbnMapDetaile,ldrmntMapDetile,htdfMapDetile,coSttnMapDetail} = useWrStore((state) => state); const { isFeatured } = comMap; const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 const gisMaps = useRef(null); @@ -79,11 +84,10 @@ const Map = () => { // SearchModal, GisContent 등에서 호출 const onFeatureFocus = (data: any) => { - console.log('data',data) + console.log("data", data); }; - const onFeatureSelect = (data: any ,popUpClick?:boolean) => { - + const onFeatureSelect = (data: any, popUpClick?: boolean) => { const map = gisMaps.current; if (!map) { console.warn("지도가 초기화되지 않았습니다."); @@ -100,21 +104,22 @@ const Map = () => { // 2. data가 OpenLayers Feature 인스턴스인지 확인 let foundFeature: any = null; let featureId = data.fid; - - if(popUpClick){ + + if (popUpClick) { const layerName = getWrLayerNm(data); const featurePk = getWrFeaturePk(layerName ?? ""); // 재이용시설 if (featurePk) { - - const lastPk = layerName==='tb_wr_ruse_fclt_dmd_co'? data[featurePk].replaceAll("-", "") : data[featurePk] + const lastPk = + layerName === "tb_wr_ruse_fclt_dmd_co" + ? data[featurePk].replaceAll("-", "") + : data[featurePk]; featureId = `${layerName}.${lastPk}`; } else { console.warn("해당 피처는 선택된 레이어에서 찾을수 없습니다."); } - }else{ - featureId=getClickFeatureId(data) - + } else { + featureId = getClickFeatureId(data); } map .getLayers() @@ -143,9 +148,9 @@ const Map = () => { } select.selectFeature(foundFeature, true); } else { - // toast.error('위치정보를 찾을수 없습니다') + // toast.error('위치정보를 찾을수 없습니다') console.warn("OpenLayers Feature를 찾을 수 없습니다:"); - console.log("foundFeature :", foundFeature ); + console.log("foundFeature :", foundFeature); console.warn("featureId : ", featureId); console.warn("data : ", data); console.warn(": 끝"); @@ -157,14 +162,21 @@ const Map = () => { setIsOpen(true); setIsSearchModalOpen(false); - if (popUpClick && foundFeature&&!( - Object.keys(foundFeature?.getProperties()).length === 0 && - foundFeature.constructor === Object - )){ - const layerName = getWrLayerNm(data); - const isColumn = getFeatureCd(foundFeature.getProperties(), String(layerName)); - if(layerName&&isColumn) - featurePopUpOpen(layerName,isColumn,popupDetailRef) + if ( + popUpClick && + foundFeature && + !( + Object.keys(foundFeature?.getProperties()).length === 0 && + foundFeature.constructor === Object + ) + ) { + const layerName = getWrLayerNm(data); + const isColumn = getFeatureCd( + foundFeature.getProperties(), + String(layerName) + ); + if (layerName && isColumn) + featurePopUpOpen(layerName, isColumn, popupDetailRef); } }; @@ -178,14 +190,12 @@ const Map = () => { const bc = new BroadcastChannel("mapDetailPopUp"); bc.onmessage = (event) => { if (event.data.type === "SELECT_FEATURE") { - onFeatureSelect(event.data.feature,true); + onFeatureSelect(event.data.feature, true); closePopup(); } }; }, []); - - const renderDetailView = useCallback( (props: any) => { const map = gisMaps.current; @@ -202,11 +212,10 @@ const Map = () => { isFeature.constructor === Object ) ) { - const layerNm = getSelectedLayers(isFeature,gisMaps); - const safeProps = isFeature.getProperties(); + const layerNm = getSelectedLayers(isFeature, gisMaps); + const safeProps = isFeature.getProperties(); const featureCd = getFeatureCd(safeProps, String(layerNm)); - if(layerNm) - featurePopUpOpen(layerNm,featureCd,popupDetailRef) + if (layerNm) featurePopUpOpen(layerNm, featureCd, popupDetailRef); } } else { console.log(" data확인 필요"); @@ -239,6 +248,158 @@ const Map = () => { } }; + + const OVERLAY_MIN_ZOOM = 16; +const OVERLAY_COORD_PRECISION = 5; +const OVERLAY_MAX_PER_VIEW = 800; + +const overlayManagersRef = useRef([]); +const overlayActiveRef = useRef(false); +const moveEndDebounceRef = useRef(null); + +const overlayFormatter = (feature: any, props: any) => { + const propertise = props || {}; + const featureId = + feature.getId?.() ?? + propertise?.id ?? + propertise?.fid ?? + propertise?.ftrIdn ?? + ''; + if (!featureId) return null; + + const fcltTableId = featureId.split('.')[0]; + const fcltCd = getFeatureCd(props, fcltTableId); + + const div = document.createElement("div"); + const root = createRoot(div); + + // 비동기 처리만 따로 + (async () => { + let overLayData; + let title = ''; + + if (fcltTableId === 'tb_il_swage_prcs_fclt') { + overLayData = await pbnMapDetaile( fcltCd); + title = '하수처리시설'; + } else if (fcltTableId === 'tb_wr_bdst_ruse_fclt') { + overLayData = await getRuseMaplListReturn(fcltCd); + title = '하수재이용시설'; + + } else if (fcltTableId === 'tb_wr_rnwtr_utztn_fclt') { + overLayData = await rainMapDetile(fcltCd); + title = '빗물이용시설'; + } else if (fcltTableId === 'tb_wr_rswtr_fclt') { + overLayData = await rswtMapDetile(fcltCd); + title = '중수도시설'; + } else if (fcltTableId === 'tb_wr_htdfl_fclt') { + overLayData = await htdfMapDetile(fcltCd); + title = '온배수시설'; + } else if (fcltTableId === 'tb_wr_ruse_fclt_dmd_co') { + overLayData = await coSttnMapDetail(fcltCd); + title = '수요처'; + } else if (fcltTableId === 'tb_wr_tmpr_dmd_co') { + overLayData = await ldrmntMapDetile(fcltCd); + title = '잠재수요처'; + } + + root.render(} />); + })(); + + // 제거 시 React unmount + (div as any).__dispose = () => root.unmount(); + + return div; // <-- HTMLDivElement 동기 리턴 +}; + +const destroyAllManagers = () => { + overlayManagersRef.current.forEach(m => { try { m.destroy(); } catch {} }); + overlayManagersRef.current = []; + overlayActiveRef.current = false; +}; + +const mountOverlayManagers = () => { + const map = gisMaps.current; + if (!map) return; + + // 이전 것 정리 + destroyAllManagers(); + + // 체크된 벡터 레이어만 골라 매니저 생성 + const checkedSet = new Set(checkedLayers.map(String)); + const layers: any[] = map.getLayers().getArray(); + const targets = layers.filter((layer: any) => { + const src = layer.getSource?.(); + const isVector = src && typeof src.getFeatures === "function"; + const id = String(layer.get?.("lyrSn") ?? layer.get?.("id") ?? ""); + return isVector && layer.getVisible?.() && checkedSet.has(id); + }); + + targets.forEach((layer: any) => { + const mgr = createOverlayManager(map, layer, overlayFormatter, { + coordPrecision: OVERLAY_COORD_PRECISION, + maxOverlaysPerViewport: OVERLAY_MAX_PER_VIEW, + }); + mgr.mount(); // 리스너 1회 등록 + mgr.enable(); // 표시 + 1회 sync + overlayManagersRef.current.push(mgr); + }); + + overlayActiveRef.current = overlayManagersRef.current.length > 0; +}; + +useEffect(() => { + const map = gisMaps.current; + if (!map) return; + + const z = map.getView()?.getZoom?.() ?? 0; + if (z < OVERLAY_MIN_ZOOM) { + // 저줌에선 그냥 안 보이게만 + overlayManagersRef.current.forEach(m => { try { m.disable({ clear:true, delayed:true }); } catch {} }); + return; + } + mountOverlayManagers(); + + return () => { destroyAllManagers(); }; +}, [checkedLayers]); + + +useEffect(() => { + const map = gisMaps.current; + if (!map) return; + + const onMoveStart = () => { + // 숨기기만 (리스너/DOM 대량 제거 X) + overlayManagersRef.current.forEach(m => { try { m.disable({ clear:false }); } catch {} }); + if (moveEndDebounceRef.current) { clearTimeout(moveEndDebounceRef.current); moveEndDebounceRef.current = null; } + }; + + const onMoveEnd = () => { + const z = map.getView()?.getZoom?.() ?? 0; + const active = overlayActiveRef.current; + + if (moveEndDebounceRef.current) clearTimeout(moveEndDebounceRef.current); + moveEndDebounceRef.current = window.setTimeout(() => { + if (z < OVERLAY_MIN_ZOOM) { + if (active) overlayManagersRef.current.forEach(m => { try { m.disable({ clear:true, delayed:true }); } catch {} }); + return; + } + if (!active) { + mountOverlayManagers(); + } else { + // 다시 보이고 1회만 동기화 + overlayManagersRef.current.forEach(m => { try { m.enable(); m.syncOverlays(); } catch {} }); + } + }, 180) as unknown as number; + }; + + map.on("movestart", onMoveStart); + map.on("moveend", onMoveEnd); + return () => { + map.un("movestart", onMoveStart); + map.un("moveend", onMoveEnd); + if (moveEndDebounceRef.current) { clearTimeout(moveEndDebounceRef.current); moveEndDebounceRef.current = null; } + }; +}, [checkedLayers]); return (
{ + pbnMapDetaile(isFeatureCd).then((res) => { setIsDetail(res); }); } else if (layerNm === "tb_wr_rnwtr_utztn_fclt") { diff --git a/apps/wr/app/store/common/code/basicCode.tsx b/apps/wr/app/store/common/code/basicCode.tsx index 8af04c59f..5fc13c16d 100644 --- a/apps/wr/app/store/common/code/basicCode.tsx +++ b/apps/wr/app/store/common/code/basicCode.tsx @@ -211,6 +211,7 @@ export const rswtPrscCdMthdCd = [ }, { val: "소독(염소, UV 등)", text: "소독(염소, UV 등)" }, { val: "기타", text: "기타" }, + { val: "확인불가", text: "확인불가" }, ]; export const htdPowerMthdCd= [ diff --git a/apps/wr/app/store/map/pbnstMap/index.tsx b/apps/wr/app/store/map/pbnstMap/index.tsx index 4cd118caa..18f9abfe2 100644 --- a/apps/wr/app/store/map/pbnstMap/index.tsx +++ b/apps/wr/app/store/map/pbnstMap/index.tsx @@ -77,7 +77,6 @@ type pbnMapActions = { ) => void; pbnMapDetaile: ( featureCd: string, - data: string ) => Promise; }; @@ -117,11 +116,11 @@ export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => { console.log(e); } }, - pbnMapDetaile: async (featureCd: string, data: string) => { + pbnMapDetaile: async ( featureCd: string) => { try { const hasuRes = await postFectch( - `/prcsFclt/ruseInfo/1/20/A/0/0/1/SW/${featureCd}`, - [data], + `/prcsFclt/ruseInfo/1/1/A/0/0/1/SW/`, + [featureCd], fetchType.COMMON ); const filteredData = hasuRes.data.map((item: any) => { @@ -131,7 +130,7 @@ export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => { ruse: item.ruse === false ? "N" : "Y", }; }); - return filteredData; + return filteredData[0]; } catch (error) { console.log(error); throw error; // 호출부에서 catch 가능하도록 diff --git a/apps/wr/app/store/map/ruseMap/index.tsx b/apps/wr/app/store/map/ruseMap/index.tsx index 5e07a9b72..f68fb96b3 100644 --- a/apps/wr/app/store/map/ruseMap/index.tsx +++ b/apps/wr/app/store/map/ruseMap/index.tsx @@ -42,8 +42,8 @@ type ruseMapActions = { fclts: string[], fcltNm: string ) => void; - ruseMapDetail: (ruseFcltId: string) => Promise; getRuseMaplList: (bscFctCd: string) => void; + getRuseMaplListReturn: (bscFctCd: string) => Promise; }; export type ruseMapType = ruseMapState & ruseMapActions; @@ -106,21 +106,7 @@ export const ruseMapActionsExport: (set: any) => ruseMapActions = ( console.log(e); } }, - ruseMapDetail: async (ruseFcltId: string) => { - try { - let ruseDetail = await getFetch("wr", `/reuseFc/pubFc/${ruseFcltId}`); - if (ruseDetail.length >= 0) { - ruseDetail = ruseDetail.find( - (item: customPbnst) => item.pbnstRuseFcltId === ruseFcltId - ); - } - // 🔑 스토어 업데이트 대신 리턴 - return ruseDetail; - } catch (errorHasu) { - console.log(errorHasu); - throw errorHasu; // 호출부에서 catch 가능하도록 - } - }, + getRuseMaplList: async (bscFctCd: string) => { try { const ruseDetailList = await getFetch( @@ -204,5 +190,66 @@ export const ruseMapActionsExport: (set: any) => ruseMapActions = ( console.log(e); } }, + + getRuseMaplListReturn: async (bscFctCd: string) => { + try { + const ruseDetailList = await getFetch( + fetchType.WR, + `/reuseFc/pubFc/${bscFctCd}` + ); + const year = new Date().getFullYear(); + const data = [bscFctCd]; + const rusePrfList = await postFectch( + `/reuseFc/pubDetailList/${year}/0/1/10`, + data, + fetchType.WR + ); + const dataList = ruseDetailList.map((ruse) => { + const matchRuse = rusePrfList?.data.find( + (prf) => prf.pbnstRuseFcltId === ruse.pbnstRuseFcltId + ); + + + const usage = matchRuse.prfList.reduce( + (acc, cur) => { + acc.inUsage += + cur.wshwtr + + cur.coolUswtr + + cur.cleanUswtr + + cur.drwtUswtr + + cur.dltnUswtr + + cur.ithlRswtr + + cur.ithlEtcUswtr + + cur.cleanToilUswtr + + cur.resn; + acc.outUsage += + cur.lndwtr + + cur.hydUswtr + + cur.ugwtChr + + cur.farmUswtr + + cur.rvrKeepUswtr + + cur.indstUswtr; + acc.total = acc.inUsage + acc.outUsage; + + return acc; + }, + { total: 0, inUsage: 0, outUsage: 0 } + ); + + + return { + ...ruse, + prfList: matchRuse.prfList, + inUsage: usage?.inUsage, + outUsage: usage?.outUsage, + + }; + }); + return dataList + } catch (errorHasu) { + console.log(errorHasu); + throw errorHasu; // 호출부에서 catch 가능하도록 + } + } }; }; diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx index 02ea00b19..42e771292 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx @@ -18,8 +18,8 @@ const rswtrObjSchema = z.object({ filtrTrtmfctProcs: z.string({ required_error: validationMessages.required("주요처리공정") }).min(1, { message: validationMessages.required("주요처리공정") }), filtrTrtmfctUsgCd: z.string().default("").nullish(), rswtrUsqty: z.number().default(0), - altUswtrUtztnYn: z.enum(["Y", "N"]).nullish(), //안쓰는값 - altUswtrKndCd: z.string().default("").nullish(), //안쓰는값 + //altUswtrUtztnYn: z.enum(["Y", "N"]).nullish(), //안쓰는값 +// altUswtrKndCd: z.string().default("").nullish(), //안쓰는값 altUswtrUtztnQnt: z.number().default(0), //안쓰는값 altUswtrIdntyMthdCd: z.string().default("").nullish(), //안쓰는값 rgtr: z.string().nullish().default(null), diff --git a/apps/wr/components/lib/func/mapUtil.ts b/apps/wr/components/lib/func/mapUtil.ts index a23b48484..283b53a75 100644 --- a/apps/wr/components/lib/func/mapUtil.ts +++ b/apps/wr/components/lib/func/mapUtil.ts @@ -1,3 +1,5 @@ +import type { comCodeState } from "@/app/store/common/code"; + const wrLayerList = [ { text: "tb_il_swage_prcs_fclt", val: "bscFctCd" }, { text: "tb_wr_bdst_ruse_fclt", val: "pbnstRuseFcltId" }, @@ -26,8 +28,7 @@ const wrFeaturePK = [ // { text: "잠재수요처", val: "tb_wr_tmpr_dmd_co" }, export const getFeatureCd = (featurePropertise: any, layerNm: string) => { - - console.log('featurePropertise ', featurePropertise) + console.log("featurePropertise ", featurePropertise); const featureCd = layerNm === "tb_wr_bdst_ruse_fclt" ? featurePropertise?.swtrmt_fclt_cd @@ -39,32 +40,30 @@ export const getFeatureCd = (featurePropertise: any, layerNm: string) => { ].includes(layerNm) ? featurePropertise?.bdst_nm : layerNm === "tb_il_swage_prcs_fclt" - ? featurePropertise?.swtrmt_fclt_nm - : layerNm === "tb_wr_ruse_fclt_dmd_co"? - featurePropertise?.bdst_info_sn - : null; + ? featurePropertise?.swtrmt_fclt_cd + : layerNm === "tb_wr_ruse_fclt_dmd_co" + ? featurePropertise?.bdst_info_sn + : null; return featureCd; }; export const getWrLayerNm = (featurePropertise: any) => { const result = wrLayerList - .filter(item => item.val in featurePropertise) // 키 존재 여부 체크 - .map(item => item.text); - + .filter((item) => item.val in featurePropertise) // 키 존재 여부 체크 + .map((item) => item.text); return result[0]; }; - -export const getWrFeaturePk = (layerName :string) => { +export const getWrFeaturePk = (layerName: string) => { const result = wrFeaturePK - .filter(item => item.val === layerName) // 키 존재 여부 체크 - .map(item => item.text); + .filter((item) => item.val === layerName) // 키 존재 여부 체크 + .map((item) => item.text); return result[0]; }; -export const getSelectedLayers = (isFeature: any, gisMaps:any) => { +export const getSelectedLayers = (isFeature: any, gisMaps: any) => { const map = gisMaps.current; if (!map) { console.warn("지도가 초기화되지 않았습니다."); @@ -97,11 +96,11 @@ export const getSelectedLayers = (isFeature: any, gisMaps:any) => { return result; }; -export const getClickFeatureId = (data: any) => { - let featureId ='' +export const getClickFeatureId = (data: any) => { + let featureId = ""; const layerName = data.layerNm; if (layerName === "하수재이용시설") { - // 재이용시설 + // 재이용시설 featureId = `${layerName}.${data.searchPk}`; } else if ( //빗물이용시설, 중수도시설 @@ -124,7 +123,11 @@ export const getClickFeatureId = (data: any) => { return featureId; }; -export function featurePopUpOpen(layerNm:string,featureCd:string,popupDetailRef){ +export function featurePopUpOpen( + layerNm: string, + featureCd: string, + popupDetailRef +) { const url = `/mapFeaturePopup?layerNm=${encodeURIComponent(String(layerNm))}&featureCd=${encodeURIComponent(featureCd)} `; const width = String(layerNm) === "tb_wr_bdst_ruse_fclt" @@ -134,8 +137,8 @@ export function featurePopUpOpen(layerNm:string,featureCd:string,popupDetailRef) String(layerNm) === "tb_wr_bdst_ruse_fclt" ? Math.floor(window.innerHeight - 100) : String(layerNm) === "tb_wr_ruse_fclt_dmd_co" - ?Math.floor(window.innerHeight - 430) - : Math.floor(window.innerHeight - 300); // 150px 여백 확보; // calc(100% - 70px) + ? Math.floor(window.innerHeight - 430) + : Math.floor(window.innerHeight - 300); // 150px 여백 확보; // calc(100% - 70px) const left = (window.screen.availWidth - width) / 2; const top = (window.screen.availHeight - height) / 2; const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes `; @@ -145,4 +148,75 @@ export function featurePopUpOpen(layerNm:string,featureCd:string,popupDetailRef) "지도 시설 상세정보 팝업", popupOptions ); -} \ No newline at end of file +} + +export const getOverLayObj = (fclType: string, data: any,code?:Partial) => { + let newObj = {}; + let title = ""; + + + if (fclType === "하수재이용시설") { + newObj = { + "시설용량(톤/일)": data?.fcltCpct ??"", + "재이용률(%)": (data?.fcltCpct / data?.bscFctCpc)||0, + "장내용수(m³/년)": data?.inUsage ?? "", + "장외용수(m3/년)":data?.outUsage ?? "", + // 수요처: data?.reuseBuilding[0]?.rainyObj.pntfColArea ?? "", + }; + title = data?.bdstNm ?? ""; + } else if (fclType === "하수처리시설") { + newObj = { + "시설용량(톤/일)": data?.fctCpc ?? "", + 지역구분: data?.efflWqltStndrdNm ?? "", + 재이용여부: data?.rswtrYn ?? "", + "재이용시설 등록여부": data?.ruse ?? "", + }; + title = data?.bscFctNm ?? ""; + } else if (fclType === "빗물이용시설" ) { + newObj = { + 건물용도: code?.code?.bdstUsgCd.find((item) => item.val===data?.reuseBuilding[0].bdstUsgCd )?.text ?? "", + 설치완료일: data?.reuseBuilding[0]?.instlCmptnDt ?? "", + 처리시설유무: data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctYn ?? "", + "처리시설용량(m³/일)": + data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctCpct ?? "", + "저류조용량(m³)": data?.reuseBuilding[0]?.rainyObj.pntfColArea ?? "", + }; + title = data?.bdstNm ?? ""; + } else if (fclType === "중수도시설") { + newObj = { + 건물용도: code?.code?.bdstUsgCd.find((item) => item.val===data?.reuseBuilding[0].bdstUsgCd )?.text ?? "", + 설치완료일: data?.reuseBuilding[0]?.instlCmptnDt ?? "", + "주요 처리공정": code?.code?.rswtPrscCdMthdCd.find((item) => item.val===data?.reuseBuilding[0]?.rswtrObj.filtrTrtmfctProcs)?.text ?? "", + 원수종류: code?.code?.orgwtKndCd.find((item) => item.val === data?.reuseBuilding[0]?.rswtrObj.orgwtKndCd)?.text ?? "", + //이용용도: isCode.filtrTrtmfctUsgCd data?.reuseBuilding[0]?.rswtrObj.altUswtrKndCd ?? "", + }; + title = data?.bdstNm ?? ""; + } else if (fclType === "온배수시설") { + newObj = { + 미정: "", + // '설치완료일': data?.reuseBuilding[0]?.instlCmptnDt??'', + // '처리시설유무': data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctYn??'', + // '처리시설용량(m³/일)': data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctCpct??'', + // '저류조용량(m³)': data?.reuseBuilding[0]?.rainyObj.pntfColArea??'', + }; + title = data?.bdstNm ?? ""; + } else if (fclType === "수요처") { + newObj = { + '사업자등록번호': data?.dmdInfoId??'', + '주소': data?.daddr??'', + '전화번호': data?.telno??'', + }; + title = data?.dmdCoNm ?? ""; + + } else if (fclType === "잠재수요처") { + newObj = { + 공장구분: data?.fctrySe ?? "", + 설립구분: data?.fndnSe ?? "", + 업종명: data?.tpbizNm ?? "", + 생산폼목: data?.prdctnItem ?? "", + }; + title = data?.dmdCoNm ?? ""; + } + + return { title, newObj }; +}; diff --git a/apps/wr/components/ui/map/wrOverLay/WrOverLayRuse/WrOverLayRuse.tsx b/apps/wr/components/ui/map/wrOverLay/WrOverLayRuse/WrOverLayRuse.tsx new file mode 100644 index 000000000..62369c348 --- /dev/null +++ b/apps/wr/components/ui/map/wrOverLay/WrOverLayRuse/WrOverLayRuse.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useEffect, useState } from "react"; +import WrOverLayTable from "../WrOverLayTable/WrOverLayTable"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@common/components/ui"; +import { getOverLayObj } from "@/components/lib/func/mapUtil"; + + +type RuseTabData = { + data: Record; + tabData:{ + pbnstRuseFcltId: string; + ruseUtztnFcltNm: string; + } +} + + +export default function WrOverLayRuse({ overLayData }: { overLayData: any }) { + const [fcltTab, setFcltTab] = useState(""); + const [ruseData, setRuseData] = useState([]as RuseTabData[]); + // useEffect(() => { + // const detail = overLayData.find((list) => list.pbnstRuseFcltId === fcltTab); + // if (detail) setIsDetail(detail); + // }, [fcltTab, overLayData]); + + useEffect(() => { + + if (overLayData && overLayData[0]) { + setFcltTab(overLayData[0].pbnstRuseFcltId); + + const newRuseData= overLayData.map((ruse) =>{ + const isData = getOverLayObj( '하수재이용시설',ruse); + return { + data: isData.newObj, + tabData:{ + pbnstRuseFcltId: ruse.pbnstRuseFcltId, + ruseUtztnFcltNm: ruse.ruseUtztnFcltNm + } + } + }) + + setRuseData(newRuseData); + } + }, [overLayData]); + + if (!(overLayData && overLayData[0])) return; + return ( + <> +
+
+
+ + + {ruseData?.map((item, index) => { + return ( + + {item.tabData.ruseUtztnFcltNm} + + ); + })} + + + {ruseData?.map((item) => ( + +
+ +
+
+ ))} +
+
+ + ); +} diff --git a/apps/wr/components/ui/map/wrOverLay/WrOverLayTable/WrOverLayTable.tsx b/apps/wr/components/ui/map/wrOverLay/WrOverLayTable/WrOverLayTable.tsx new file mode 100644 index 000000000..db992a507 --- /dev/null +++ b/apps/wr/components/ui/map/wrOverLay/WrOverLayTable/WrOverLayTable.tsx @@ -0,0 +1,33 @@ +import { Table, TableBody, TableCell, TableRow } from "@common/components/ui"; + +export default function WrOverLayTable({overLayData}:{overLayData:any,}){ + + + + return( + + <> + + + {Object.entries(overLayData).map(([key, value]) => ( + + + {" "} + {key}{" "} + + + + {" "} + {String(value)}{" "} + + + ))} + +
+ + ) +} \ No newline at end of file diff --git a/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx b/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx new file mode 100644 index 000000000..bc8394a9a --- /dev/null +++ b/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx @@ -0,0 +1,50 @@ +"use client"; +import type { comCodeState } from "@/app/store/common/code"; +import { getOverLayObj } from "@/components/lib/func/mapUtil"; +import { Card } from "@common/business_components/ui/chart/lib/card"; +import { useEffect, useState } from "react"; +import WrOverLayTable from "./WrOverLayTable/WrOverLayTable"; +import WrOverLayRuse from "./WrOverLayRuse/WrOverLayRuse"; + +export default function WrOverLay({ + title, + data, + code, +}: { + title: string; + data: any; + code: Partial; +}) { + const [newOverLayData, setNewOverLayData] = useState>( + {} + ); + const [typeTitle, setTypeTitle] = useState(""); + + useEffect(() => { + if (title !== "하수재이용시설" && data) { + const overRayData = getOverLayObj(title, data, code); + setTypeTitle(overRayData?.title ?? ""); + setNewOverLayData(overRayData.newObj ?? {}); + } + }, [title, data]); + + return ( + <> + + {/* 파란 제목 바 */} + {title !== "하수재이용시설" ? ( +
+ {typeTitle + "(" + title + ")"} +
+ ) : ( + <> + )} + {title === "하수재이용시설" ? ( + + ) : ( + + )} +
+ + ); +} From 44c687d432e4aae09f3b594d9680eb46ff598d07 Mon Sep 17 00:00:00 2001 From: leedj312 Date: Tue, 2 Sep 2025 17:45:36 +0900 Subject: [PATCH 2/5] =?UTF-8?q?gis=20=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/index.ts | 3 +- .../src/ui/map/GisOverlay.js | 216 ++++++++++++++++++ .../src/ui/map/lib/util.js | 47 ++++ 3 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 packages/common/business_components/src/ui/map/GisOverlay.js diff --git a/packages/common/business_components/src/ui/index.ts b/packages/common/business_components/src/ui/index.ts index 8604f80d5..4686e3a99 100644 --- a/packages/common/business_components/src/ui/index.ts +++ b/packages/common/business_components/src/ui/index.ts @@ -23,4 +23,5 @@ export * from "./title"; export * from "./sdmInsertForm"; export * from "./popover_ui"; export * from "./date_month_picker"; -export * from "./date_picker_range" \ No newline at end of file +export * from "./date_picker_range" +export * from "./map/GisOverlay"; \ No newline at end of file diff --git a/packages/common/business_components/src/ui/map/GisOverlay.js b/packages/common/business_components/src/ui/map/GisOverlay.js new file mode 100644 index 000000000..5e4fb8948 --- /dev/null +++ b/packages/common/business_components/src/ui/map/GisOverlay.js @@ -0,0 +1,216 @@ +// Overlay manager (no React here) +import Overlay from 'ol/Overlay'; +import { getFeatureLabelCoordinate } from '../map/lib/util'; +import { buildCoordinateKeyFactory } from '../map/lib/util'; + +// ---- main factory ---- +export default function createOverlayManager(map, vectorLayer, overlayFormatter, options = {}) { + const coordPrecision = Math.max(0, Math.min(12, Number.isFinite(options.coordPrecision) ? options.coordPrecision : 6)); + const maxOverlaysPerViewport = Number.isFinite(options.maxOverlaysPerViewport) ? options.maxOverlaysPerViewport : Infinity; + const getCoordinateKey = buildCoordinateKeyFactory(coordPrecision); + + let currentFormatter = overlayFormatter; + const overlayMap = new Map(); // coordKey -> Overlay + const overlayElementMap = new Map(); // coordKey -> HTMLElement + const vectorSource = vectorLayer?.getSource?.(); + + const SYNC_MIN_INTERVAL = 120; + let lastSyncAt = 0; + + let isMounted = false; + let isActive = false; // when false, event handlers no-op + let rafId = null; + let idleClearId = null; + + + + const clearAllOverlaysAsync = (batch = 150) => { + const keys = Array.from(overlayMap.keys()); + if (!keys.length) return; + + // 재귀로 사용하는 이유 한번에 다지운는ㄴ것이아닌 150개 이상 더있을경우를 대비해서 다시 호출하기 위해(지도가 계속 멈춤) + const step = () => { + let iNum = 0; + const end = Math.min(iNum + batch, keys.length); + for (; iNum < end; iNum++) { + const k = keys[iNum]; + const ov = overlayMap.get(k); + try { if (ov) map.removeOverlay(ov); } catch {} + overlayMap.delete(k); + const el = overlayElementMap.get(k); + if (el) { try { el.remove?.(); } catch {} overlayElementMap.delete(k); } + } + if (iNum < keys.length) { + (typeof requestAnimationFrame === 'function' ? requestAnimationFrame : setTimeout)(step, 0); + } + }; + + step(); + }; + + const scheduleSync = () => { + if (!isActive) return; + const now = Date.now(); + const since = now - lastSyncAt; + const run = () => { + if (rafId) cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(() => { + try { syncOverlays(); } finally { lastSyncAt = Date.now(); } + }); + }; + if (since >= SYNC_MIN_INTERVAL) run(); else run(); + }; + + const onVisibleChange = () => { if (!isActive) return; scheduleSync(); }; + const onAddFeature = () => { if (!isActive) return; scheduleSync(); }; + const onChangeFeature = () => { if (!isActive) return; scheduleSync(); }; + const onRemoveFeature = () => { if (!isActive) return; scheduleSync(); }; + + const clearAllOverlays = () => { + overlayMap.forEach((ov) => { try { map.removeOverlay(ov); } catch {} }); + overlayMap.clear(); + overlayElementMap.clear(); + }; + + const ensureOverlay = (feature, properties) => { + const coord = getFeatureLabelCoordinate(feature); + if (!Array.isArray(coord)) return null; + const key = getCoordinateKey(coord); + + let el = overlayElementMap.get(key); + if (!el) { + el = currentFormatter(feature,properties); + overlayElementMap.set(key, el); + } + + let isOverlay = overlayMap.get(key); + if (!isOverlay) { + isOverlay = new Overlay({ + element: el, + positioning: 'bottom-center', + offset: [0, -18], + stopEvent: true, + }); + overlayMap.set(key, isOverlay); + map.addOverlay(isOverlay); + } + + isOverlay.setPosition(coord); + if (el && el.style) el.style.display = 'block'; + return isOverlay; + }; + + const syncOverlays = () => { + if (!vectorLayer?.getVisible?.()) { clearAllOverlays(); return; } + const view = map.getView?.(); + const size = map.getSize?.(); + const viewExtent = view && size ? view.calculateExtent(size) : null; + + // gather features in viewport only and group by coordinate + const groups = new Map(); // coordKey -> feature[] + const addFeature = (f) => { + const c = getFeatureLabelCoordinate(f); + if (!Array.isArray(c)) return; + const k = getCoordinateKey(c); + const arr = groups.get(k); + if (arr) arr.push(f); else groups.set(k, [f]); + }; + + if (vectorSource?.forEachFeatureInExtent && viewExtent) { + vectorSource.forEachFeatureInExtent(viewExtent, addFeature); + } else { + vectorSource?.forEachFeature(addFeature); + } + + // cap overlays per viewport with simple uniform sampling + const keys = Array.from(groups.keys()); + let selectedKeys; + if (!isFinite(maxOverlaysPerViewport) || keys.length <= maxOverlaysPerViewport) { + selectedKeys = keys; + } else { + const step = Math.ceil(keys.length / maxOverlaysPerViewport); + selectedKeys = []; + for (let i = 0; i < keys.length && selectedKeys.length < maxOverlaysPerViewport; i += step) { + selectedKeys.push(keys[i]); + } + } + + // ensure selected overlays + for (const k of selectedKeys) { + const featuresAtKey = groups.get(k); + const rep = featuresAtKey[0]; + const props = rep.getProperties?.() ?? {}; + if (featuresAtKey.length > 1) props.__groupCount = featuresAtKey.length; + ensureOverlay(rep, props); + } + + // remove non-selected overlays + const keep = new Set(selectedKeys); + for (const [k, ov] of overlayMap.entries()) { + if (!keep.has(k)) { + try { map.removeOverlay(ov); } catch {} + overlayMap.delete(k); + const el = overlayElementMap.get(k); + if (el) { try { el.remove?.(); } catch {} overlayElementMap.delete(k); } + } + } + }; + + const mount = () => { + if (isMounted) return; + vectorLayer?.on?.('change:visible', onVisibleChange); + vectorSource?.on?.('addfeature', onAddFeature); + vectorSource?.on?.('changefeature', onChangeFeature); + vectorSource?.on?.('removefeature', onRemoveFeature); + isMounted = true; + }; + + // Backward-compat: make dispose lightweight so it doesn't freeze the map + const dispose = () => { + isActive = false; // pause reactions + setVisible(false); // hide our labels only + const run = () => { clearAllOverlaysAsync(150); idleClearId = null; }; + if (typeof requestIdleCallback === 'function') idleClearId = requestIdleCallback(run, { timeout: 250 }); + else idleClearId = setTimeout(run, 180); + }; + + // Explicit lifecycle controls + const enable = () => { + isActive = true; + setVisible(true); + scheduleSync(); + }; + + const disable = ({ clear = false, delayed = true } = {}) => { + isActive = false; + setVisible(false); + if (clear) { + const run = () => { clearAllOverlaysAsync(150); idleClearId = null; }; + if (delayed && typeof requestIdleCallback === 'function') idleClearId = requestIdleCallback(run, { timeout: 250 }); + else idleClearId = setTimeout(run, delayed ? 180 : 0); + } + }; + + const destroy = () => { + if (!isMounted) return; + vectorLayer?.un?.('change:visible', onVisibleChange); + vectorSource?.un?.('addfeature', onAddFeature); + vectorSource?.un?.('changefeature', onChangeFeature); + vectorSource?.un?.('removefeature', onRemoveFeature); + isMounted = false; + if (rafId) cancelAnimationFrame(rafId), rafId = null; + if (idleClearId) { (typeof cancelIdleCallback === 'function' ? cancelIdleCallback : clearTimeout)(idleClearId); idleClearId = null; } + clearAllOverlaysAsync(200); + }; + + const setFormatter = (nextFormatter) => { currentFormatter = nextFormatter; syncOverlays(); }; + + const setVisible = (visible) => { + const display = visible ? 'block' : 'none'; + for (const el of overlayElementMap.values()) { + try { if (el && el.style) el.style.display = display; } catch {} + } + }; + + return { mount, dispose, enable, disable, destroy, setFormatter, syncOverlays, setVisible }; +} \ No newline at end of file diff --git a/packages/common/business_components/src/ui/map/lib/util.js b/packages/common/business_components/src/ui/map/lib/util.js index 9fe55869b..7de8c2fc4 100644 --- a/packages/common/business_components/src/ui/map/lib/util.js +++ b/packages/common/business_components/src/ui/map/lib/util.js @@ -594,3 +594,50 @@ export const isMobile = () => { return width < GLOBAL.MOBILE_SIZE; }; + + +export const getFeatureLabelCoordinate = (feature) => { + const geometry = feature?.getGeometry?.(); + if (!geometry || typeof geometry.getType !== 'function') return null; + + const type = geometry.getType(); + + // 1) 기본 타입 + if (type === 'Point') return geometry.getCoordinates(); + if (type === 'Polygon') return geometry.getInteriorPoint().getCoordinates(); + if (type === 'LineString') return geometry.getCoordinateAt(0.5); + + // 2) 멀티 타입들: 가능한 한 대표 좌표를 추출 + if (type === 'MultiPoint') { + const coords = geometry.getCoordinates?.(); + return (coords && coords[0]) ? coords[0] : null; + } + if (type === 'MultiLineString') { + try { return geometry.getLineString?.(0)?.getCoordinateAt?.(0.5) ?? null; } catch {} + } + if (type === 'MultiPolygon') { + try { return geometry.getInteriorPoints?.()?.getFirstCoordinate?.() ?? null; } catch {} + } + + // 3) 기타: extent 중심점 + try { + const extent = geometry.getExtent?.(); + if (extent) return [ + (extent[0] + extent[2]) / 2, + (extent[1] + extent[3]) / 2, + ]; + } catch {} + + return null; +}; + + // 수는 “좌표를 문자열 키로 변환하는 함수”를 + //좌표 정밀도(소수점 자리수)를 조절해서 근사한 좌표를 같은 그룹으로 취급할 수 있게 함. + export const buildCoordinateKeyFactory =(precision)=> { + const p = Math.max(0, Math.min(12, Number.isFinite(precision) ? precision : 6)); + return function getCoordinateKey(coordinate) { + const x = coordinate[0]; + const y = coordinate[1]; + return `${x.toFixed(p)}_${y.toFixed(p)}`; + }; + } \ No newline at end of file From 721dcbc634fff67e1135f391fb1b137da7adffaf Mon Sep 17 00:00:00 2001 From: leedj312 Date: Wed, 3 Sep 2025 14:34:34 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=EB=AC=BC=EC=9E=AC=EC=9D=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4=20?= =?UTF-8?q?=ED=8C=9D=EC=97=85=20=EC=88=98=EC=A0=95(=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8,=20=EC=B6=95=EC=86=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(map-layout)/map/mainMap/page.tsx | 78 +++++-------- apps/wr/app/store/map/rainMap/index.tsx | 2 - apps/wr/components/lib/func/mapUtil.ts | 70 ++++++------ .../components/ui/map/wrOverLay/wrOverLay.tsx | 104 +++++++++++++++--- apps/wr/package.json | 1 + 5 files changed, 152 insertions(+), 103 deletions(-) diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index f31263202..52955d457 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -18,7 +18,6 @@ import toast, { Toaster } from "react-hot-toast"; import createOverlayManager from "@common/business_components/ui/map/GisOverlay"; import { createRoot } from "react-dom/client"; import WrOverLay from "@/components/ui/map/wrOverLay/wrOverLay"; -import type { comCodeState } from "@/app/store/common/code"; const Map = () => { @@ -257,60 +256,41 @@ const overlayManagersRef = useRef([]); const overlayActiveRef = useRef(false); const moveEndDebounceRef = useRef(null); -const overlayFormatter = (feature: any, props: any) => { - const propertise = props || {}; +// 반환: Promise +const overlayFormatter = async (feature: any, props: any) => { + const p = props || {}; const featureId = - feature.getId?.() ?? - propertise?.id ?? - propertise?.fid ?? - propertise?.ftrIdn ?? - ''; + feature.getId?.() ?? p?.id ?? p?.fid ?? p?.ftrIdn ?? ''; if (!featureId) return null; - const fcltTableId = featureId.split('.')[0]; - const fcltCd = getFeatureCd(props, fcltTableId); + const tableId = String(featureId).split('.')[0]??''; + const fcltCd = getFeatureCd(props, tableId); + if (!fcltCd) return null; - const div = document.createElement("div"); + let data: any = null; + let title = ''; + + if (tableId === 'tb_il_swage_prcs_fclt') { data = await pbnMapDetaile(fcltCd); title = '하수처리시설'; } + else if (tableId === 'tb_wr_bdst_ruse_fclt') { data = await getRuseMaplListReturn(fcltCd); title = '하수재이용시설'; } + else if (tableId === 'tb_wr_rnwtr_utztn_fclt'){ data = await rainMapDetile(fcltCd); title = '빗물이용시설'; } + else if (tableId === 'tb_wr_rswtr_fclt') { data = await rswtMapDetile(fcltCd); title = '중수도시설'; } + else if (tableId === 'tb_wr_htdfl_fclt') { data = await htdfMapDetile(fcltCd); title = '온배수시설'; } + else if (tableId === 'tb_wr_ruse_fclt_dmd_co'){ data = await coSttnMapDetail(fcltCd); title = '수요처'; } + else if (tableId === 'tb_wr_tmpr_dmd_co') { data = await ldrmntMapDetile(fcltCd); title = '잠재수요처'; } + + const empty = + data == null || + (Array.isArray(data) && data.length === 0) || + (typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length === 0); + + if (empty) return null; + + const div = document.createElement('div'); const root = createRoot(div); - - // 비동기 처리만 따로 - (async () => { - let overLayData; - let title = ''; - - if (fcltTableId === 'tb_il_swage_prcs_fclt') { - overLayData = await pbnMapDetaile( fcltCd); - title = '하수처리시설'; - } else if (fcltTableId === 'tb_wr_bdst_ruse_fclt') { - overLayData = await getRuseMaplListReturn(fcltCd); - title = '하수재이용시설'; - - } else if (fcltTableId === 'tb_wr_rnwtr_utztn_fclt') { - overLayData = await rainMapDetile(fcltCd); - title = '빗물이용시설'; - } else if (fcltTableId === 'tb_wr_rswtr_fclt') { - overLayData = await rswtMapDetile(fcltCd); - title = '중수도시설'; - } else if (fcltTableId === 'tb_wr_htdfl_fclt') { - overLayData = await htdfMapDetile(fcltCd); - title = '온배수시설'; - } else if (fcltTableId === 'tb_wr_ruse_fclt_dmd_co') { - overLayData = await coSttnMapDetail(fcltCd); - title = '수요처'; - } else if (fcltTableId === 'tb_wr_tmpr_dmd_co') { - overLayData = await ldrmntMapDetile(fcltCd); - title = '잠재수요처'; - } - - root.render(} />); - })(); - - // 제거 시 React unmount - (div as any).__dispose = () => root.unmount(); - - return div; // <-- HTMLDivElement 동기 리턴 + (div as any).__dispose = () => { try { root.unmount(); } catch {} }; + root.render(); + return div; // ← 데이터 있을 때만 HTMLElement 반환 }; - const destroyAllManagers = () => { overlayManagersRef.current.forEach(m => { try { m.destroy(); } catch {} }); overlayManagersRef.current = []; diff --git a/apps/wr/app/store/map/rainMap/index.tsx b/apps/wr/app/store/map/rainMap/index.tsx index f80c27cc7..1d23e7991 100644 --- a/apps/wr/app/store/map/rainMap/index.tsx +++ b/apps/wr/app/store/map/rainMap/index.tsx @@ -114,8 +114,6 @@ export const rainMapActionsExport: (set: any) => rainMapActions = ( }, rainMapDetile: async (featureCd: string) => { try { - console.log('featureCd ', featureCd) - const rainDetail = await getFetch(fetchType.WR, `/reuseFc/rainyInfoList/1/3/0/${featureCd}` ) diff --git a/apps/wr/components/lib/func/mapUtil.ts b/apps/wr/components/lib/func/mapUtil.ts index 283b53a75..925961b88 100644 --- a/apps/wr/components/lib/func/mapUtil.ts +++ b/apps/wr/components/lib/func/mapUtil.ts @@ -1,4 +1,4 @@ -import type { comCodeState } from "@/app/store/common/code"; +import { formatCommaNumber, formatPhoneNumber } from "./commom"; const wrLayerList = [ { text: "tb_il_swage_prcs_fclt", val: "bscFctCd" }, @@ -28,7 +28,6 @@ const wrFeaturePK = [ // { text: "잠재수요처", val: "tb_wr_tmpr_dmd_co" }, export const getFeatureCd = (featurePropertise: any, layerNm: string) => { - console.log("featurePropertise ", featurePropertise); const featureCd = layerNm === "tb_wr_bdst_ruse_fclt" ? featurePropertise?.swtrmt_fclt_cd @@ -150,72 +149,73 @@ export function featurePopUpOpen( ); } -export const getOverLayObj = (fclType: string, data: any,code?:Partial) => { +export const getOverLayObj = (fclType: string, data: any,code?:any) => { let newObj = {}; let title = ""; + + if (fclType === "하수재이용시설") { newObj = { - "시설용량(톤/일)": data?.fcltCpct ??"", + "시설용량(톤/일)": formatCommaNumber(data?.fcltCpct) ??'확인불가', "재이용률(%)": (data?.fcltCpct / data?.bscFctCpc)||0, - "장내용수(m³/년)": data?.inUsage ?? "", - "장외용수(m3/년)":data?.outUsage ?? "", - // 수요처: data?.reuseBuilding[0]?.rainyObj.pntfColArea ?? "", + "장내용수(m³/년)": formatCommaNumber(data?.inUsage) ?? '확인불가', + "장외용수(m3/년)":formatCommaNumber(data?.outUsage) ?? '확인불가', + // 수요처: data?.reuseBuilding[0]?.rainyObj.pntfColArea ?? '확인불가', }; - title = data?.bdstNm ?? ""; + title = data?.bdstNm ?? '확인불가'; } else if (fclType === "하수처리시설") { newObj = { - "시설용량(톤/일)": data?.fctCpc ?? "", - 지역구분: data?.efflWqltStndrdNm ?? "", - 재이용여부: data?.rswtrYn ?? "", - "재이용시설 등록여부": data?.ruse ?? "", + "시설용량(톤/일)": formatCommaNumber(data?.fctCpc) ?? '확인불가', + 지역구분: data?.efflWqltStndrdNm ?? '확인불가', + 재이용여부: data?.rswtrYn ?? '확인불가', + "재이용시설 등록여부": data?.ruse ?? '확인불가', }; - title = data?.bscFctNm ?? ""; + title = data?.bscFctNm ?? '확인불가'; } else if (fclType === "빗물이용시설" ) { newObj = { - 건물용도: code?.code?.bdstUsgCd.find((item) => item.val===data?.reuseBuilding[0].bdstUsgCd )?.text ?? "", - 설치완료일: data?.reuseBuilding[0]?.instlCmptnDt ?? "", - 처리시설유무: data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctYn ?? "", - "처리시설용량(m³/일)": - data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctCpct ?? "", - "저류조용량(m³)": data?.reuseBuilding[0]?.rainyObj.pntfColArea ?? "", + 건물용도: code?.bdstUsgCd.find((item) => item.val=== String(data?.reuseBuilding[0].bdstUsgCd) )?.text ?? '확인불가', + 설치완료일: data?.reuseBuilding[0]?.instlCmptnDt ?? '확인불가', + 처리시설유무: data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctYn ?? '확인불가', + "처리시설용량(m³/일)": formatCommaNumber(data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctCpct) ?? '확인불가', + "저류조용량(m³)": formatCommaNumber(data?.reuseBuilding[0]?.rainyObj.pntfColArea) ?? '확인불가', }; - title = data?.bdstNm ?? ""; + title = data?.bdstNm ?? '확인불가'; } else if (fclType === "중수도시설") { newObj = { - 건물용도: code?.code?.bdstUsgCd.find((item) => item.val===data?.reuseBuilding[0].bdstUsgCd )?.text ?? "", - 설치완료일: data?.reuseBuilding[0]?.instlCmptnDt ?? "", - "주요 처리공정": code?.code?.rswtPrscCdMthdCd.find((item) => item.val===data?.reuseBuilding[0]?.rswtrObj.filtrTrtmfctProcs)?.text ?? "", - 원수종류: code?.code?.orgwtKndCd.find((item) => item.val === data?.reuseBuilding[0]?.rswtrObj.orgwtKndCd)?.text ?? "", - //이용용도: isCode.filtrTrtmfctUsgCd data?.reuseBuilding[0]?.rswtrObj.altUswtrKndCd ?? "", + 건물용도: code?.bdstUsgCd.find((item) => item.val===String(data?.reuseBuilding[0].bdstUsgCd) )?.text ?? '확인불가', + 설치완료일: data?.reuseBuilding[0]?.instlCmptnDt ?? '확인불가', + "주요 처리공정": code?.rswtPrscCdMthdCd.find((item) => item.val===data?.reuseBuilding[0]?.rswtrObj.filtrTrtmfctProcs)?.text ?? '확인불가', + 원수종류: code?.orgwtKndCd.find((item) => item.val === data?.reuseBuilding[0]?.rswtrObj.orgwtKndCd)?.text ?? '확인불가', + //이용용도: isCode.filtrTrtmfctUsgCd data?.reuseBuilding[0]?.rswtrObj.altUswtrKndCd ?? '확인불가', }; - title = data?.bdstNm ?? ""; + title = data?.bdstNm ?? '확인불가'; } else if (fclType === "온배수시설") { newObj = { - 미정: "", + 미정: '확인불가', // '설치완료일': data?.reuseBuilding[0]?.instlCmptnDt??'', // '처리시설유무': data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctYn??'', // '처리시설용량(m³/일)': data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctCpct??'', // '저류조용량(m³)': data?.reuseBuilding[0]?.rainyObj.pntfColArea??'', }; - title = data?.bdstNm ?? ""; + title = data?.bdstNm ?? '확인불가'; } else if (fclType === "수요처") { newObj = { '사업자등록번호': data?.dmdInfoId??'', '주소': data?.daddr??'', - '전화번호': data?.telno??'', + '전화번호': formatPhoneNumber(data?.telno)??'', }; - title = data?.dmdCoNm ?? ""; + title = data?.dmdCoNm ?? '확인불가'; } else if (fclType === "잠재수요처") { newObj = { - 공장구분: data?.fctrySe ?? "", - 설립구분: data?.fndnSe ?? "", - 업종명: data?.tpbizNm ?? "", - 생산폼목: data?.prdctnItem ?? "", + 공장구분: data?.fctrySe ?? '확인불가', + 설립구분: data?.fndnSe ?? '확인불가', + 업종명: data?.tpbizNm ?? '확인불가', + 생산폼목: data?.prdctnItem ?? '확인불가', }; - title = data?.dmdCoNm ?? ""; + title = data?.dmdCoNm ?? '확인불가'; } return { title, newObj }; diff --git a/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx b/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx index bc8394a9a..225b2ff4c 100644 --- a/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx +++ b/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx @@ -1,10 +1,10 @@ "use client"; -import type { comCodeState } from "@/app/store/common/code"; import { getOverLayObj } from "@/components/lib/func/mapUtil"; import { Card } from "@common/business_components/ui/chart/lib/card"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import WrOverLayTable from "./WrOverLayTable/WrOverLayTable"; import WrOverLayRuse from "./WrOverLayRuse/WrOverLayRuse"; +import Draggable from "react-draggable"; export default function WrOverLay({ title, @@ -13,12 +13,18 @@ export default function WrOverLay({ }: { title: string; data: any; - code: Partial; + code: any; }) { const [newOverLayData, setNewOverLayData] = useState>( {} ); const [typeTitle, setTypeTitle] = useState(""); + const [pos, setPos] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); + + const [open, setOpen] = useState(true); + const bubbleLabel = title?.[0] ?? "•"; + + const dragRef = useRef(null); useEffect(() => { if (title !== "하수재이용시설" && data) { @@ -30,21 +36,85 @@ export default function WrOverLay({ return ( <> - - {/* 파란 제목 바 */} - {title !== "하수재이용시설" ? ( -
- {typeTitle + "(" + title + ")"} + {open ? ( + setPos({ x: data.x, y: data.y })} + > +
+ + {/* 헤더: 제목 바 + 우측 정렬 버튼 (절대배치 제거, 타이틀 중앙 정렬 유지) */} +
+
+ {/* 좌측 스페이서: 우측 버튼과 동일한 너비를 차지하도록 동일 버튼을 투명하게 복제 */} +
+
+ + + +
+
+ + {/* 중앙 타이틀 */} +
+ {typeTitle + "(" + title + ")"} +
+ + {/* 우측 실제 버튼 영역 */} +
+ + + +
+
+
+ + {/* 본문 */} + {title === "하수재이용시설" ? ( + + ) : ( + + )} +
- ) : ( - <> - )} - {title === "하수재이용시설" ? ( - - ) : ( - - )} - +
+ ) : ( + + )} ); } diff --git a/apps/wr/package.json b/apps/wr/package.json index 28438b1d8..39f7c2274 100644 --- a/apps/wr/package.json +++ b/apps/wr/package.json @@ -25,6 +25,7 @@ "next": "15.1.7", "ol": "^7.3.0", "ol-ext": "^4.0.11", + "react-draggable": "^4.5.0", "react-hook-form": "^7.54.2", "react-hot-toast": "^2.5.2", "react-i18next": "^15.6.0", From 4dec379846748aa7c2460a2ad4e7dc68638f4efe Mon Sep 17 00:00:00 2001 From: leedj312 Date: Wed, 3 Sep 2025 14:38:56 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20div=20=EB=A5=BC=E3=84=B9=20promise?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EC=9D=84=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/map/GisOverlay.js | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/packages/common/business_components/src/ui/map/GisOverlay.js b/packages/common/business_components/src/ui/map/GisOverlay.js index 5e4fb8948..ca22cf9c9 100644 --- a/packages/common/business_components/src/ui/map/GisOverlay.js +++ b/packages/common/business_components/src/ui/map/GisOverlay.js @@ -76,28 +76,54 @@ export default function createOverlayManager(map, vectorLayer, overlayFormatter, const coord = getFeatureLabelCoordinate(feature); if (!Array.isArray(coord)) return null; const key = getCoordinateKey(coord); - + let el = overlayElementMap.get(key); if (!el) { - el = currentFormatter(feature,properties); - overlayElementMap.set(key, el); + if (!currentFormatter || typeof currentFormatter !== 'function') return null; + + const maybeEl = currentFormatter(feature, properties); + + // 1) 동기 HTMLElement + if (maybeEl && typeof maybeEl === 'object' && (maybeEl.nodeType === 1 || maybeEl.tagName)) { + el = maybeEl; + overlayElementMap.set(key, el); + } + // 2) 비동기 Promise + else if (maybeEl && typeof maybeEl.then === 'function') { + maybeEl.then((resolvedEl) => { + if (!resolvedEl || !(resolvedEl.nodeType === 1 || resolvedEl.tagName)) return; + // 이미 다른 사이클에서 만들어졌으면 덮어쓰지 않음 + if (!overlayElementMap.has(key)) overlayElementMap.set(key, resolvedEl); + + let ov = overlayMap.get(key); + if (!ov) { + ov = new Overlay({ element: resolvedEl, positioning: 'bottom-center', offset: [0, -18], stopEvent: true }); + overlayMap.set(key, ov); + try { map.addOverlay(ov); } catch {} + } else { + try { ov.setElement(resolvedEl); } catch {} + } + ov.setPosition(coord); + if (resolvedEl.style) resolvedEl.style.display = 'block'; + }).catch(() => {}); + return null; // 지금은 안 만듦 + } + // 3) 그 외(null/undefined 등) → 안 만듦 + else { + return null; + } } - - let isOverlay = overlayMap.get(key); - if (!isOverlay) { - isOverlay = new Overlay({ - element: el, - positioning: 'bottom-center', - offset: [0, -18], - stopEvent: true, - }); - overlayMap.set(key, isOverlay); - map.addOverlay(isOverlay); + + // 여기까지 왔으면 el은 HTMLElement + let ov = overlayMap.get(key); + if (!ov) { + ov = new Overlay({ element: el, positioning: 'bottom-center', offset: [0, -18], stopEvent: true }); + overlayMap.set(key, ov); + map.addOverlay(ov); } - - isOverlay.setPosition(coord); - if (el && el.style) el.style.display = 'block'; - return isOverlay; + ov.setPosition(coord); + if (el.style) el.style.display = 'block'; + return ov; }; const syncOverlays = () => { From a03359540a57d48b8bc1af7ca8ccc2a232e2d12a Mon Sep 17 00:00:00 2001 From: leedj312 Date: Wed, 3 Sep 2025 16:27:58 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=EC=A3=BC=EC=86=8C=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=ED=8A=B9=EC=88=98=EB=AC=B8=EC=9E=90,=20sql=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EB=B0=A9=EC=A7=80=20=ED=95=A8=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/search_input_address/findAddress.ts | 38 ++++++++++++ .../src/ui/search_input_address/index.tsx | 59 +++++++++++-------- 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/packages/common/business_components/src/ui/search_input_address/findAddress.ts b/packages/common/business_components/src/ui/search_input_address/findAddress.ts index 985d20465..c458b80c4 100644 --- a/packages/common/business_components/src/ui/search_input_address/findAddress.ts +++ b/packages/common/business_components/src/ui/search_input_address/findAddress.ts @@ -87,3 +87,41 @@ export const findAddress = async (search: string) => { } + +export const searchFilter=(search:string)=>{ + + + if(search.length >0){ + //특수문자 제거 + const expText = /[%=><]/ ; + if(expText.test(search) == true){ + alert("특수문자를 입력 할수 없습니다.") ; + search = search.split(expText).join(""); + return false; + } + + //특정문자열(sql예약어의 앞뒤공백포함) 제거 + const sqlArray = [ + //sql 예약어 + "OR", "SELECT", "INSERT", "DELETE", "UPDATE", "CREATE", "DROP", "EXEC", + "UNION", "FETCH", "DECLARE", "TRUNCATE" + ] + + let regex; + for(let i=0; i 선택된 주소 데이터 반환 */ export default function SearchInputAddress({ - initValue , + initValue, setRtnData = () => {}, - readonly=false + readonly = false, }: thisProps) { - const [isOpen, setIsOpen] = useState(false); const [textValue, setTextValue] = useState(initValue || ""); const [searchList, setSearchList] = useState([]); - - useEffect(()=>{ - - if(initValue !== textValue && initValue !== undefined && initValue !== null && initValue !== ''){ - setTextValue(initValue) - } - - },[initValue]) + useEffect(() => { + if ( + initValue !== textValue && + initValue !== undefined && + initValue !== null && + initValue !== "" + ) { + setTextValue(initValue); + } + }, [initValue]); const wrapperRef = useRef(null); + const handleInputChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - setTextValue(value); - findAddress(value).then((data) => { - setSearchList(data); - }) + + if (searchFilter(value)) { + setTextValue(value); + findAddress(value).then((data) => { + setSearchList(data); + }); + }else{ + setTextValue(""); + setSearchList([]); + } if (value) { setIsOpen(true); } else { setIsOpen(false); } }, - [setSearchList] + [setSearchList], ); const handleSuggestionClick = useCallback( (roadAddrPart1: string) => { setTextValue(roadAddrPart1); setIsOpen(false); - setRtnData(searchList?.find((item) => item.roadAddrPart1 === roadAddrPart1) || ({} as addressType)); + setRtnData( + searchList?.find((item) => item.roadAddrPart1 === roadAddrPart1) || + ({} as addressType), + ); }, - [searchList, setRtnData] + [searchList, setRtnData], ); useEffect(() => { @@ -80,18 +91,18 @@ export default function SearchInputAddress({ value={textValue} onChange={handleInputChange} /* onFocus={() => setIsOpen(true)} */ - className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + className="h-[3.2rem] w-full rounded-md border border-gray-300 pl-3 !text-xl text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder={`주소를 입력하세요`} readOnly={readonly} /> {isOpen && ( -
+
{searchList && searchList.length > 0 ? ( searchList.map((item: addressType, index: number) => (
handleSuggestionClick(item.roadAddrPart1)} role="button" tabIndex={0} @@ -106,7 +117,7 @@ export default function SearchInputAddress({
)) ) : ( -
+
검색 결과가 없습니다.
)}