목록에서 시설 클릭시 화면으로 이동 기능 추가

This commit is contained in:
yschoi
2025-08-12 17:26:26 +09:00
parent b87d1280ef
commit 3641058fd0
8 changed files with 734 additions and 512 deletions
@@ -275,7 +275,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
// API에서 상세 데이터 가져오기
if (apiEndpoint) {
try {
console.log(`API 호출: ${apiEndpoint}`);
const result = await getData(apiEndpoint);
if (result) {
+29 -27
View File
@@ -1,6 +1,6 @@
"use client";
import Gis, { useGisContext } from "@common/business_components/ui/map/Gis";
import Gis from "@common/business_components/ui/map/Gis";
import { getData } from "../../components/lib/api";
import { useCallback, useEffect, useRef, useState } from "react";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
@@ -43,7 +43,6 @@ const Map = () => {
const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
fetchData();
}, []);
@@ -96,6 +95,7 @@ const Map = () => {
});
}
// 3. 피처 선택
if (foundFeature) {
let geometry = foundFeature.getGeometry();
@@ -138,31 +138,33 @@ const Map = () => {
return (
<div className="bg-container">
<Gis
treeData={treeData}
isSearch={true}
gisMaps={gisMaps}
isMiniMap={false}
searchView={SearchModal}
detailView={FeatureClickEventPopup}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
searchBar={<ProcSearchBar gisMaps={gisMaps} facilityTypes={facilityTypes} />}
waterDepth={showWaterDepth ? WaterDepth : undefined}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying as any}
targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any}
orderedLayers={ORDERED_LAYERS as any}
checkInterval={2500}
/>
</div>
<>
<div className="bg-container">
<Gis
treeData={treeData}
isSearch={true}
gisMaps={gisMaps}
isMiniMap={false}
searchView={SearchModal}
detailView={FeatureClickEventPopup}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
searchBar={<ProcSearchBar gisMaps={gisMaps} facilityTypes={facilityTypes} />}
waterDepth={showWaterDepth ? WaterDepth : undefined}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying as any}
targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any}
orderedLayers={ORDERED_LAYERS as any}
checkInterval={2500}
/>
</div>
</>
);
};
@@ -10,6 +10,7 @@ import OperationTab, { operationCleaningTableHeadlist, operationInspectionTableH
import ReportTab from './reportTab';
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import useStore from '../../../store/useStore';
import GisApp from "@common/business_components/ui/map/lib/gis.core";
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`;
@@ -184,7 +185,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
size: '10',
sort: 'string',
});
console.log('basicType', basicType);
if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = zoneCqlFilters(); // 하수처리구역
else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = facilityCqlFilters(); // 하수처리시설
@@ -195,10 +195,7 @@ const SearchModal: React.FC<SearchModalProps> = ({
if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and '));
console.log('API URL:', `${API_URL}/v1/fclts?${queryParams.toString()}`);
const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`);
console.log('필터링된 API 결과(개수):', res.data.count);
console.log('필터링된 API 결과(데이터):', res.data.data);
// 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가
const dataWithRowNumber = (res.data.data || []).map((item: any, index: number) => {
@@ -217,7 +214,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
setTableData(dataWithRowNumber);
setTableCount(res.data.count || []);
console.log('tableCount 상태 업데이트 후:', res.data.count); // tableCount 로그 추가
} else if (activeTab === 'operation') {
const RequestDTO: Record<string, string> = {
@@ -247,13 +243,9 @@ const SearchModal: React.FC<SearchModalProps> = ({
const params = new URLSearchParams(RequestDTO).toString();
console.log('API URL:', `/v1/bizInfo?${params}`);
const res = await getData(`v1/bizInfo?${params}`);
console.log('필터링된 API 결과(개수):', res.totalElements);
console.log('필터링된 API 결과(데이터):', res.content);
const mappedContent = (res.content || []).map((item: any, index: number) => {
console.log("item:", item);
const formattedItem = formatDisplayData(item);
const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTypeCd);
return {
@@ -456,7 +448,12 @@ const SearchModal: React.FC<SearchModalProps> = ({
// 기본정보 테이블 cellClick 핸들러
const handleBasicCellClick = (index?: number | undefined, headId?: String) => {
if (typeof index === "number") {
onFeatureSelect(tableData[index % 10]); // 10은 page size 하드 코딩한 숫자
const row = tableData[index];
const layerNm = row.lyrTypeNm;
const fid = row.fid;
const core = GisApp.Module.layer as any;
const r = core.showSearchResultsFromGeoJSON(layerNm, fid);
onOpenChange(false); // SearchModal 닫기
}
};
@@ -480,10 +477,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
console.log('운영정보 cellClick 데이터:', res);
console.log('운영정보 cellClick.data:', res.data);
console.log('운영정보 length:', res.data.length);
const mappedData = (res.data || []).map((item: any, detailIndex: number) => {
const formattedItem = formatDisplayData(item);
@@ -497,8 +490,6 @@ const SearchModal: React.FC<SearchModalProps> = ({
};
});
console.log('운영정보 cellClick mappedData:', mappedData);
setDetailTableData(mappedData);
setDetailTableCount(res.count || 0);
} catch (error) {
+4 -1
View File
@@ -34,7 +34,10 @@
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"zod": "^3.24.2",
"zustand": "^5.0.3"
"zustand": "^5.0.3",
"ol": "^7.3.0",
"ol-ext": "^4.0.11",
"proj4": "^2.9.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -21,6 +21,7 @@ import GisToolBar from "./GisToolBar";
import GisTree from "./GisTree";
import "./lib/css/gis.common.css";
import GisApp from "./lib/gis.core";
import LoadingOverlay from '@/components/ui/LoadingBar'
export const GisContext = createContext({
gisCore: null,
@@ -149,6 +150,7 @@ function Gis({
// 체크된 레이어 상태 관리
const [checkedLayers, setCheckedLayers] = useState([]);
const [checkedLayersData, setCheckedLayersData] = useState([]);
const [isLoading, setIsLoading] = useState(true) //로딩 상태
let gisMap = null;
const closePopup = () => {
@@ -216,12 +218,16 @@ function Gis({
}),
});
} else {
// 📌 재마운트 시 target 없으면 다시 지정
// 재마운트 시 target 없으면 다시 지정
if (!map.getTarget()) {
map.setTarget(mapRef.current);
}
}
map.on('loadstart', function () {
setIsLoading(false)
});
const select = GisApp.Module.select;
if (select) {
if (select.selectInteraction) {
@@ -316,9 +322,9 @@ function Gis({
const handleCheckedLayersChange = useCallback(
(checkedIds, checkedNodesData) => {
setTimeout(() => {
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
onCheckedLayersChange?.(checkedIds, checkedNodesData);
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
onCheckedLayersChange?.(checkedIds, checkedNodesData);
}, 1000);
},
[onCheckedLayersChange],
@@ -332,6 +338,7 @@ function Gis({
return (
<>
<LoadingOverlay isLoading={isLoading ?? false} />
<GisContext.Provider
value={{
gisCore,
File diff suppressed because it is too large Load Diff
@@ -650,7 +650,7 @@ GisApp.core.prototype = {
*/
getFeatureGeomProp: function (geometry, ftrIds, ftrCde) {
let returnFeature;
this.map.getLayers().forEach(function (layer) {
this.map.getLayers().getArray().some(function (layer) {
let type = layer.get('type');
if (type != 'base') {
let source = layer.getSource();
@@ -673,6 +673,7 @@ GisApp.core.prototype = {
featureFtrCde = properties['FTR_CDE'];
if (ftrIds == featureFtrIds && ftrCde == featureFtrCde) {
returnFeature = feature;
return true;
}
}
}
@@ -685,7 +686,7 @@ GisApp.core.prototype = {
*/
getFeatureByFtrIdn: function (typeName, ftrIdn) {
let returnFeature;
this.map.getLayers().forEach(function (layer) {
this.map.getLayers().getArray().some(function (layer) {
let layerTypeName = layer.get('typeName');
if (layerTypeName === typeName) {
layer
@@ -695,12 +696,16 @@ GisApp.core.prototype = {
let featureFtrIdn = feature.get('ftrIdn');
if (featureFtrIdn === ftrIdn) {
returnFeature = feature;
return true;
}
});
}
});
return returnFeature;
},
getFeatureByFeatureId: function (featureId) {
return GisApp.Module.layer.getFeatureById(featureId);
},
/**
* [사용]맵 이벤트 추가
*/
@@ -33,6 +33,7 @@ const GisOlLayer = function (options) {
GisOlLayer.prototype = {
layers: [],
cesiumLayers: [],
searchLayer: null,
defaults: {},
storageNm: 'hasudo:',
defaultLayerOptions: {
@@ -76,7 +77,7 @@ GisOlLayer.prototype = {
let toProj = GisApp.TO_PROJ;
let lyrTyNm = options.lyrTyNm;
let typename = this.storageNm + lyrTyNm;
const params = {};
params['TYPENAME'] = typename;
params['SERVICE'] = 'WFS';
@@ -92,11 +93,11 @@ GisOlLayer.prototype = {
const [minX, minY, maxX, maxY] = extent;
const bboxFilter = `BBOX(geom, ${minX}, ${minY}, ${maxX}, ${maxY}, '${toProj}')`;
const filter = options?.flterSql && options.flterSql && options.flterSql.trim() !== ''
? `(${options.flterSql}) AND ${bboxFilter}`
: bboxFilter;
? `(${options.flterSql}) AND ${bboxFilter}`
: bboxFilter;
params['CQL_FILTER'] = filter;
const strParams = new URLSearchParams(params).toString();
return url + '?' + strParams;
},
@@ -107,13 +108,13 @@ GisOlLayer.prototype = {
/**
* [사용]WFS 벡터 소스 반환
*/
getVectorTileSource: function (options) {
getVectorSourceAll: function (options) {
let url = GisApp.GEOSERVER_URL;
let toProj = GisApp.TO_PROJ;
let lyrTyNm = options.lyrTyNm;
let typename = this.storageNm + lyrTyNm;
let filter = options?.flterSql && options.flterSql && options.flterSql.trim() !== '';
let params = {};
params['TYPENAME'] = typename;
params['SERVICE'] = 'WFS';
@@ -150,12 +151,12 @@ GisOlLayer.prototype = {
let cmncNm = option.cmncNm;
try {
if(cmncNm == 'bbox') {
if (cmncNm == 'bbox') {
source = this.getVectorSourceBbox(option);
}else {
source = this.getVectorTileSource(option);
} else {
source = this.getVectorSourceAll(option);
}
}catch(error){
} catch (error) {
console.warn(option);
}
@@ -548,7 +549,7 @@ GisOlLayer.prototype = {
cesiumLayer['properties'] = properties;
me.cesiumLayers.push(cesiumLayer);
if (properties?.visible) {
const zoomLevel = getOpenLayersZoom(cesiumScene);
const zoomLevel = this.getOpenLayersZoom(cesiumScene);
cesiumLayer.show = zoomLevel >= Number(properties?.minZoom);
} else {
cesiumLayer.show = false;
@@ -557,7 +558,7 @@ GisOlLayer.prototype = {
});
if (me.cesiumLayers.length > 0) {
cesiumScene._moveEndListener = () => {
const zoomLevel = getOpenLayersZoom(cesiumScene); // 대략적인 줌 계산
const zoomLevel = this.getOpenLayersZoom(cesiumScene); // 대략적인 줌 계산
me.cesiumLayers.forEach(cesiumLayer => {
const properties = cesiumLayer.properties;
if (properties?.visible) {
@@ -569,22 +570,121 @@ 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;
}
};
function getOpenLayersZoom(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)));
}
export default GisOlLayer;