feat: 검색 버튼(SearchModal) 위치 Gis 좌측 상단 버튼으로 변경, GisTab.js를 Gis Tree.js 내부로 통합
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import './lib/css/gis.common.css';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@common/components/ui/tabs';
|
||||
import OlMap from 'ol/Map';
|
||||
import View from 'ol/View';
|
||||
import { defaults as defaultControls, OverviewMap } from 'ol/control';
|
||||
@@ -10,13 +9,11 @@ import proj4 from 'proj4';
|
||||
import { register } from 'ol/proj/proj4';
|
||||
import GisApp from './lib/gis.core';
|
||||
import GisContent from './GisContent';
|
||||
import GisLonLat from './GisLonLat';
|
||||
import GisToolBar from './GisToolBar';
|
||||
import GisTab from './GisTab';
|
||||
import MapToggleBtn from './MapToggleBtn';
|
||||
import FeatureClickEventPopup from '@/map/featureClickEventPopup';
|
||||
import GisTree from './GisTree';
|
||||
import SearchModal from '@/map/SearchModal';
|
||||
|
||||
export const GisContext = createContext({gisCore: null, gisMap: null, treeData:null, checkedLayerIds:null});
|
||||
export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null });
|
||||
export const useGisContext = () => useContext(GisContext);
|
||||
|
||||
function Gis({ treeData = [], isSearch = false }) {
|
||||
@@ -24,7 +21,7 @@ function Gis({ treeData = [], isSearch = false }) {
|
||||
const [mapObject, setMapObject] = useState({});
|
||||
const [tabVisible, setTabVisible] = useState(true);
|
||||
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
|
||||
const [featureData,setFeatureData] = useState(null) // 선택된 피쳐 데이터
|
||||
const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터
|
||||
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
|
||||
const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []);
|
||||
const mapInstanceRef = useRef(null); // 맵 생성 여부를 추적할 ref
|
||||
@@ -181,7 +178,7 @@ function Gis({ treeData = [], isSearch = false }) {
|
||||
//setEditLayerItems(prev => [...layerItems])
|
||||
}, [treeData]);
|
||||
|
||||
const getLayerItems= (layerCode) => {
|
||||
const getLayerItems = (layerCode) => {
|
||||
const layerItems = Object.entries(layerCode)
|
||||
.filter(([key, obj], idx) => obj?.updtPsblYn === 'Y')
|
||||
.map(([key, obj]) => ({
|
||||
@@ -210,8 +207,8 @@ function Gis({ treeData = [], isSearch = false }) {
|
||||
<>
|
||||
<GisContext.Provider value={{ gisCore, gisMap: mapInstanceRef.current, treeData, checkedLayerIds }}>
|
||||
<div className="h-[calc(100vh-10rem)] flex relative">
|
||||
<GisTab visible={tabVisible} setVisible={setTabVisible} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} onFeatureSelect={handleFeatureSelect} />
|
||||
<GisContent onFeatureSelect={handleFeatureSelect} />
|
||||
<GisTree visible={tabVisible} setVisible={setTabVisible} />
|
||||
<GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} />
|
||||
{/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
|
||||
{isOpen && featureData && !isSearchModalOpen && (
|
||||
<FeatureClickEventPopup
|
||||
@@ -219,6 +216,11 @@ function Gis({ treeData = [], isSearch = false }) {
|
||||
onClose={closePopup} // 닫기 콜백 전달
|
||||
/>
|
||||
)}
|
||||
<SearchModal
|
||||
open={isSearchModalOpen}
|
||||
onOpenChange={setIsSearchModalOpen}
|
||||
onFeatureSelect={handleFeatureSelect}
|
||||
/>
|
||||
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
|
||||
{/* <GisLonLat/>
|
||||
<GisToolBar/>
|
||||
@@ -229,4 +231,4 @@ function Gis({ treeData = [], isSearch = false }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Gis);
|
||||
export default React.memo(Gis);
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import GisApp from './lib/gis.core';
|
||||
import { useGisContext } from './Gis';
|
||||
import GisLonLat from './GisLonLat';
|
||||
@@ -6,9 +6,9 @@ import GisSelect from './GisSelect';
|
||||
import GisToolBar from './GisToolBar';
|
||||
import './lib/css/gis.edit.css';
|
||||
import MapToggleBtn from './MapToggleBtn';
|
||||
import GisEdit from './GisEdit';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
export default function GisContent({ edit = false, onFeatureSelect }) {
|
||||
export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) {
|
||||
const { gisMap, gisCore } = useGisContext();
|
||||
const [visibleComponents, setVisibleComponents] = useState({});
|
||||
const addressResultLayer = useRef(null);
|
||||
@@ -16,12 +16,12 @@ export default function GisContent({ edit = false, onFeatureSelect }) {
|
||||
|
||||
//셀렉트 버튼 이벤트
|
||||
// const clickFeature = useCallback(feature => {
|
||||
// if (feature) {
|
||||
// const layer = feature.getLayer();
|
||||
// setVisibleComponents(prevState => ({
|
||||
// ...prevState,
|
||||
// }));
|
||||
// }
|
||||
// if (feature) {
|
||||
// const layer = feature.getLayer();
|
||||
// setVisibleComponents(prevState => ({
|
||||
// ...prevState,
|
||||
// }));
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
const selectRslt = useCallback(feature => {
|
||||
@@ -45,16 +45,35 @@ export default function GisContent({ edit = false, onFeatureSelect }) {
|
||||
}));
|
||||
}
|
||||
}, [gisMap]);
|
||||
|
||||
|
||||
if (!gisMap) return null;
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*<GisTree />*/}
|
||||
<MapToggleBtn />
|
||||
<GisLonLat />
|
||||
<GisToolBar />
|
||||
<GisSelect onFeatureSelect={onFeatureSelect}/>
|
||||
<GisSelect onFeatureSelect={onFeatureSelect} />
|
||||
|
||||
{/* 검색 모달 */}
|
||||
{isSearch && (
|
||||
<div className="absolute right-8 top-[254px] z-20">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSearchModalOpen(true)}
|
||||
className="overflow-hidden w-[40px] h-[40px] bg-white
|
||||
border border-[#9aa7b8]
|
||||
rounded
|
||||
shadow-[0_0_5px_#999]
|
||||
flex items-center justify-center
|
||||
p-0
|
||||
"
|
||||
>
|
||||
<Search size={24} className="text-gray-700" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,85 +1,85 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@common/components/ui/tabs';
|
||||
import GisTree from './GisTree';
|
||||
import GisSearch from './GisSearch';
|
||||
import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
|
||||
import SearchModal from '@/map/SearchModal';
|
||||
// import React, { useState } from 'react';
|
||||
// import { Tabs, TabsList, TabsTrigger } from '@common/components/ui/tabs';
|
||||
// import GisTree from './GisTree';
|
||||
// import GisSearch from './GisSearch';
|
||||
// import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
|
||||
// import SearchModal from '@/map/SearchModal';
|
||||
|
||||
|
||||
function GisTab({ visible, setVisible, isSearch, isSearchModalOpen, setIsSearchModalOpen, onFeatureSelect }) {
|
||||
const [activeTab, setActiveTab] = useState('layers');
|
||||
// function GisTab({ visible, setVisible, isSearch, isSearchModalOpen, setIsSearchModalOpen, onFeatureSelect }) {
|
||||
// const [activeTab, setActiveTab] = useState('layers');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`absolute top-0 left-0 w-[250px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-40
|
||||
${visible ? 'translate-x-0 opacity-100' : '-translate-x-full opacity-0 pointer-events-none'}`}
|
||||
>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col overflow-hidden">
|
||||
<TabsList className="flex-none mt-4 border-none rounded-none">
|
||||
<TabsTrigger value="layers" className="flex-1 h-10">레이어</TabsTrigger>
|
||||
{isSearch && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSearchModalOpen(true)}
|
||||
className="h-10 w-10 flex items-center justify-center px-0"
|
||||
>
|
||||
<Search size={18} className="text-gray-700" />
|
||||
</button>
|
||||
// <TabsTrigger value="search" className="flex-1 h-10 flex items-center justify-center">
|
||||
// <Search size={18} />
|
||||
// </TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
// return (
|
||||
// <>
|
||||
// <div
|
||||
// className={`absolute top-0 left-0 w-[250px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-40
|
||||
// ${visible ? 'translate-x-0 opacity-100' : '-translate-x-full opacity-0 pointer-events-none'}`}
|
||||
// >
|
||||
// <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col overflow-hidden">
|
||||
// <TabsList className="flex-none mt-4 border-none rounded-none">
|
||||
// <TabsTrigger value="layers" className="flex-1 h-10">레이어</TabsTrigger>
|
||||
// {/* {isSearch && (
|
||||
// <button
|
||||
// type="button"
|
||||
// onClick={() => setIsSearchModalOpen(true)}
|
||||
// className="h-10 w-10 flex items-center justify-center px-0"
|
||||
// >
|
||||
// <Search size={18} className="text-gray-700" />
|
||||
// </button>
|
||||
// // <TabsTrigger value="search" className="flex-1 h-10 flex items-center justify-center">
|
||||
// // <Search size={18} />
|
||||
// // </TabsTrigger>
|
||||
// )} */}
|
||||
// </TabsList>
|
||||
|
||||
{/* GisTree는 항상 마운트되며, 탭 상태에 따라 보여주기만 함 */}
|
||||
<div
|
||||
className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${activeTab === 'layers' ? 'block' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<GisTree />
|
||||
</div>
|
||||
// {/* GisTree는 항상 마운트되며, 탭 상태에 따라 보여주기만 함 */}
|
||||
// <div
|
||||
// className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${activeTab === 'layers' ? 'block' : 'hidden'
|
||||
// }`}
|
||||
// >
|
||||
// <GisTree />
|
||||
// </div>
|
||||
|
||||
{/* GisSearch도 동일하게 처리 */}
|
||||
{/* {isSearch && (
|
||||
<div
|
||||
className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${
|
||||
activeTab === 'search' ? 'block' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
<GisSearch />
|
||||
</div>
|
||||
)} */}
|
||||
// {/* GisSearch도 동일하게 처리 */}
|
||||
// {/* {isSearch && (
|
||||
// <div
|
||||
// className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${
|
||||
// activeTab === 'search' ? 'block' : 'hidden'
|
||||
// }`}
|
||||
// >
|
||||
// <GisSearch />
|
||||
// </div>
|
||||
// )} */}
|
||||
|
||||
|
||||
{/* 🔍 검색 모달 */}
|
||||
<SearchModal
|
||||
open={isSearchModalOpen}
|
||||
onOpenChange={setIsSearchModalOpen}
|
||||
onFeatureSelect={onFeatureSelect}
|
||||
/>
|
||||
</Tabs>
|
||||
// {/* 🔍 검색 모달 */}
|
||||
// {/* <SearchModal
|
||||
// open={isSearchModalOpen}
|
||||
// onOpenChange={setIsSearchModalOpen}
|
||||
// onFeatureSelect={onFeatureSelect}
|
||||
// /> */}
|
||||
// </Tabs>
|
||||
|
||||
{/* 접기 버튼 */}
|
||||
<button
|
||||
onClick={() => setVisible(false)}
|
||||
className="absolute top-1/2 right-[-3px] transform -translate-y-1/2 z-50 bg-white border border-l border-gray-300 rounded-l-none rounded-r px-1 py-2 shadow-sm"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
</div>
|
||||
// {/* 접기 버튼 */}
|
||||
// <button
|
||||
// onClick={() => setVisible(false)}
|
||||
// className="absolute top-1/2 right-[-3px] transform -translate-y-1/2 z-50 bg-white border border-l border-gray-300 rounded-l-none rounded-r px-1 py-2 shadow-sm"
|
||||
// >
|
||||
// <ChevronLeft size={16} />
|
||||
// </button>
|
||||
// </div>
|
||||
|
||||
{/* 열기 버튼 */}
|
||||
{!visible && (
|
||||
<button
|
||||
onClick={() => setVisible(true)}
|
||||
className="absolute top-1/2 left-[-3px] transform -translate-y-1/2 z-50 bg-white border border-r border-gray-300 rounded-r px-1 py-2 shadow-sm"
|
||||
>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
// {/* 열기 버튼 */}
|
||||
// {!visible && (
|
||||
// <button
|
||||
// onClick={() => setVisible(true)}
|
||||
// className="absolute top-1/2 left-[-3px] transform -translate-y-1/2 z-50 bg-white border border-r border-gray-300 rounded-r px-1 py-2 shadow-sm"
|
||||
// >
|
||||
// <ChevronRight size={16} />
|
||||
// </button>
|
||||
// )}
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
||||
export default React.memo(GisTab);
|
||||
// export default React.memo(GisTab);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import GisApp from './lib/gis.core';
|
||||
import { util } from './lib/util';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useGisContext } from './Gis';
|
||||
import { DndProvider } from "react-dnd";
|
||||
@@ -8,14 +7,14 @@ import {
|
||||
MultiBackend,
|
||||
getBackendOptions
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@common/components/ui/tabs";
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => {} }) {
|
||||
export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible }) {
|
||||
const { gisCore, treeData } = useGisContext();
|
||||
const [checked, setChecked] = useState([]);
|
||||
const [expanded, setExpanded] = useState([]);
|
||||
const [nodes, setNodes] = useState([]);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('layers');
|
||||
|
||||
useEffect(() => {
|
||||
if (treeData && treeData.length > 0) {
|
||||
@@ -115,7 +114,7 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
|
||||
};
|
||||
|
||||
const makeLegendSrc = layerinfo => {
|
||||
if(!layerinfo.gobjTy) return null;
|
||||
if (!layerinfo.gobjTy) return null;
|
||||
|
||||
let src;
|
||||
if (layerinfo.iconNm) {
|
||||
@@ -176,34 +175,61 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
|
||||
|
||||
return (
|
||||
<>
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<Tree
|
||||
tree={nodes}
|
||||
rootId={null}
|
||||
initialOpen={expanded}
|
||||
onOpenChange={setExpanded}
|
||||
render={(node, { depth, isOpen, onToggle }) => (
|
||||
<div
|
||||
className="flex items-center gap-2 py-1 px-2 hover:bg-gray-100 rounded"
|
||||
style={{ marginInlineStart: depth * 16 + (!node.droppable ? 16 : 0) }}
|
||||
>
|
||||
{node.droppable && (
|
||||
<i onClick={onToggle} className={`diveicon di-arrow-forward mr-2 text-base text-label ${isOpen ? 'rotate-90' : ''}`}
|
||||
title={isOpen ? '닫기' : '열기'}>
|
||||
</i>
|
||||
)}
|
||||
<input type="checkbox" checked={checked.includes(node.id)} onChange={() => handleCheck(node.id)} className="cursor-pointer"/>
|
||||
{node.icon && <span>{node.icon}</span>}
|
||||
<span className="text-[1.3rem] font-medium text-label">{node.text}</span>
|
||||
</div>
|
||||
)}
|
||||
sort={(a, b) => {
|
||||
const aOrder = parseInt(a.data?.sortOrdr ?? '0');
|
||||
const bOrder = parseInt(b.data?.sortOrdr ?? '0');
|
||||
return aOrder - bOrder;
|
||||
}}
|
||||
/>
|
||||
</DndProvider>
|
||||
<div
|
||||
className={`absolute top-0 left-0 w-[250px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-40
|
||||
${visible ? 'translate-x-0 opacity-100' : '-translate-x-full opacity-0 pointer-events-none'}`}
|
||||
>
|
||||
<div
|
||||
className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${activeTab === 'layers' ? 'block' : 'hidden'}`}
|
||||
>
|
||||
<h3 className="text-3xl text-gray-600 text-bold font-bold mx-2 my-4 ">레이어</h3>
|
||||
<hr className="mb-4" />
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<Tree
|
||||
tree={nodes}
|
||||
rootId={null}
|
||||
initialOpen={expanded}
|
||||
onOpenChange={setExpanded}
|
||||
render={(node, { depth, isOpen, onToggle }) => (
|
||||
<div
|
||||
className="flex items-center gap-2 py-1 px-2 hover:bg-gray-100 rounded"
|
||||
style={{ marginInlineStart: depth * 16 + (!node.droppable ? 16 : 0) }}
|
||||
>
|
||||
{node.droppable && (
|
||||
<i onClick={onToggle} className={`diveicon di-arrow-forward mr-2 text-base text-label ${isOpen ? 'rotate-90' : ''}`}
|
||||
title={isOpen ? '닫기' : '열기'}>
|
||||
</i>
|
||||
)}
|
||||
<input type="checkbox" checked={checked.includes(node.id)} onChange={() => handleCheck(node.id)} className="cursor-pointer" />
|
||||
{node.icon && <span>{node.icon}</span>}
|
||||
<span className="text-[1.3rem] font-medium text-label">{node.text}</span>
|
||||
</div>
|
||||
)}
|
||||
sort={(a, b) => {
|
||||
const aOrder = parseInt(a.data?.sortOrdr ?? '0');
|
||||
const bOrder = parseInt(b.data?.sortOrdr ?? '0');
|
||||
return aOrder - bOrder;
|
||||
}}
|
||||
/>
|
||||
</DndProvider>
|
||||
</div>
|
||||
{/* 접기 버튼 */}
|
||||
<button
|
||||
onClick={() => setVisible(false)}
|
||||
className="absolute top-1/2 right-[-3px] transform -translate-y-1/2 z-50 bg-white border border-l border-gray-300 rounded-l-none rounded-r px-1 py-2 shadow-sm"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{/* 열기 버튼 */}
|
||||
{!visible && (
|
||||
<button
|
||||
onClick={() => setVisible(true)}
|
||||
className="absolute top-1/2 left-[-3px] transform -translate-y-1/2 z-50 bg-white border border-r border-gray-300 rounded-r px-1 py-2 shadow-sm"
|
||||
>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user