sn: 지도 component 수정 반영

This commit is contained in:
2026-06-26 17:23:03 +09:00
parent c0888ea2a4
commit f93f2674df
9 changed files with 110 additions and 35 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -49,12 +49,14 @@ function Gis({
searchView: SearchView,
detailView: DetailView,
onFeatureSelect,
canSelectFeature,
setIsSearchModalOpen,
onCheckedLayersChange,
searchBar: SearchBar,
waterDepth: WaterDepth = null,
layerSettingView: LayerSettingView = null,
inlineLayerSettingView,
treeSectionClassName = "",
layerIconVisible,
isPlaying = false,
setIsPlaying = () => { },
@@ -73,6 +75,7 @@ function Gis({
zoomGuide,
showFloatingSearchButton = true,
treeTogglePosition = "center",
detachedTreeToggleButton = false,
showZoomScaleInfo = false,
}) {
const mapRef = useRef();
@@ -111,7 +114,7 @@ function Gis({
(data) => {
onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출
},
[mapInstanceRef],
[onFeatureSelect],
);
// 지도 생성 및 초기화
@@ -315,7 +318,9 @@ function Gis({
onProgressChange={handleProgressChange}
layerSettingView={LayerSettingView}
inlineLayerSettingView={inlineLayerSettingView}
treeSectionClassName={treeSectionClassName}
toggleButtonPosition={treeTogglePosition}
detachedToggleButton={detachedTreeToggleButton}
/>
{typeof WaterDepth === "function" && (
@@ -354,7 +359,7 @@ function Gis({
<GisZoomLevel zoomGuide={zoomGuide} showZoomScaleInfo={showZoomScaleInfo} />
)}
<GisMapToggle />
<GisSelect onFeatureSelect={handleFeatureSelect} />
<GisSelect onFeatureSelect={handleFeatureSelect} canSelectFeature={canSelectFeature} />
</div>
</div>
@@ -1,25 +1,43 @@
import React, { useContext, useEffect } from "react";
import React, { useContext, useEffect, useRef } from "react";
import { GisContext } from "./Gis";
import GisApp from "./lib/gis.core";
import GisOlSelect from "./lib/gis.select";
export default React.memo(function GisSelect({ onFeatureSelect }) {
export default React.memo(function GisSelect({ onFeatureSelect, canSelectFeature }) {
const { gisMap } = useContext(GisContext);
const onFeatureSelectRef = useRef(onFeatureSelect);
const canSelectFeatureRef = useRef(canSelectFeature);
useEffect(() => {
onFeatureSelectRef.current = onFeatureSelect;
}, [onFeatureSelect]);
useEffect(() => {
canSelectFeatureRef.current = canSelectFeature;
}, [canSelectFeature]);
useEffect(() => {
if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함
// 이미 select가 있으면 재생성하지 않음
const select = new GisOlSelect(gisMap);
GisApp.Module.select = select;
// 피처 선택 시 콜백 등록
select.getSelectedInfo(function (features) {
if (features?.[0]) {
onFeatureSelect(features[0]);
const isSelectable = canSelectFeatureRef.current?.(features[0]);
if (isSelectable === false) {
select.resetSelectedFeature?.();
return;
}
onFeatureSelectRef.current?.(features[0]);
}
});
}, [gisMap, onFeatureSelect]);
return () => {
select.removeInteration?.();
};
}, [gisMap]);
// const select = new GisOlSelect(gisMap);
// GisApp.Module.select = select;
@@ -29,6 +29,7 @@ import GisApp from "./lib/gis.core";
* - onProgressChange(total, current): 애니메이션 진행상태 콜백
* - layerSettingView: (선택) 좌측 레이어 트리 하단에 표시할 설정 영역
* - layerIconVisible: (선택) 레이어 행 우측 액션 아이콘 노출 여부를 결정하는 함수
* - detachedToggleButton: (선택) 접기/펼치기 버튼을 패널 밖 고정 핸들로 노출할지 여부
*/
export default React.memo(function GisTree({
checkedNodes = [],
@@ -46,7 +47,10 @@ export default React.memo(function GisTree({
layerSettingView: LayerSettingView = null,
layerIconVisible,
inlineLayerSettingView,
treeSectionClassName = "",
toggleButtonPosition = "center",
showLayerPanelTitle = true,
detachedToggleButton = false,
}) {
const { gisCore, treeData } = useGisContext();
@@ -442,10 +446,11 @@ export default React.memo(function GisTree({
* 레이어 정보로부터 범례 이미지 경로 또는 동적 생성 이미지를 반환합니다.
*/
const makeLegendSrc = (layerinfo) => {
if (!layerinfo.gobjTy) return null;
if (layerinfo.iconNm) {
return GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm;
} else {
}
if (!layerinfo.gobjTy) return null;
else {
return makeLegendImage(layerinfo).src;
}
};
@@ -512,7 +517,10 @@ export default React.memo(function GisTree({
if (!treeData || treeData.length === 0) return null;
const toggleButtonPositionClass =
toggleButtonPosition === "top" ? "top-[5.5rem]" : "top-1/2 -translate-y-1/2";
toggleButtonPosition === "top" ? "top-4" : "top-1/2 -translate-y-1/2";
const thinScrollbarClass =
"[scrollbar-width:thin] [&::-webkit-scrollbar]:w-[4px]";
const treeSectionClassNames = [treeSectionClassName, thinScrollbarClass].filter(Boolean).join(" ");
return (
<>
@@ -524,13 +532,18 @@ export default React.memo(function GisTree({
}`}
>
<div
className={`grow overflow-y-auto border-none p-2 pt-0 shadow-none ${activeTab === "layers" ? "block" : "hidden"
className={`grow overflow-y-auto border-none p-2 ${showLayerPanelTitle ? "pt-0" : "pt-2"} shadow-none ${activeTab === "layers" ? "block" : "hidden"
}`}
>
<h3 className="gis-layer-panel-title mx-2 my-4 text-3xl font-bold text-gray-600 text-center">시설물</h3>
<hr className="mb-4" />
{showLayerPanelTitle && (
<>
<h3 className="gis-layer-panel-title mx-2 my-4 text-3xl font-bold text-gray-600 text-center">시설물</h3>
<hr className="mb-4" />
</>
)}
{/* DnD Tree */}
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<div className={treeSectionClassNames || undefined}>
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<Tree
tree={nodes}
rootId={null}
@@ -542,6 +555,13 @@ export default React.memo(function GisTree({
(child) =>
child.parent === node.id && child.data.exprsYn === "Y"
);
const nodeLabel = String(node?.data?.lyrNm ?? "").trim();
const isSinkholeRootTreeNode =
String(node.id) === "SN_SINKHOLE" ||
nodeLabel === "지반침하정보";
const isFloodHistoryRootTreeNode = nodeLabel === "침수흔적도";
const hideTreeLegendIcon =
isSinkholeRootTreeNode || isFloodHistoryRootTreeNode;
const showLayerActionButton = layerIconVisible?.(node);
const showLayerActionIcon = depth > 0 && imageVisible;
@@ -550,9 +570,10 @@ export default React.memo(function GisTree({
return (
<div style={{ display: isVisible ? "block" : "none" }}>
<div
className="gis-layer-row flex items-center gap-2 rounded px-2 py-1 hover:bg-gray-100"
className="gis-layer-row flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-gray-100"
style={{
marginInlineStart: depth * 16,
width: `calc(100% - ${depth * 16}px)`,
}}
>
{hasVisibleChildren ? (
@@ -563,7 +584,7 @@ export default React.memo(function GisTree({
title={isOpen ? "닫기" : "열기"}
/>
) : (
<span style={{ display: "inline-block", width: 16 }} />
<span className="shrink-0" style={{ display: "inline-block", width: 16 }} />
)}
<input
type="checkbox"
@@ -571,14 +592,17 @@ export default React.memo(function GisTree({
onChange={() => handleCheck(node.id)}
className="cursor-pointer"
/>
{node.icon && <span>{node.icon}</span>}
<span className="text-[1.3rem] font-medium text-label">
{!hideTreeLegendIcon && node.icon && <span>{node.icon}</span>}
<span
className="min-w-0 flex-1 truncate whitespace-nowrap text-[1.3rem] font-medium text-label"
title={String(node.text ?? "")}
>
{node.text}
</span>
{showLayerActionButton && (
<button
type="button"
className="gis-layer-action-button ml-1 shrink-0 rounded-sm border border-[#9cc6f0] bg-white px-2 py-[1px] text-[1.1rem] font-medium leading-5 text-[#2c6fb7] transition-colors hover:bg-[#eef6ff]"
className="gis-layer-action-button ml-auto shrink-0 rounded-sm border border-[#9cc6f0] bg-white px-2 py-[1px] text-[1.1rem] font-medium leading-5 text-[#2c6fb7] transition-colors hover:bg-[#eef6ff]"
onClick={() =>
handleLayerImgClick(
node.data.lyrTyNm,
@@ -624,26 +648,41 @@ export default React.memo(function GisTree({
return aOrder - bOrder;
}}
/>
</DndProvider>
</DndProvider>
</div>
</div>
{LayerSettingView && (
<div className="gis-layer-setting shrink-0 bg-white px-2 pb-3">
<LayerSettingView />
</div>
)}
{!detachedToggleButton && (
<button
onClick={() => setVisible(false)}
className={`gis-layer-toggle absolute right-[-3px] z-50 transform rounded-l-none rounded-r border border-l border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`}
>
<ChevronLeft size={16} />
</button>
)}
</div>
{detachedToggleButton && visible && (
<button
onClick={() => setVisible(false)}
className={`gis-layer-toggle absolute right-[-3px] z-50 transform rounded-l-none rounded-r border border-l border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`}
className={`gis-layer-toggle absolute left-[250px] z-50 flex h-[36px] w-[20px] transform items-center justify-center rounded-[4px] border border-[#d8dee8] bg-[rgba(255,255,255,0.96)] text-[#1e88e5] shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition hover:bg-[#eaf4ff] ${toggleButtonPositionClass}`}
title="접기"
>
<ChevronLeft size={16} />
<ChevronLeft className="h-4 w-4" />
</button>
</div>
)}
{!visible && (
<button
onClick={() => setVisible(true)}
className={`gis-layer-toggle absolute left-[-3px] z-50 transform rounded-r border border-r border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`}
className={detachedToggleButton
? `gis-layer-toggle absolute left-0 z-50 flex h-[36px] w-[20px] transform items-center justify-center rounded-[4px] border border-[#d8dee8] bg-[rgba(255,255,255,0.96)] text-[#1e88e5] shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition hover:bg-[#eaf4ff] ${toggleButtonPositionClass}`
: `gis-layer-toggle absolute left-[-3px] z-50 transform rounded-r border border-r border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`}
title="열기"
>
<ChevronRight size={16} />
{detachedToggleButton ? <ChevronRight className="h-4 w-4" /> : <ChevronRight size={16} />}
</button>
)}
</>
@@ -45,7 +45,7 @@ GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${G
// NGII 정보(배경지도)
//GisApp.NGII_WMTS_URL = process.env.NEXT_PUBLIC_NGII_URL;
GisApp.NGII_WMTS_URL = `/ngii-server/openapi/Gettile.do?apikey=${process.env.NEXT_PUBLIC_NGII_PARAM}`;
GisApp.NGII_WMTS_PARAM = process.env.NEXT_PUBLIC_NGII_PARAM ?? '';
GisApp.EMAP_URL = '/emap/';
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
@@ -79,6 +79,17 @@ GisApp.setLayerInfos = function (layerInfos) {
GisApp.layerInfos = layerInfos;
};
GisApp.getNgiiWmtsUrl = function (layerName, matrix, x, y) {
const layerAwareParams = /(^|&)layer=[^&]*/i.test(GisApp.NGII_WMTS_PARAM)
? GisApp.NGII_WMTS_PARAM.replace(/(^|&)layer=[^&]*/i, `$1layer=${layerName}`)
: `${GisApp.NGII_WMTS_PARAM}&layer=${layerName}`;
return `/ngii-server/openapi/Gettile.do?apikey=${layerAwareParams}`
.replace("{z}", matrix)
.replace("{x}", x)
.replace("{y}", y);
};
/**
* GisApp core
* @requires gis.layer.js
@@ -161,7 +172,7 @@ GisApp.core.prototype = {
});
const wmtsSource = new TileWMTS({
//url: GisApp.NGII_WMTS_URL,
// url은 setTileUrlFunction에서 layer별로 동적으로 만든다.
projection: projection,
tileGrid: tileGrid,
crossOrigin: "anonymous",
@@ -175,11 +186,7 @@ GisApp.core.prototype = {
wmtsSource.setTileUrlFunction((tileCoord) => {
const [z, x, y] = tileCoord;
const matrix = matrixIds[z];
// console.log(GisApp.NGII_WMTS_URL.replace("{z}", matrix).replace("{x}", x).replace("{y}", y));
return GisApp.NGII_WMTS_URL
.replace("{z}", matrix)
.replace("{x}", x)
.replace("{y}", y);
return GisApp.getNgiiWmtsUrl(_layer, matrix, x, y);
});
return new TileLayer({
@@ -742,10 +742,13 @@ GisOlLayer.prototype = {
const extent = this.combineExtents(geometries.map(g => g?.getExtent()));
let map = GisApp.Module.core.map;
const targetZoom = 16;
const currentZoom = Number(map.getView().getZoom());
const maxZoom = Number.isFinite(currentZoom) && currentZoom > targetZoom ? currentZoom : targetZoom;
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16
maxZoom: maxZoom
});
}
} else {
@@ -132,13 +132,15 @@ GisOlSelect.prototype = {
const selectedLayer = e.target?.getLayer(feature) || feature.getLayer?.();
const clckZoom = Number(selectedLayer?.get('clckZoom'));
const targetZoom = Number.isFinite(clckZoom) && clckZoom > 0 ? clckZoom : 16;
const currentZoom = Number(map.getView().getZoom());
const maxZoom = Number.isFinite(currentZoom) && currentZoom > targetZoom ? currentZoom : targetZoom;
const geometry = feature.getGeometry();
if (geometry) {
const extent = geometry.getExtent();
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: targetZoom
maxZoom: maxZoom
});
}
}
@@ -343,12 +343,13 @@ GisOlStyle.prototype = {
//레이어 스타일 반환
getLayerStyle: function (layer, options) {
let gobjTy = layer.get('gobjTy');
if (!gobjTy) return;
if (!gobjTy || typeof this[gobjTy] !== 'function') return;
let style = this[gobjTy](layer, options);
return style;
},
getLayerStyleFunction: function (layer, options) {
let gobjTy = layer.get('gobjTy');
if (!gobjTy || typeof this[gobjTy] !== 'function') return;
let styleFunction = function (feature, resolution) {
return this[gobjTy](layer, options, feature, resolution);
}.bind(this);