diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index 3fabc529d..5acc41863 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -6,6 +6,7 @@ const CLAIM_NAME = "Authorization"; const AUTH_USER_ID = "token-id"; const serverApiUrl = CS_URL; const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; +const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; export function middleware(req: NextRequest) { const pathname = req.nextUrl.pathname; @@ -55,9 +56,17 @@ export function middleware(req: NextRequest) { return NextResponse.rewrite(new URL(newUrl)); } + if (pathname.startsWith("/sewerage-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + return NextResponse.next(); } export const config = { - matcher: ["/server/:path*", "/attach-service/:path*", "/geoserver/:path*"], + matcher: ["/server/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*"], }; diff --git a/packages/common/business_components/package.json b/packages/common/business_components/package.json index e7aae9fc5..da6f8c344 100644 --- a/packages/common/business_components/package.json +++ b/packages/common/business_components/package.json @@ -35,6 +35,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^3.6.0", + "libphonenumber-js": "^1.12.9", "lucide-react": "^0.474.0", "path": "^0.12.7", "react-day-picker": "^8.10.1", diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 5ac98f27f..6d46e77aa 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -1,6 +1,5 @@ import { Search } from 'lucide-react'; import OlMap from 'ol/Map'; -import Overlay from 'ol/Overlay'; import View from 'ol/View'; import { defaults as defaultControls, OverviewMap } from 'ol/control'; import { defaults } from 'ol/interaction'; @@ -8,7 +7,6 @@ import * as olProj from 'ol/proj'; import { register } from 'ol/proj/proj4'; import proj4 from 'proj4'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { createRoot } from 'react-dom/client'; import GisLonLat from './GisLonLat'; import GisMapToggle from './GisMapToggle'; import GisSelect from './GisSelect'; @@ -16,27 +14,11 @@ import GisToolBar from './GisToolBar'; import GisTree from './GisTree'; import './lib/css/gis.common.css'; import GisApp from './lib/gis.core'; -// import FacilityInfoPopup from './FacilityInfoPopup'; export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); export const useGisContext = () => useContext(GisContext); -function Gis({ - isOpen, - gisMaps, - isSearchModalOpen, - treeData = [], - isSearch = false, - isMiniMap = false, - setIsOpen, - setFeatureData, - featureData, - searchView: SearchView, - detailView: DetailView, - onFeatureSelect, - setIsSearchModalOpen, - onCheckedLayersChange -}) { +function Gis({ isOpen, gisMaps, isSearchModalOpen, treeData = [], isSearch = false, isMiniMap = false, setIsOpen, setFeatureData, featureData, searchView: SearchView, detailView: DetailView, onFeatureSelect, setIsSearchModalOpen }) { const mapRef = useRef(); const [mapObject, setMapObject] = useState({}); const [tabVisible, setTabVisible] = useState(true); @@ -46,21 +28,13 @@ function Gis({ const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []); const mapInstanceRef = useRef(null); const checkedLayerIds = useRef([]); - - // 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가 - // const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]); - // const facilityOverlaysRef = useRef([]); - // 체크된 레이어 상태 관리 - const [checkedLayers, setCheckedLayers] = useState([]); - const [checkedLayersData, setCheckedLayersData] = useState([]); - let gisMap = null; const closePopup = () => { setIsOpen(false); // 팝업 닫기 setFeatureData(null) }; - // Feature Click Event + // SearchModal, GisContent 등에서 호출 const handleFeatureSelect = useCallback((data) => { gisMap = mapInstanceRef.current; @@ -115,10 +89,8 @@ function Gis({ setFeatureData(data); setIsOpen(true); setIsSearchModalOpen(false); - onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출 }, [mapInstanceRef]); - // 지도 생성 및 초기화 useEffect(() => { if (!mapRef.current) return; if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 @@ -132,7 +104,7 @@ function Gis({ ]); register(proj4); - // OpenLayers 지도 객체 생성 + //맵 생성 const map = new OlMap({ layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']], renderer: 'webgl', @@ -149,7 +121,7 @@ function Gis({ projection: GisApp.TO_PROJ, zoom: gisCore.defaults.zoom, center: olProj.transform( - [126.6757, 37.5585], + [127.4875, 37.4916], GisApp.FROM_PROJ, GisApp.TO_PROJ ), @@ -160,7 +132,7 @@ function Gis({ gisMaps.current = map - // 미니맵 추가 + //미니맵 if (isMiniMap) { const overviewMap = new OverviewMap({ layers: [GisApp.Module.core.baseMiniMapLayers['base']], @@ -175,7 +147,6 @@ function Gis({ map.addControl(overviewMap); } - // 스케일 컨트롤 및 베이스 레이어 설정 gisCore.__proto__.map = map; gisCore.scaleControl(); gisCore.changeBaseLayer('base'); @@ -183,20 +154,16 @@ function Gis({ mapInstanceRef.current = map; // 생성 완료 플래그 설정 }, []); - // treeData 변경 시 레이어 코드 생성 useEffect(() => { if (!treeData || treeData.length === 0) return; - // 기존 오버레이 및 레이어 모두 삭제 gisCore.deleteAllOverlay(); gisCore.deleteAllLayer(); - // 트리 데이터를 레이어 코드 객체로 변환 const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn'); - GisApp.layerCode = layerCode; // 전역 레이어 코드 설정 + GisApp.layerCode = layerCode; //setTreeData(data); - // 편집 가능한 레이어 목록 생성 const layerItems = getLayerItems(layerCode); //setEditLayerItems(prev => [...layerItems]) }, [treeData]); @@ -226,159 +193,31 @@ function Gis({ } } - // // 데이터 로드 및 팝업 생성 로직 - // useEffect(() => { - // if (!mapInstanceRef.current) return; // 맵이 생성되지 않았으면 실행 안 함 - - // // 1. 팝업 데이터 가져오기 (feature.js의 getData와 유사) - // const fetchPopupData = async () => { - // // 실제 API 호출 로직으로 대체해야 합니다. - // // 예시: const response = await yourApiService.getFcltyWtCnt(); - // // setFacilityInfoPopupData(response.data); - - // // 임시 목업 데이터 - // const mockData = [ - // { ftrIdn: 1, ftrCde: 'SA206', bplcNm: '처리장A', fcltyUseYn: 'Y', x: 127.005, y: 37.282, aCnt: 10, cCnt: 5, eCnt: 3, mCnt: 8, iCnt: 2 }, - // { ftrIdn: 2, ftrCde: 'SA112', bplcNm: '처리장B', fcltyUseYn: 'N', x: 127.010, y: 37.285, aCnt: 8, cCnt: 4, eCnt: 2, mCnt: 6, iCnt: 1 } - // ]; - // setFacilityInfoPopupData(mockData); - // }; - - // fetchPopupData(); - - // }, [mapInstanceRef.current]); // 맵 인스턴스가 생성된 후 한 번만 실행 - - // // 데이터가 변경되면 오버레이를 생성/업데이트하는 로직 - // useEffect(() => { - // const map = mapInstanceRef.current; - // if (!map || facilityInfoPopupData.length === 0) return; - - // // 기존 오버레이 정리 - // facilityOverlaysRef.current.forEach(overlay => map.removeOverlay(overlay)); - // facilityOverlaysRef.current = []; - - // facilityInfoPopupData.forEach(data => { - // const popupId = `${data.ftrIdn}_${data.ftrCde}`; - - // // 팝업을 담을 div 엘리먼트 생성 - // const popupElement = document.createElement('div'); - // popupElement.id = popupId; - - // // OpenLayers Overlay 생성 - // const position = olProj.transform([Number(data.x), Number(data.y)], GisApp.FROM_PROJ, GisApp.TO_PROJ); - // const overlay = new Overlay({ - // element: popupElement, - // position: position, - // offset: [-75, -210], // 팝업 너비의 절반, 높이만큼 오프셋 조정 - // positioning: 'bottom-center', - // className: 'facility-popup-overlay' // 필요시 공통 클래스 추가 - // }); - - // map.addOverlay(overlay); - // facilityOverlaysRef.current.push(overlay); - - // // React 컴포넌트를 popupElement에 렌더링 - // const root = createRoot(popupElement); - // root.render(); - // }); - - // // 초기 가시성 업데이트 - // updatePopupVisibility(); - - // }, [facilityInfoPopupData]); // 팝업 데이터가 변경될 때마다 실행 - - // // 레이어 가시성에 따라 팝업 가시성을 제어하는 함수 - // const updatePopupVisibility = useCallback(() => { - // const map = mapInstanceRef.current; - // if (!map) return; - - // const visibleLayers = new Set(); - // map.getLayers().forEach(layer => { - // if (layer.getVisible()) { - // // feature.js의 로직처럼 typeName이나 ftrCde를 사용해 필터링 - // const typeName = layer.get('typeName'); - // if (typeName === 'VW_WTL_FCLTY_AS') { // 예시 레이어 이름 - // visibleLayers.add(layer.get('ftrCde')); - // } - // } - // }); - - // facilityOverlaysRef.current.forEach(overlay => { - // const element = overlay.getElement(); - // const ftrCde = element.querySelector('table')?.classList[0]; // FacilityPopup의 클래스에서 ftrCde 가져오기 - - // if (visibleLayers.has(ftrCde)) { - // element.style.display = ''; - // } else { - // element.style.display = 'none'; - // } - // }); - // }, []); - - // GisTree에서 체크 상태가 변경될 때 이 함수를 호출하도록 연결해야 합니다. - // 예시: GisTree 컴포넌트에 onCheck 콜백을 추가하고, 그 콜백에서 이 함수를 호출 - // useEffect(() => { - // // 레이어 가시성이 변경될 때마다 팝업 가시성 업데이트 - // const map = mapInstanceRef.current; - // if (!map) return; - - // const handleLayerVisibilityChange = () => { - // updatePopupVisibility(); - // }; - - // map.getLayers().on('change:length', handleLayerVisibilityChange); - // map.getLayers().forEach(layer => { - // layer.on('change:visible', handleLayerVisibilityChange); - // }); - - // return () => { - // // 클린업 - // map.getLayers().un('change:length', handleLayerVisibilityChange); - // map.getLayers().forEach(layer => { - // layer.un('change:visible', handleLayerVisibilityChange); - // }); - // }; - // }, [updatePopupVisibility]); - - // 체크된 레이어 정보를 처리하는 함수 - const handleCheckedLayersChange = useCallback((checkedIds, checkedNodesData) => { - setCheckedLayers(checkedIds); - setCheckedLayersData(checkedNodesData); - onCheckedLayersChange?.(checkedIds, checkedNodesData); - }, [onCheckedLayersChange]); - return ( <>
- + {/* */} - {/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} + {/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} {isOpen && featureData && !isSearchModalOpen && ( )} - {/* SearchView가 존재할 때만 렌더링 */} - {SearchView && ( - - )} + - +
{isSearch && ( 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 72a584970..ddfcec3a1 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 @@ -1,24 +1,26 @@ import MousePosition from 'ol/control/MousePosition'; import ScaleLine from 'ol/control/ScaleLine'; -import { getTopLeft } from 'ol/extent'; import Feature from 'ol/Feature'; -import GeoSJON from 'ol/format/GeoJSON'; import WKT from 'ol/format/WKT'; -import { Point } from 'ol/geom'; import LinearRing from 'ol/geom/LinearRing'; import Draw from 'ol/interaction/Draw'; import TileLayer from 'ol/layer/Tile'; -import VectorLayer from 'ol/layer/Vector'; import * as olProj from 'ol/proj'; import VectorSource from 'ol/source/Vector'; -import TileWMTS from 'ol/source/WMTS'; import XYZ from 'ol/source/XYZ'; +import GeoSJON from 'ol/format/GeoJSON'; import { Fill, Stroke, Style } from 'ol/style'; import CircleStyle from 'ol/style/Circle'; +import VectorLayer from 'ol/layer/Vector'; +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 { register } from 'ol/proj/proj4'; 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'; @@ -92,7 +94,7 @@ GisApp.core.prototype = { target: '', centerX: 37.2806094, centerY: 127.0032502, - zoom: 12, + zoom: 15, minZoom: 6, maxZoom: 20, }, @@ -560,7 +562,6 @@ GisApp.core.prototype = { * * Returns: */ - // 레이어를 지도에 표시/숨김 setLayerOnMap: function (layerIds, visible, addQurryFlter) { for (let i in layerIds) { let id = layerIds[i]; @@ -571,14 +572,10 @@ GisApp.core.prototype = { const layerCode = GisApp.layerCode[id]; const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || ''); layerCode['flterSql'] = queryFlter; - //layer = this.addWmsLayer(layerCode); - - // WFS 레이어 생성 및 지도에 추가 + //layer = this.addWmsLayer(layerCode); layer = this.addWfsLayer(layerCode); } } - - // 선택 인터랙션 및 스냅 기능 설정 if (GisApp.Module.select) { this.setSelectInteraction(); } 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 d9657c313..097c90ba5 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 @@ -77,7 +77,15 @@ GisOlLayer.prototype = { let lyrTyNm = options.lyrTyNm; let typename = this.storageNm + lyrTyNm; - if(options.lyrTyNm == "om_dt_untrt_swg_rain"){ //중점우수토실실 + if(options.lyrTyNm == "om_dt_untrt_swg_rain" //중점우수토실 + || options.lyrTyNm == "om_wqlt_station" //관측소 + || options.lyrTyNm == "tb_wr_bdst_ruse_fclt" //재이용시설 + || options.lyrTyNm == "tb_wr_rnwtr_utztn_fclt" //빗물이용시설 + || options.lyrTyNm == "tb_wr_rswtr_fclt" //중수도시설 + || options.lyrTyNm == "tb_wr_htdfl_fclt" //온배수시설 + || options.lyrTyNm == "tb_wr_tmpr_dmd_co" //잠재수요처 + ) + { url = GisApp.SN_GEOSERVER_URL; typename = lyrTyNm; } diff --git a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx index 6ccaac946..2d7d2bf90 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -3,9 +3,9 @@ import { SelectBox } from "../select"; import { Input } from "@common/components"; import { FormUi } from "../form_ui"; import { useCallback, useEffect, useState } from "react"; +import { AsYouType } from "libphonenumber-js"; import type { ControllerRenderProps, UseFormReturn } from "react-hook-form"; import { SdmSearchForm } from "./sdmNameBox"; -import { set } from "date-fns"; // 수요처 정보 export type DemandCompType = { @@ -79,6 +79,19 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun const [updateCoNm, setUpdateNm] = useState('') + + const formatPhoneNumber = (value:string) => { + const input = value; + const formatter = new AsYouType("KR"); + const formatted = formatter.input(input); + + // 저장할 값 처리: 숫자만 추출 + const digitsOnly = input.replace(/\D/g, ""); + + return formatted + }; + + const initializeSigunData = useCallback((sidoCd: string) => { if (sidoCd === '0') { setSigunValue('0'); @@ -325,14 +338,23 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun { name: "telno", label: "전화번호", - component: (field) => ( + component: (field) => + + { + const formattedValue = formatPhoneNumber(field.value??''); // 초기 렌더링 시에도 포맷 적용 + + return ( { + const value = e.target.value.replace(/\D/g, ''); // 숫자만 저장 + field.onChange(value); + }} /> - ) + )} }, { diff --git a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx index 13e461910..4219602e6 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -41,8 +41,8 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,up telno: z.string() .min(8, "전화번호는 8자 이상 입력해주세요") .max(11, "전화번호는 11자 이내로 입력해주세요") - .regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), - rgtr: z.string().optional(), + .regex(/^[0-9]+$/, "전화번호는 숫자만 입력 가능합니다"), + rgtr: z.string().optional(), dmdAplyPswd : updateList?.dmdInfoPk // 업데이트일경우 ? z.string().optional() // 패스워드는 선택 사항이며 길이 제한 없음 : z.string().optional()