지도 작업 중1

This commit is contained in:
yschoi
2025-08-01 14:13:19 +09:00
parent 96ef328fbf
commit d4c115d7ca
34 changed files with 533 additions and 368 deletions
@@ -233,6 +233,7 @@ const Map = () => {
<>
<div className="bg-container">
<Gis
height='h-screen'
treeData={treeData}
isSearch={false}
gisMaps={gisMaps}
@@ -1,8 +1,6 @@
"use client";
import { useDmStore } from "@/app/store";
import {
SearchDiv,
SearchFormLeft,
SelectBox,
type SelectDataType,
} from "@common/business_components/ui";
@@ -48,6 +46,7 @@ export function SearchBar({
},
[Common.selectSigunData]
);
const onSelectValue = useCallback(
(val: string, type: string) => {
switch (type) {
@@ -62,78 +61,58 @@ export function SearchBar({
[initializeSigunData]
);
// useEffect(() => {
// console.log("map.ruseFcltList ", map.ruseFcltList);
// }, [map.ruseFcltList]);
return (
<>
<div className="grid grid-cols-4 gap-4 ">
<div className="col-span-4">
<SearchDiv>
<SearchFormLeft>
<div className="col-span-10 flex flex-row">
<SelectBox
className="col-span-2 w-1/6 h-full text-2xl"
label="시도"
selectClass={""}
selectArray={Common.selectSidoData || []}
onSelectValue={(val) =>
onSelectValue(val.toString(), "ctpvCd")
}
value={sidoValue || Common.selectSidoData[0]?.val || ""}
/>
<SelectBox
className="col-span-2 w-1/6 h-full text-2xl ml-10"
label="시군구"
selectClass={""}
selectArray={filteredSigunData || []}
onSelectValue={(val) =>
onSelectValue(val.toString(), "sggCd")
}
value={sigunValue}
/>
<div className="col-span-6 flex flex-row items-center min-w-[27rem] ml-10">
<Label className="text-xl justify-self-center mt-2 mx-3">
하수재이용시설
</Label>
<SearchInputText
label={"시설명"}
initValue={undefined}
setValue={(searchNm?: string) => {
let sggOrCity = "";
if (searchNm) {
if (sigunValue === "0" || sigunValue === "") {
sggOrCity = sidoValue;
} else {
sggOrCity = sigunValue;
}
findRuseFclt(sggOrCity, searchNm);
}
}}
setRtnData={(rtnData: any) => {
setHasuPk(rtnData);
}}
searchList={map.ruseFcltList || []}
/>
</div>
<div className="flex flex-row space-x-2">
<Button
color="sky"
className="mt-2"
size={"sm"}
onClick={() => {
onApply();
}}
>
적용
</Button>
</div>
</div>
</SearchFormLeft>
</SearchDiv>
</div>
<div className="flex justify-center w-full px-4 py-3">
<div className="flex items-center w-full max-w-screen-lg rounded-xl bg-white shadow-md px-6 py-4 space-x-3">
<SelectBox
labelClass="w-[40px]"
className="w-[200px]"
label="시도"
selectClass="rounded-lg bg-white"
selectArray={Common.selectSidoData || []}
onSelectValue={(val) => onSelectValue(val.toString(), "ctpvCd")}
value={sidoValue || Common.selectSidoData[0]?.val || ""}
/>
<SelectBox
labelClass="w-[65px]"
className="w-[200px]"
label="시군구"
selectClass="rounded-lg bg-white"
selectArray={filteredSigunData || []}
onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")}
value={sigunValue}
/>
<Label className="text-gray-600">하수재이용시설</Label>
<SearchInputText
label="시설명"
initValue={undefined}
setValue={(searchNm?: string) => {
let sggOrCity = "";
if (searchNm) {
if (sigunValue === "0" || sigunValue === "") {
sggOrCity = sidoValue;
} else {
sggOrCity = sigunValue;
}
findRuseFclt(sggOrCity, searchNm);
}
}}
setRtnData={(rtnData: any) => {
setHasuPk(rtnData);
}}
searchList={map.ruseFcltList || []}
/>
<Button
color="sky"
className="ml-2 text-white"
size="sm"
onClick={() => {
onApply();
}}
>
적용
</Button>
</div>
</>
</div>
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1016 B

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 923 B

+4 -12
View File
@@ -3,6 +3,7 @@
import Gis from "@common/business_components/ui/map/Gis";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { useEffect, useRef, useState } from "react";
import { SearchBar } from "./searchBar/searchBar";
interface FeatureData {
fid?: string;
@@ -123,22 +124,13 @@ const Map = () => {
setIsSearchModalOpen(false);
};
// onCheckedLayersChange handler
const handleCheckedLayersChange = (
checkedIds: string[],
checkedNodesData: unknown[]
) => {
// Handle checked layers change if needed
console.log("Checked layers changed:", checkedIds, checkedNodesData);
};
return (
<div className="bg-container">
<Gis
treeData={treeData}
isSearch={false}
gisMaps={gisMaps}
isMiniMap={true}
isMiniMap={false}
searchView={null}
detailView={null}
onFeatureSelect={onFeatureSelect}
@@ -148,9 +140,9 @@ const Map = () => {
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
onCheckedLayersChange={undefined}
imgVisble={false}
searchBar={undefined}
searchBar={(props:any) => <SearchBar onFeatureSelect={onFeatureSelect} gisMaps={gisMaps} />}
/>
</div>
);
@@ -0,0 +1,158 @@
"use client";
import { SelectBox, type SelectDataType } from "@common/business_components/ui";
import { Button } from "@common/components/ui";
import { useCallback, useEffect, useState } from "react";
interface SearchBarProps {
onFeatureSelect: (feature: any) => void;
gisMaps: React.RefObject<any>;
}
export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) {
const [sidoValue, setSidoValue] = useState("0");
const [sigunValue, setSigunValue] = useState("0");
const [sidoData, setSidoData] = useState<SelectDataType[]>([{ text: "선택", val: "0" }]);
const [sigunData, setSigunData] = useState<SelectDataType[]>([{ text: "선택", val: "0" }]);
const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([{ text: "선택", val: "0" }]);
const [facilityData, setFacilityData] = useState<SelectDataType[]>([{ text: "선택", val: "0" }]);
const [facilityValue, setFacilityValue] = useState("0");
const fetchSido = async () => {
const url = `http://rsone.iptime.org:10010/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_ctpv&outputFormat=application/json&propertyName=mctpv_korn_nm,mctpv_cd`;
const res = await fetch(url);
const json = await res.json();
const list = json.features.map((f: any) => ({
text: f.properties.mctpv_korn_nm,
val: f.properties.mctpv_cd,
}));
setSidoData([{ text: "선택", val: "0" }, ...list]);
};
const fetchSigun = async () => {
const url = `http://rsone.iptime.org:10010/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_sgg&outputFormat=application/json&propertyName=sgg_cd,sgg_korn_nm`;
const res = await fetch(url);
const json = await res.json();
const list = json.features.map((f: any) => ({
text: f.properties.sgg_korn_nm,
val: f.properties.sgg_cd,
}));
setSigunData(list);
};
const fetchFacilitiesBySggNm = async (sggNm: string) => {
const encodedSggNm = encodeURIComponent(sggNm);
const url = `http://rsone.iptime.org:10010/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_il_swage_prcs_fclt&outputFormat=application/json&cql_filter=sgg_nm='${encodedSggNm}'`;
const res = await fetch(url);
const json = await res.json();
const list = json.features.map((f: any) => ({
text: f.properties.swtrmt_fclt_nm ?? "(이름없음)",
val: f.id, // f.properties.swtrmt_fclt_cd 도 가능
}));
setFacilityData([{ text: "선택", val: "0" }, ...list]);
setFacilityValue("0");
};
useEffect(() => {
fetchSido();
fetchSigun();
}, []);
const initializeSigunData = useCallback(
(sidoCd: string) => {
if (sidoCd === "0") {
setFilteredSigunData([{ text: "선택", val: "0" }]);
setSigunValue("0");
return;
}
const prefix = sidoCd.substring(0, 2);
const filtered = sigunData.filter((item) => item.val.startsWith(prefix));
setFilteredSigunData([{ text: "선택", val: "0" }, ...filtered]);
setSigunValue("0");
},
[sigunData]
);
const onSelectValue = useCallback(
(val: string, type: string) => {
if (type === "ctpvCd") {
setSidoValue(val);
initializeSigunData(val);
setFacilityData([{ text: "선택", val: "0" }]);
} else if (type === "sggCd") {
setSigunValue(val);
const sggNm = sigunData.find((item) => item.val === val)?.text || "";
if (sggNm) fetchFacilitiesBySggNm(sggNm);
} else if (type === "fcltCd") {
setFacilityValue(val);
}
},
[initializeSigunData, sigunData]
);
const handleSearch = () => {
if (!gisMaps.current || facilityValue === "0") return;
const layers = gisMaps.current.getLayers().getArray();
for (const layer of layers) {
const source = layer.getSource?.();
const feature = source?.getFeatureById?.(facilityValue);
if (feature) {
onFeatureSelect(feature);
return;
}
}
alert("해당 처리시설을 지도에서 찾을 수 없습니다.");
};
return (
<div className="grid grid-cols-4 gap-4">
<div className="col-span-4">
<div className="card card-border flex">
<div className="col-span-10 flex flex-row">
<SelectBox
labelClass="w-[40px]"
className="mr-10 w-[200px]"
selectClass="rounded-lg bg-white"
label="시도"
selectArray={sidoData}
onSelectValue={(val) => onSelectValue(val.toString(), "ctpvCd")}
value={sidoValue}
/>
<SelectBox
labelClass="w-[65px]"
className="mr-10 w-[200px]"
selectClass="rounded-lg bg-white"
label="시군구"
selectArray={filteredSigunData}
onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")}
value={sigunValue}
/>
<SelectBox
labelClass="w-[140px]"
className="mr-3 w-[250px]"
selectClass="rounded-lg bg-white"
label="하수처리시설"
selectArray={facilityData}
onSelectValue={(val) => onSelectValue(val.toString(), "fcltCd")}
value={facilityValue}
/>
</div>
<div className="flex flex-row space-x-2">
<Button
color="primary"
className="mx-2 mt-1 ml-10"
size={"sm"}
onClick={handleSearch}
>
조회
</Button>
</div>
</div>
</div>
</div>
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1016 B

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1016 B

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 923 B

@@ -1,321 +1,356 @@
import { Search } from 'lucide-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 { register } from 'ol/proj/proj4';
import proj4 from 'proj4';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import GisMapToggle from './GisMapToggle';
import GisSelect from './GisSelect';
import GisToolBar from './GisToolBar';
import GisTree from './GisTree';
import './lib/css/gis.common.css';
import GisApp from './lib/gis.core';
import { Search } from "lucide-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 { register } from "ol/proj/proj4";
import proj4 from "proj4";
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import GisMapToggle from "./GisMapToggle";
import GisSelect from "./GisSelect";
import GisToolBar from "./GisToolBar";
import GisTree from "./GisTree";
import "./lib/css/gis.common.css";
import GisApp from "./lib/gis.core";
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);
export const searchUpperPpls = async (fcltMngNo) => {
const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || '';
const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
method: "GET",
});
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
method: "GET",
});
let data = null;
if (res.ok) {
try {
// 응답이 비어있는지 확인
const contentLength = res.headers.get("content-length");
if (contentLength === "0")
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
let data = null;
if (res.ok) {
try {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === "")
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
// JSON 파싱 시도
data = JSON.parse(text);
} catch (error) {
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
}
}
// JSON 파싱 시도
data = JSON.parse(text);
} catch (error) {
console.error('JSON parsing error:', error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
}
}
const select = GisApp.Module.select;
const gisMap = GisApp.Module.core?.map;
let foundFeature = null;
const select = GisApp.Module.select;
data.forEach((item) => {
const pplMngNo = item.ppl_mng_no.trim();
const gisMap = GisApp.Module.core?.map;
gisMap
.getLayers()
.getArray()
.filter((e) => e.get("lyrTyNm") == "tb_ms_swage_ppl")
.forEach((layer) => {
const source = layer.getSource?.();
if (source?.getFeatures) {
const features = source.getFeatures();
for (const f of features) {
if (f.values_.fclt_mng_no.trim() === pplMngNo) {
foundFeature = f;
break;
}
}
}
});
let foundFeature = null;
if (foundFeature) select.selectMultiFeature(foundFeature, true);
});
data.forEach(item => {
const pplMngNo = item.ppl_mng_no.trim();
if (!foundFeature) alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
gisMap.getLayers().getArray().filter(e => e.get("lyrTyNm") == 'tb_ms_swage_ppl').forEach(layer => {
const source = layer.getSource?.();
if (source?.getFeatures) {
const features = source.getFeatures();
for (const f of features) {
if (f.values_.fclt_mng_no.trim() === pplMngNo) {
foundFeature = f;
break;
}
}
}
});
let geometry = foundFeature?.getGeometry();
if (geometry) {
let extent = geometry.getExtent();
if (foundFeature) select.selectMultiFeature(foundFeature, true);
});
if (!foundFeature) alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
let geometry = foundFeature?.getGeometry();
if (geometry) {
let extent = geometry.getExtent();
gisMap.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 14,
constrainResolution: true,
nearest: true,
});
}
gisMap.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 14,
constrainResolution: true,
nearest: true,
});
}
};
function Gis({
isOpen, //우측 상세 레이어 팝업
gisMaps,
isSearchModalOpen,
treeData = [],
isSearch = false,
isMiniMap = false,
setIsOpen,
setFeatureData,
featureData,
imgVisble = false,
searchView: SearchView,
detailView: DetailView,
onFeatureSelect,
setIsSearchModalOpen,
onCheckedLayersChange,
searchBar: SearchBar
isOpen, //우측 상세 레이어 팝업
gisMaps,
isSearchModalOpen,
treeData = [],
isSearch = false,
isMiniMap = false,
setIsOpen,
setFeatureData,
featureData,
imgVisble = false,
searchView: SearchView,
detailView: DetailView,
onFeatureSelect,
setIsSearchModalOpen,
onCheckedLayersChange,
searchBar: SearchBar,
height:height="h-[calc(100vh-10rem)]",
}) {
const mapRef = useRef();
const [mapObject, setMapObject] = useState({});
const [tabVisible, setTabVisible] = useState(true);
const [layerNm, setLayerNm] = useState('')
// 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([]);
const layerNmRef = useRef('')
const mapRef = useRef();
const [mapObject, setMapObject] = useState({});
const [tabVisible, setTabVisible] = useState(true);
const [layerNm, setLayerNm] = useState("");
// 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([]);
const layerNmRef = useRef("");
// 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가
// const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]);
// const facilityOverlaysRef = useRef([]);
// 체크된 레이어 상태 관리
const [checkedLayers, setCheckedLayers] = useState([]);
const [checkedLayersData, setCheckedLayersData] = useState([]);
// 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가
// const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]);
// const facilityOverlaysRef = useRef([]);
// 체크된 레이어 상태 관리
const [checkedLayers, setCheckedLayers] = useState([]);
const [checkedLayersData, setCheckedLayersData] = useState([]);
let gisMap = null;
const closePopup = () => {
setIsOpen(false); // 팝업 닫기
setFeatureData(null)
};
let gisMap = null;
const closePopup = () => {
setIsOpen(false); // 팝업 닫기
setFeatureData(null);
};
// Feature Click Event
const handleFeatureSelect = useCallback((data) => {
onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출
}, [mapInstanceRef]);
// Feature Click Event
const handleFeatureSelect = useCallback(
(data) => {
onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출
},
[mapInstanceRef],
);
// 지도 생성 및 초기화
useEffect(() => {
if (!mapRef.current) return;
if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단
// 지도 생성 및 초기화
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);
// 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);
// OpenLayers 지도 객체 생성
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(
[127.4875, 37.4916],
GisApp.FROM_PROJ,
GisApp.TO_PROJ
),
minZoom: gisCore.defaults.minZoom,
maxZoom: gisCore.defaults.maxZoom,
}),
});
// OpenLayers 지도 객체 생성
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(
[127.4875, 37.4916],
GisApp.FROM_PROJ,
GisApp.TO_PROJ,
),
minZoom: gisCore.defaults.minZoom,
maxZoom: gisCore.defaults.maxZoom,
}),
});
gisMaps.current = map
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);
}
// 미니맵 추가
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; // 생성 완료 플래그 설정
// 스케일 컨트롤 및 베이스 레이어 설정
gisCore.__proto__.map = map;
gisCore.scaleControl();
gisCore.changeBaseLayer("base");
setMapObject({ map: map });
mapInstanceRef.current = map; // 생성 완료 플래그 설정
}, []);
}, []);
// treeData 변경 시 레이어 코드 생성
useEffect(() => {
if (!treeData || treeData.length === 0) return;
// treeData 변경 시 레이어 코드 생성
useEffect(() => {
if (!treeData || treeData.length === 0) return;
// 기존 오버레이 및 레이어 모두 삭제
gisCore.deleteAllOverlay();
gisCore.deleteAllLayer();
// 기존 오버레이 및 레이어 모두 삭제
gisCore.deleteAllOverlay();
gisCore.deleteAllLayer();
// 트리 데이터를 레이어 코드 객체로 변환
const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, "lyrSn");
GisApp.layerCode = layerCode; // 전역 레이어 코드 설정
// 트리 데이터를 레이어 코드 객체로 변환
const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn');
GisApp.layerCode = layerCode; // 전역 레이어 코드 설정
//setTreeData(data);
// 편집 가능한 레이어 목록 생성
//const layerItems = getLayerItems(layerCode);
//setEditLayerItems(prev => [...layerItems])
}, [treeData]);
//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;
};
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); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
}
}
if (mapObject.map) {
try {
gisMap = mapObject.map;
} catch (e) {
console.error('오류 발생:', e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
}
}
// 체크된 레이어 정보를 처리하는 함수
const handleCheckedLayersChange = useCallback(
(checkedIds, checkedNodesData) => {
setTimeout(() => {
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
onCheckedLayersChange?.(checkedIds, checkedNodesData);
}, 1000);
},
[onCheckedLayersChange],
);
// 체크된 레이어 정보를 처리하는 함수
const handleCheckedLayersChange = useCallback((checkedIds, checkedNodesData) => {
setTimeout(() => {
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
onCheckedLayersChange?.(checkedIds, checkedNodesData);
}, 1000);
}, [onCheckedLayersChange]);
return (
<>
<GisContext.Provider
value={{
gisCore,
gisMap: mapInstanceRef.current,
treeData,
checkedLayerIds,
}}
>
<div className={`relative flex ${height}`}>
<div
ref={mapRef}
className="gis-map relative flex-1"
style={{ height: "100%", padding: 0 }}
>
<GisTree
visible={tabVisible}
setVisible={setTabVisible}
onCheckedChange={handleCheckedLayersChange}
onLayerIconClick={(layerNm) => {
layerNmRef.current = layerNm;
setLayerNm(layerNm);
setIsOpen(true);
const dummyFeature = {
getProperties: () => ({}), // 최소 구조
};
onFeatureSelect(dummyFeature);
}}
imageVisible={imgVisble}
/>
return (
<>
<GisContext.Provider value={{ gisCore, gisMap: mapInstanceRef.current, treeData, checkedLayerIds }}>
<div className="h-[calc(100vh-10rem)] flex relative">
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} >
<GisTree
visible={tabVisible}
setVisible={setTabVisible}
onCheckedChange={handleCheckedLayersChange}
onLayerIconClick={(layerNm) => {
layerNmRef.current = layerNm
setLayerNm(layerNm); setIsOpen(true); const dummyFeature = {
getProperties: () => ({}), // 최소 구조
};
onFeatureSelect(dummyFeature);
}}
imageVisible={imgVisble}
/>
{/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
{isOpen && !isSearchModalOpen && (
<DetailView
featureData={featureData || { getProperties: () => ({}) }}
seletectedLayerNm={layerNm}
onClose={closePopup} // 닫기 콜백 전달
onFeatureSelect={handleFeatureSelect}
/>
)}
{/* SearchView가 존재할 때만 렌더링 */}
{SearchView && (
<SearchView
open={isSearchModalOpen}
onOpenChange={setIsSearchModalOpen}
onFeatureSelect={handleFeatureSelect}
/>
)}
{/* 시도/시군구 조회 영역 */}
{SearchBar && (
<div
className="absolute left-1/2 top-4 z-30 -translate-x-1/2 rounded"
style={{ maxWidth: "calc(100% - 2rem)" }}
>
<SearchBar />
</div>
)}
<GisToolBar />
<GisMapToggle />
<GisSelect onFeatureSelect={handleFeatureSelect} />
</div>
</div>
{/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
{isOpen && !isSearchModalOpen && (
<DetailView
featureData={featureData || { getProperties: () => ({}) }}
seletectedLayerNm={layerNm}
onClose={closePopup} // 닫기 콜백 전달
onFeatureSelect={handleFeatureSelect}
/>
)}
{/* SearchView가 존재할 때만 렌더링 */}
{SearchView && (
<SearchView
open={isSearchModalOpen}
onOpenChange={setIsSearchModalOpen}
onFeatureSelect={handleFeatureSelect}
/>
)}
{/* 시도/시군구 조회 영역 */}
{SearchBar && (
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-30 bg-gray-300 rounded " style={{ maxWidth: 'calc(100% - 2rem)' }}>
<SearchBar/>
</div>
)}
<GisToolBar />
<GisMapToggle />
<GisSelect onFeatureSelect={handleFeatureSelect} />
</div>
</div>
{isSearch && (
<div className="absolute right-8 top-[305px] z-20">
<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>
)}
</GisContext.Provider>
</>
);
{isSearch && (
<div className="absolute right-8 top-[305px] z-20">
<button
onClick={() => setIsSearchModalOpen(true)}
className="flex h-[40px] w-[40px] items-center justify-center overflow-hidden rounded border border-[#9aa7b8] bg-white p-0 shadow-[0_0_5px_#999]"
>
<Search size={24} className="text-gray-700" />
</button>
</div>
)}
</GisContext.Provider>
</>
);
}
export default React.memo(Gis);
@@ -196,7 +196,7 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
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
className={`absolute top-0 left-0 w-[250px] h-full 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