From c4732ebb8f14e0db90c0b07adc8c9f62c402a8e8 Mon Sep 17 00:00:00 2001 From: swcho Date: Wed, 20 Aug 2025 11:07:51 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20-=20=EC=9A=B4=EC=98=81=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EA=B2=B0=EA=B3=BC=20=EA=B4=80=EB=A1=9C=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=EC=8B=9C=20=ED=95=B4=EB=8B=B9=20=EA=B4=80=EB=A1=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20-=20searchModal=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=9C=A0=EC=A7=80=EB=90=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/searchModal/searchModal.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx index 61f7f5acf..7c4762a3c 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx @@ -149,18 +149,7 @@ const SearchModal: React.FC = ({ setIsResetDetailSelection(true); // 선택 초기화 setDetailBizId(0); // queryParams는 fetchFilteredData 내에서 매번 새로 생성되므로 별도 상태 초기화는 필요 없음 - }, [activeTab, basicType, workType, open]); - - useEffect(() => { - setActiveTab(TABS[0]?.val ?? "basic"); - setBasicType(BASIC_TYPES[0]?.val ?? ""); - setWorkType(WORK_TYPES[0]?.val ?? ""); - setTableCount(0); - setTableData([]); - setDetailTableCount(0); - setDetailTableData([]); - setDetailBizId(0); - }, [open]); + }, [activeTab, basicType, workType]); // 페이지 변경 시마다 데이터 fetch useEffect(() => { @@ -521,8 +510,7 @@ const SearchModal: React.FC = ({ RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"')); const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`); - console.log('운영정보 cellClick 데이터1:', res); - console.log('운영정보 cellClick 데이터2:', res.data); + console.log('운영정보 cellClick 데이터:', res.data); const mappedData = (res.data || []).map((item: any, detailIndex: number) => { const formattedItem = formatDisplayData(item); @@ -587,6 +575,18 @@ const SearchModal: React.FC = ({ } }; + const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => { + if (typeof index === "number") { + + const row = detailTableData[index]; + const layerNm = row.lyrTypeNm; + const fid = row.fid; + const core = GisApp.Module.layer as any; + core.showSearchResultsFromGeoJSON(layerNm, fid); + onOpenChange(false); // SearchModal 닫기 + } + }; + // 기본정보 Excel 다운로드 핸들러 const BasicExcelDownload = async () => { if (tableData.length === 0) { @@ -1006,7 +1006,7 @@ const SearchModal: React.FC = ({ <> {/* 탭 영역 */} ({ value: tab.val, label: tab.text @@ -1133,7 +1133,7 @@ const SearchModal: React.FC = ({ headlist={dynamicHeadlist2} pageSize={OPERATION_TABLE_SIZE} total={detailTableCount} - cellClick={undefined} + cellClick={handleOperationResultCellClick} onPageChange={setDetailCurPage} resetPageNo={isResetDetailPage} resetSelection={isResetDetailSelection} From 3fb331b4bc2ddf732e7339c290ee7f10c50e3d7c Mon Sep 17 00:00:00 2001 From: swcho Date: Wed, 20 Aug 2025 18:42:35 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=EC=84=A4=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=EC=97=90=EC=84=9C=20=EB=B3=B4=EA=B8=B0=20(=EC=9D=B4?= =?UTF-8?q?=EC=A0=84=20=EC=8B=9C=EC=84=A4=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=ED=95=B4=EC=95=BC=20=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/searchModal/searchModal.tsx | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx index 7c4762a3c..2fb4e7cae 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx @@ -479,14 +479,16 @@ const SearchModal: React.FC = ({ }; // 기본정보 테이블 cellClick 핸들러 - const handleBasicCellClick = (index?: number | undefined, headId?: String) => { + const handleBasicCellClick = async (index?: number | undefined, headId?: String) => { if (typeof index === "number") { + setIsLoading(true); const row = tableData[index]; const layerNm = row.lyrTypeNm; const fid = row.fid; const core = GisApp.Module.layer as any; - core.showSearchResultsFromGeoJSON(layerNm, fid); + await core.showSearchResultsFromGeoJSON(layerNm, fid); + setIsLoading(false); onOpenChange(false); // SearchModal 닫기 } }; @@ -577,18 +579,20 @@ const SearchModal: React.FC = ({ const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => { if (typeof index === "number") { + setIsLoading(true); const row = detailTableData[index]; const layerNm = row.lyrTypeNm; const fid = row.fid; const core = GisApp.Module.layer as any; - core.showSearchResultsFromGeoJSON(layerNm, fid); + await core.showSearchResultsFromGeoJSON(layerNm, fid); + setIsLoading(false); onOpenChange(false); // SearchModal 닫기 } }; // 기본정보 Excel 다운로드 핸들러 - const BasicExcelDownload = async () => { + const basicExcelDownload = async () => { if (tableData.length === 0) { alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); return; @@ -804,7 +808,7 @@ const SearchModal: React.FC = ({ }; // 운영정보 Excel 다운로드 핸들러 추가 - const OperationExcelDownload = async () => { + const operationExcelDownload = async () => { if (tableData.length === 0) { alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); return; @@ -898,7 +902,7 @@ const SearchModal: React.FC = ({ }; // 운영정보 Excel 다운로드 핸들러 추가 - const OperationResultExcelDownload = async () => { + const operationResultExcelDownload = async () => { if (detailTableData.length === 0) { alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); return; @@ -1001,6 +1005,20 @@ const SearchModal: React.FC = ({ } }; + const viewMultipleFeature = async () => { + if (!(tableData.length === 0)) { + setIsLoading(true); + + const row = tableData[0]; + const layerNm = row.lyrTypeNm; + const fids = tableData.map((item) => item.fid); + const core = GisApp.Module.layer as any; + await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids); + setIsLoading(false); + onOpenChange(false); + } + } + const renderBody = () => ( <> @@ -1084,7 +1102,10 @@ const SearchModal: React.FC = ({ resetPageNo={isResetPage} resetSelection={isResetSelection} headButton={ - +
+ + +
} /> @@ -1112,7 +1133,7 @@ const SearchModal: React.FC = ({ resetPageNo={isResetPage} resetSelection={isResetSelection} headButton={ - + } /> @@ -1138,7 +1159,7 @@ const SearchModal: React.FC = ({ resetPageNo={isResetDetailPage} resetSelection={isResetDetailSelection} headButton={ - + } /> From ebfc520d676842cc71556474359fbfd28f0ca244 Mon Sep 17 00:00:00 2001 From: swcho Date: Wed, 20 Aug 2025 18:50:08 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20-=20=EC=97=AC=EB=9F=AC=20=EA=B0=9C?= =?UTF-8?q?=EC=9D=98=20=EA=B2=80=EC=83=89=20=EA=B2=B0=EA=B3=BC=EB=A5=BC=20?= =?UTF-8?q?GeoJSON=EC=9C=BC=EB=A1=9C=20=ED=91=9C=EC=8B=9C=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80=20-=20=ED=94=BC=EC=B3=90=20ID?= =?UTF-8?q?=EB=A1=9C=20=EC=A7=80=EC=98=A4=EC=84=9C=EB=B2=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B2=80=EC=83=89=ED=95=B4=EC=84=9C=20=EC=97=AC?= =?UTF-8?q?=EB=9F=AC=EA=B0=9C=20=ED=94=BC=EC=B2=98=EC=9D=98=20=EB=B2=A1?= =?UTF-8?q?=ED=84=B0=EC=86=8C=EC=8A=A4=20=EB=A6=AC=ED=84=B4=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=EC=97=AC=EB=9F=AC=20extent=20=EB=B0=B0=EC=97=B4=EC=9D=84=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=20=EC=A4=91=EC=95=99=EA=B0=92=20extent?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/map/lib/gis.layer.js | 226 +++++++++++++++++- 1 file changed, 218 insertions(+), 8 deletions(-) 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 6cbc054f8..538fad976 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 @@ -569,14 +569,224 @@ GisOlLayer.prototype = { cesiumScene.camera.moveEnd.addEventListener(cesiumScene._moveEndListener); } }, - getFeatureById: function(featureId){ - this.layers - .getArray() - .forEach(layer => { - const source = layer.getSource(); - const feature = source.getFeatureById(String(featureId).trim()); - if (feature) return feature; - }); + getFeatureById: function (featureId) { + let returnFeature; + this.layers.getArray().some(function (layer) { + const source = layer.getSource(); + + if (source instanceof VectorSource) { + const feature = source.getFeatureById(featureId); + if (feature) { + returnFeature = feature; + return true; + } + } + }); + + return returnFeature; + }, + getOpenLayersZoom: function (cesiumScene) { + const cameraHeight = cesiumScene.camera.positionCartographic.height; + const maxResolution = 156543.03392804097; // OpenLayers 기본 해상도 (Google Mercator 기준) + const tileSize = 256; // 기본 타일 크기 + return Math.ceil(Math.log2(maxResolution / (cameraHeight / tileSize))); + }, + ensureSearchLayer: function () { + let layer = this.searchLayer; + + if (!layer) { + layer = new VectorLayer({ + source: new VectorSource(), + zIndex: 9999 + }); + let map = GisApp.Module.core.map; + map.addLayer(layer); + } + return layer; + }, + clearSearchLayer: function () { + const layer = this.ensureSearchLayer(); + layer.getSource().clear(true); + }, + showSearchResultsFromGeoJSON: function (lyrTyNm, featureId) { + const layer = this.ensureSearchLayer(); + + // 이전 검색결과는 깨끗이 + const prev = layer.getSource(); + if (prev) prev.clear(true); + + //레이어 타입 설정(point, line 등. 이걸 style에서 사용) + const layerCode = GisApp.layerCode; + const matched = Object.values(layerCode).find(obj => obj.lyrTyNm === lyrTyNm); + layer.set("gobjTy", matched.gobjTy); + + // 새 소스 만들기 + const source = this.getFeatureSourceFromGeoserver(lyrTyNm, featureId); + + // 1) 로드 완료시 extent 맞추기 + const onLoadEnd = () => { + const feats = source.getFeatures(); + if (feats && feats.length > 0) { + const select = GisApp.Module.select; + select.selectFeature(feats[0], false); + } else { + console.warn('[search-layer] GeoJSON에 피처가 없습니다.'); + } + // 한번만 쓰고 이벤트 정리 + source.un('featuresloadend', onLoadEnd); + source.un('featuresloaderror', onLoadError); + }; + + const onLoadError = (e) => { + console.error('[search-layer] GeoJSON 로드 실패:', e); + source.un('featuresloadend', onLoadEnd); + source.un('featuresloaderror', onLoadError); + }; + + // 이벤트를 먼저 걸고 → setSource + source.once('featuresloadend', onLoadEnd); + source.once('featuresloaderror', onLoadError); + + // 레이어에 소스 적용(이때부터 로딩 시작) + layer.setSource(source); + + // 필요시 명시적으로 재요청 + source.refresh(); + }, + /** + * 피쳐 ID로 지오서버에서 검색해서 피처의 벡터소스를 리턴 + * @param {} lyrTyNm + * @param {*} featureId + * @returns + */ + getFeatureSourceFromGeoserver: function (lyrTyNm, featureId) { + + const typeName = this.storageNm + lyrTyNm; + const srsName = GisApp.TO_PROJ; + const baseUrl = GisApp.GEOSERVER_URL; + const source = new VectorSource({ + format: new GeoJSON(), + url: () => { + const params = new URLSearchParams({ + SERVICE: 'WFS', + VERSION: '1.1.0', + REQUEST: 'GetFeature', + TYPENAME: typeName, + SRSNAME: srsName, + OUTPUTFORMAT: 'application/json', + FEATUREID: featureId, + }); + return `${baseUrl}?${params.toString()}`; + }, + strategy: all, + }); + + return source; + }, + /** + * 여러 개의 검색 결과를 GeoJSON으로 표시 + * @param {string} lyrTyNm 레이어 타입명 + * @param {Array|string} featureIds 피처 ID 배열 또는 단일 ID + */ + showMultipleSearchResultsFromGeoJSON: function (lyrTyNm, featureIds) { + const layer = this.ensureSearchLayer(); + + const prev = layer.getSource(); + if (prev) prev.clear(true); + + const layerCode = GisApp.layerCode; + const matched = Object.values(layerCode).find(obj => obj.lyrTyNm === lyrTyNm); + layer.set("gobjTy", matched.gobjTy); + + const source = this.getMultiFeatureSourceFromGeoserver(lyrTyNm, featureIds); + + const onLoadEnd = async () => { + const feats = source.getFeatures(); + if (feats && feats.length > 0) { + const select = GisApp.Module.select; + await select.resetSelectedFeature(); + for (const feature of feats) select.selectMultiFeature(feature, true); + const geometries = feats.map(f => f.getGeometry()); + if (geometries.length > 0) { + const extent = this.combineExtents(geometries.map(g => g?.getExtent())); + + let map = GisApp.Module.core.map; + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16 + }); + } + } else { + console.warn('[search-layer] GeoJSON에 피처가 없습니다.'); + } + source.un('featuresloadend', onLoadEnd); + source.un('featuresloaderror', onLoadError); + }; + + const onLoadError = (e) => { + console.error('[search-layer] GeoJSON 로드 실패:', e); + source.un('featuresloadend', onLoadEnd); + source.un('featuresloaderror', onLoadError); + }; + + source.once('featuresloadend', onLoadEnd); + source.once('featuresloaderror', onLoadError); + + layer.setSource(source); + + source.refresh(); + }, + /** + * 피쳐 ID로 지오서버에서 검색해서 여러개 피처의 벡터소스를 리턴 + * @param {} lyrTyNm + * @param {*} featureId + * @returns + */ + getMultiFeatureSourceFromGeoserver: function (lyrTyNm, featureId) { + + const typeName = this.storageNm + lyrTyNm; + const srsName = GisApp.TO_PROJ; + const baseUrl = GisApp.GEOSERVER_URL; + + const ids = Array.isArray(featureId) ? featureId : [featureId]; + + const source = new VectorSource({ + format: new GeoJSON(), + url: () => { + const params = new URLSearchParams({ + SERVICE: 'WFS', + VERSION: '1.1.0', + REQUEST: 'GetFeature', + TYPENAME: typeName, + SRSNAME: srsName, + OUTPUTFORMAT: 'application/json', + FEATUREID: ids.join(','), + }); + return `${baseUrl}?${params.toString()}`; + }, + strategy: all, + }); + + return source; + }, + /** + * 여러 extent 배열을 받아 결합된 extent를 반환 + * @param {Array>} extents [[minX,minY,maxX,maxY], ...] + * @returns {Array|null} + */ + combineExtents: function (extents) { + if (!extents || extents.length === 0) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const e of extents) { + if (!e || e.length < 4) continue; + minX = Math.min(minX, e[0]); + minY = Math.min(minY, e[1]); + maxX = Math.max(maxX, e[2]); + maxY = Math.max(maxY, e[3]); + } + if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) return null; + return [minX, minY, maxX, maxY]; } };