diff --git a/apps/sn/app/map/page.tsx b/apps/sn/app/map/page.tsx
new file mode 100644
index 000000000..7185c9660
--- /dev/null
+++ b/apps/sn/app/map/page.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import Gis, { useGisContext } from "@common/business_components/ui/map/Gis";
+import { mapService } from "@service";
+import { useCallback, useEffect, useRef, useState } from "react";
+import GisApp from "@common/business_components/ui/map/lib/gis.core";
+import SearchModal from "./SearchModal";
+import FeatureClickEventPopup from "./featureClickEventPopup";
+
+const Map = () => {
+ const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
+ const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태
+ const gisMaps = useRef(null);
+ const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
+ const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
+ const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
+ const fetchData = async () => {
+ try {
+ const response = await mapService.getLyrStngs();
+ setTreeData(response.data); // API 응답 저장
+ } catch (error) {
+ console.error("데이터 불러오기 실패:", error);
+ }
+ };
+
+ // 컴포넌트 마운트 시 1회 실행
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ // SearchModal, GisContent 등에서 호출
+ const onFeatureSelect = (data: any) => {
+ let map = gisMaps.current;
+ if (!map) {
+ console.warn("지도가 초기화되지 않았습니다.");
+ return;
+ }
+
+ // 항상 같은 select 객체 사용
+ const select = GisApp.Module.select as any;
+ if (!select) return;
+
+ // 1. 기존 선택 초기화
+ select.resetSelectedFeature();
+
+ // 2. data가 OpenLayers Feature 인스턴스인지 확인
+ let foundFeature = null;
+ if (
+ data &&
+ typeof data.getGeometry === "function" &&
+ typeof data.setStyle === "function"
+ ) {
+ // 이미 Feature 인스턴스라면 그대로 사용
+ foundFeature = data;
+ } else {
+ // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
+ const featureId = data.fid || data.ftrIdn || data.id;
+ map
+ .getLayers()
+ .getArray()
+ .forEach((layer: any) => {
+ const source = layer.getSource?.();
+ if (source?.getFeatureById) {
+ const feature = source.getFeatureById(String(featureId).trim());
+ if (feature) foundFeature = feature;
+ }
+ });
+ }
+
+ // 3. 피처 선택
+ if (foundFeature) {
+ let geometry = foundFeature.getGeometry();
+ if (geometry) {
+ let extent = geometry.getExtent();
+
+ map.getView().fit(extent, {
+ duration: 300,
+ padding: [50, 50, 50, 50],
+ maxZoom: 16,
+ constrainResolution: true,
+ nearest: true,
+ });
+ }
+ select.selectFeature(foundFeature, true);
+ } else {
+ console.warn("OpenLayers Feature를 찾을 수 없습니다:", data);
+ }
+
+ setFeatureData(data);
+ setIsOpen(true);
+ setIsSearchModalOpen(false);
+ };
+ return (
+
+
+
+ );
+};
+
+export default Map;
diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js
new file mode 100644
index 000000000..4fd9a8eb4
--- /dev/null
+++ b/packages/common/business_components/src/ui/map/Gis.js
@@ -0,0 +1,246 @@
+import './lib/css/gis.common.css';
+import React, { createContext, useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react';
+import OlMap from 'ol/Map';
+import View from 'ol/View';
+import { defaults as defaultControls, OverviewMap } from 'ol/control';
+import { defaults } from 'ol/interaction';
+import * as olProj from 'ol/proj';
+import proj4 from 'proj4';
+import { register } from 'ol/proj/proj4';
+import GisApp from './lib/gis.core';
+import GisContent from './GisContent';
+import GisTree from './GisTree';
+import GisLonLat from './GisLonLat';
+import GisSelect from './GisSelect';
+import GisToolBar from './GisToolBar';
+import GisMapToggle from './GisMapToggle';
+import { Search } from 'lucide-react';
+
+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 }) {
+ const mapRef = useRef();
+ const [mapObject, setMapObject] = useState({});
+ const [tabVisible, setTabVisible] = useState(true);
+ // const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
+ // const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터
+ // const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
+ const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []);
+ const mapInstanceRef = useRef(null);
+ const checkedLayerIds = useRef([]);
+ let gisMap = null;
+ const closePopup = () => {
+ setIsOpen(false); // 팝업 닫기
+ setFeatureData(null)
+ };
+
+ // SearchModal, GisContent 등에서 호출
+ const handleFeatureSelect = useCallback((data) => {
+ gisMap = mapInstanceRef.current;
+
+ if (!gisMap) {
+ console.warn('지도가 초기화되지 않았습니다.');
+ return;
+ }
+
+ // 항상 같은 select 객체 사용
+ const select = GisApp.Module.select;
+ if (!select) return;
+
+ // 1. 기존 선택 초기화
+ select.resetSelectedFeature();
+
+ // 2. data가 OpenLayers Feature 인스턴스인지 확인
+ let foundFeature = null;
+ if (data && typeof data.getGeometry === 'function' && typeof data.setStyle === 'function') {
+ // 이미 Feature 인스턴스라면 그대로 사용
+ foundFeature = data;
+ } else {
+ // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
+ const featureId = data.fid || data.ftrIdn || data.id;
+ gisMap.getLayers().getArray().forEach(layer => {
+ const source = layer.getSource?.();
+ if (source?.getFeatureById) {
+ const feature = source.getFeatureById(String(featureId).trim());
+ if (feature) foundFeature = feature;
+ }
+ });
+ }
+
+ // 3. 피처 선택
+ if (foundFeature) {
+ let geometry = foundFeature.getGeometry();
+ if (geometry) {
+ let extent = geometry.getExtent();
+
+ gisMap.getView().fit(extent, {
+ duration: 300,
+ padding: [50, 50, 50, 50],
+ maxZoom: 16,
+ constrainResolution: true,
+ nearest: true,
+ });
+ }
+ select.selectFeature(foundFeature, true);
+ } else {
+ console.warn('OpenLayers Feature를 찾을 수 없습니다:', data);
+ }
+
+ setFeatureData(data);
+ setIsOpen(true);
+ setIsSearchModalOpen(false);
+ }, [mapInstanceRef]);
+
+ useEffect(() => {
+ if (!mapRef.current) return;
+ if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단
+
+ // EPSG:5179 등록
+ proj4.defs([
+ [
+ 'EPSG:5179',
+ '+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs',
+ ],
+ ]);
+ register(proj4);
+
+ //맵 생성
+ const map = new OlMap({
+ layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']],
+ renderer: 'webgl',
+ target: mapRef.current,
+ interactions: defaults({ doubleClickZoom: false }),
+ controls: defaultControls({
+ zoom: false,
+ rotate: false,
+ attributionOptions: {
+ collapsible: false,
+ },
+ }).extend([]),
+ view: new View({
+ projection: GisApp.TO_PROJ,
+ zoom: gisCore.defaults.zoom,
+ center: olProj.transform(
+ [126.6757, 37.5585],
+ GisApp.FROM_PROJ,
+ GisApp.TO_PROJ
+ ),
+ minZoom: gisCore.defaults.minZoom,
+ maxZoom: gisCore.defaults.maxZoom,
+ }),
+ });
+
+ gisMaps.current = map
+
+ //미니맵
+ if(isMiniMap){
+ const overviewMap = new OverviewMap({
+ layers: [GisApp.Module.core.baseMiniMapLayers['base']],
+ view: new View({
+ projection: GisApp.TO_PROJ,
+ minZoom: 9,
+ }),
+ collapsed: false,
+ collapseLabel: '\u00BB',
+ label: '\u00AB',
+ });
+ map.addControl(overviewMap);
+ }
+
+ gisCore.__proto__.map = map;
+ gisCore.scaleControl();
+ gisCore.changeBaseLayer('base');
+ setMapObject({ map: map });
+ mapInstanceRef.current = map; // 생성 완료 플래그 설정
+ }, []);
+
+ useEffect(() => {
+ if (!treeData || treeData.length === 0) return;
+
+ gisCore.deleteAllOverlay();
+ gisCore.deleteAllLayer();
+
+ const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn');
+ GisApp.layerCode = layerCode;
+
+ //setTreeData(data);
+ const layerItems = getLayerItems(layerCode);
+ //setEditLayerItems(prev => [...layerItems])
+ }, [treeData]);
+
+ const getLayerItems = (layerCode) => {
+ const layerItems = Object.entries(layerCode)
+ .filter(([key, obj], idx) => obj?.updtPsblYn === 'Y')
+ .map(([key, obj]) => ({
+ ...obj,
+ name: obj.lyrNm,
+ label: obj.lyrNm,
+ value: obj.lyrSn + '',
+ }))
+ .sort((a, b) => {
+ if (a.sortOrdr > b.sortOrdr) return 1;
+ if (a.sortOrdr < b.sortOrdr) return -1;
+ return 0;
+ });
+ return layerItems;
+ };
+
+ if (mapObject.map) {
+ try {
+ gisMap = mapObject.map;
+ } catch (e) {
+ console.error('오류 발생:', e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+ {/*
*/}
+
+ {/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
+ {isOpen && featureData && !isSearchModalOpen && (
+
+ )}
+
+
+
+
+
+
+
+ {isSearch && (
+
+
+
+ )}
+
+ >
+ );
+}
+
+export default React.memo(Gis);
\ No newline at end of file
diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js
new file mode 100644
index 000000000..136f394ad
--- /dev/null
+++ b/packages/common/business_components/src/ui/map/GisContent.js
@@ -0,0 +1,79 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import GisApp from './lib/gis.core';
+import { useGisContext } from './Gis';
+import GisLonLat from './GisLonLat';
+import GisSelect from './GisSelect';
+import GisToolBar from './GisToolBar';
+import './lib/css/gis.edit.css';
+import GisMapToggle from './GisMapToggle';
+import { Search } from 'lucide-react';
+
+export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) {
+ const { gisMap, gisCore } = useGisContext();
+ const [visibleComponents, setVisibleComponents] = useState({});
+ const addressResultLayer = useRef(null);
+ const [playShow, setPlayShow] = useState({ show: false });
+
+ //셀렉트 버튼 이벤트
+ // const clickFeature = useCallback(feature => {
+ // if (feature) {
+ // const layer = feature.getLayer();
+ // setVisibleComponents(prevState => ({
+ // ...prevState,
+ // }));
+ // }
+ // }, []);
+
+ const selectRslt = useCallback(feature => {
+ GisApp.Module.layer.highLightFeature(
+ [feature],
+ {
+ id: GisApp.hlLayerId,
+ zIndex: 10,
+ opacity: 1,
+ selectable: true,
+ },
+ { width: 30 }
+ );
+ clickFeature(feature);
+ }, []);
+
+ useEffect(() => {
+ if (gisMap) {
+ setVisibleComponents(prevState => ({
+ ...prevState,
+ }));
+ }
+ }, [gisMap]);
+
+ if (!gisMap) return null;
+
+ return (
+ <>
+ {/**/}
+ {/*
+
+ */}
+ {/* */}
+
+ {/* 검색 모달 */}
+ {isSearch && (
+
+
+
+ )}
+ >
+ );
+}
\ No newline at end of file