From 4df286b0863bd5b5ca0aab2c138733f49d029947 Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 19 Aug 2025 15:24:52 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20UiTable=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=20headButton=20props=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 --- .../business_components/src/ui/table/index.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/common/business_components/src/ui/table/index.tsx b/packages/common/business_components/src/ui/table/index.tsx index fa7c9b7fe..dcf7f4aa4 100644 --- a/packages/common/business_components/src/ui/table/index.tsx +++ b/packages/common/business_components/src/ui/table/index.tsx @@ -30,6 +30,7 @@ type UiTableProps = { boldArray?: number[]; resetSelection?: boolean; index?: number; + headButton?: React.ReactNode | undefined; }; type UiHeadProps = { @@ -53,6 +54,7 @@ export function UiTable({ boldArray = [], resetSelection = false, index = -1, + headButton = undefined, }: UiTableProps) { const [cur, setCur] = useState(1); const [idx, setIdx] = useState(index); @@ -100,9 +102,16 @@ export function UiTable({ return ( <> {headName ? ( -
-

{headName}

-
+ headButton ? ( +
+

{headName}

+ <>{headButton} +
+ ) : ( +
+

{headName}

+
+ ) ) : ( <> )} From ebfc520d676842cc71556474359fbfd28f0ca244 Mon Sep 17 00:00:00 2001 From: swcho Date: Wed, 20 Aug 2025 18:50:08 +0900 Subject: [PATCH 2/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]; } }; From 2f4cfa0d78a63366dd1883931df25322a3827d6b Mon Sep 17 00:00:00 2001 From: jsyang Date: Tue, 26 Aug 2025 13:30:24 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=EB=B0=94=EC=B0=A8=ED=8A=B8=20=ED=88=B4?= =?UTF-8?q?=ED=8C=81=20=EC=88=AB=EC=9E=90=EC=9D=98=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EC=B2=9C=EC=9E=90=EB=A6=AC=20=EC=89=BC=ED=8F=AC=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 --- .../business_components/src/ui/chart/BarChart.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/common/business_components/src/ui/chart/BarChart.tsx b/packages/common/business_components/src/ui/chart/BarChart.tsx index 79aa61c98..ec0647dff 100644 --- a/packages/common/business_components/src/ui/chart/BarChart.tsx +++ b/packages/common/business_components/src/ui/chart/BarChart.tsx @@ -19,6 +19,14 @@ const customStyles = { } as CSSProperties, }; +const fncNumberFormet = (value : string | number) => { + return String(value).includes('.')? + String(parseFloat(value.toString().replace(/,/g, ''))).split('.').map((part, index) => + index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part + ).join('.') + : String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} + type BarChartProps = { chartConfig: ChartConfig | undefined; chartData: any[] | undefined; @@ -51,7 +59,7 @@ const CustomTooltip = ({
{/* 색상 박스 추가 (범례 색상과 동일) */}
- {` ${item.name} : ${item.value}`} + {` ${item.name} : ${fncNumberFormet(item.value)}`}
))}