Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/oi

This commit is contained in:
JIeunhuh
2025-05-22 11:19:04 +09:00
14 changed files with 520 additions and 203 deletions
@@ -0,0 +1,74 @@
import type { AxiosResponse } from "axios";
interface SortConfig {
empty: boolean;
sorted: boolean;
unsorted: boolean;
}
interface PageableConfig {
sort: SortConfig;
offset: number;
pageNumber: number;
pageSize: number;
paged: boolean;
unpaged: boolean;
}
export interface PageableList<T> {
content: T[];
pageable: PageableConfig;
sort: SortConfig;
totalPages: number;
totalElements: number;
number: number;
numberOfElements: number;
size: number;
first: boolean;
last: boolean;
empty: boolean;
}
export interface PageableParams {
page?: number;
size?: number;
sort?: string[];
}
export interface AttachmentParams extends PageableParams {
keywordType?: string;
keyword?: string;
atflCd?: string;
atflSn?: number;
atflId?: string;
orgnlFileNm?: string;
atflFlieSz?: number;
atflType?: string;
dwnldNocs?: number;
delYn?: string;
lnkgDmnId?: string;
lnkgDmnNm?: string;
}
export interface Attachment {
atflCd: string;
atflSn: number;
atflId: string;
physFileNm: string;
orgnlFileNm: string;
atflFlieSz: number;
atflType: string;
dwnldNocs: number;
delYn: string;
lnkgDmnId: string;
lnkgDmnNm: string;
regDt: string;
}
export type AttachmentList = PageableList<Attachment>;
export interface AttachmentService {
getAttachmentsList: (params: AttachmentParams) => Promise<AttachmentList>;
getAttachmentsInfo: (atflCd: string) => Promise<Attachment[]>;
deleteAttachmentByAtflCd: (atflCd: string) => Promise<void>;
}
@@ -0,0 +1,42 @@
import type { AxiosResponse } from "axios";
import type {
Attachment,
AttachmentList,
AttachmentParams,
AttachmentService,
} from "../attachment/attachment-interface";
import { getData, deleteData } from "../index";
export const ENDPOINT = {
ATTACH_SERVICE: {
TUS_UPLOAD: "/attach-service/api/v1/tus/upload",
EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor",
IMAGES: "/attach-service/api/v1/images",
MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail",
ATTACHMENTS: "/attach-service/api/v1/atfl/attachments",
DOWNLOAD: "/attach-service/api/v1/atflcd/download",
STREAM_VIDEO: "/attach-service/api/v1/stream/video",
},
};
export class AttachmentApiService implements AttachmentService {
public async getAttachmentsList(
params: AttachmentParams,
): Promise<AttachmentList> {
const response = await getData(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}`);
return response.data;
}
public async getAttachmentsInfo(atflCd: string): Promise<Attachment[]> {
const response = await getData(
`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`,
);
return response.data;
}
public async deleteAttachmentByAtflCd(atflCd: string): Promise<void> {
await deleteData(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/atflCd/${atflCd}`);
}
}
export const AttachmentApi = new AttachmentApiService();
@@ -1,10 +1,9 @@
import { Upload, type HttpResponse } from "tus-js-client";
import { getCookie, setCookie } from "@utils";
import { ENDPOINT } from "../api/attachment/attachment-service";
const AUTH_USER_ID = process.env.AUTH_USER_ID || '';
const CLAIM_NAME = process.env.CLAIM_NAME || '';
const BASE_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || '';
const END_URL = `${BASE_URL}/attach-service/api/v1/tus/upload`;
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
type UploadOptions = {
handleUploadStart?: (upload: Upload) => void;
@@ -24,7 +23,7 @@ export const handleUpload = (files: FileList, options?: UploadOptions) => {
};
const upload = new Upload(file, {
endpoint: END_URL,
endpoint: ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD,
chunkSize: 10 * 1024 * 1024,
headers: {
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
+47 -19
View File
@@ -1,11 +1,14 @@
"use client"
import { FileUpload, SelectBox } from '@common/business_components/ui';
import { Button, Input } from '@common/components/ui';
import React, { useState } from 'react'
import type { HttpResponse, Upload } from "tus-js-client";
import { FileUploader } from "@common/components";
import { handleUpload } from "@utils";
const FileUploadForm = ({ title, onSubmit }: { title: string, onSubmit:(files:FileList, year:string, quarter:string) => void }) => {
// 현재 년도부터 10년 전까지의 년도 옵션 생성
// 현재 년도부터 10년 전까지의 년도 옵션 생성
const yearOptions = Array.from({ length: 10 }, (_, i) => {
const year = new Date().getFullYear() - i;
return { text: `${year}년`, val: year.toString() };
@@ -35,25 +38,50 @@ const FileUploadForm = ({ title, onSubmit }: { title: string, onSubmit:(files:Fi
selectArray={quater}
onSelectValue={(value: string | number) => setSelectedQuarter(String(value))}
/>
</div>
<div className='flex mt-5 items-center'>
<div className="w-[200px] mr-4">
<Input id="file" type="file" onChange={(e)=>{if(e.target.files)setFiles(e.target.files)}}/>
</div>
<Button
className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700"
size="sm"
onClick={() => {
console.log(files, selectedYear, selectedQuarter);
if (!files || !selectedYear || !selectedQuarter) {
alert('파일과 등록년도를 선택해주세요.');
return;
<div className='flex ml-5 items-center'>
<div className="w-[80px] mr-4">
<FileUploader
id="atflCd"
className="!h-auto !max-w-none flex-col !items-start"
accept=".jpeg,.jpg,.png"
onUpload={(e) => {
const selectedFiles = e.target.files;
if (!selectedFiles) return;
const fileArray = Array.from(selectedFiles);
setFiles(fileArray);
if (fileArray[0]) {
handleUpload(selectedFiles, {
handleUploadStart: setUpload,
handleSuccess: setUpload,
handleResponse: handleUploadResponse(fileArray[0]),
});
}
onSubmit(files, selectedYear, selectedQuarter); // 부모 컴포넌트로 데이터 전달
}}
>
등록
</Button>
customPreview={<></>}
/>
</div>
<Button
className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700"
size="sm"
onClick={() => {
if (!files?.[0]) {
alert('파일을 선택해주세요.');
return;
}
if (!selectedYear) {
alert('등록년도를 선택해주세요.');
return;
}
if (!selectedQuarter) {
alert('분기기를 선택해주세요.');
return;
}
}}
>
등록
</Button>
</div>
</div>
</div>
</div>
+4
View File
@@ -22,4 +22,8 @@
@apply border-b-[#3f4042];
}
}
input[type="file"] {
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
}
}
+54
View File
@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from "next/server";
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
const serverApiUrl =
process.env.NEXT_PUBLIC_SERVER_API_URL || "http://localhost:3002";
const geoServerUrl = process.env.NEXT_PUBLIC_GEOSERVER_URL || "";
export function middleware(req: NextRequest) {
console.log("Middleware 진입:", req.nextUrl.pathname);
const pathname = req.nextUrl.pathname;
const searchParams = req.nextUrl.searchParams;
if (pathname.startsWith("/server")) {
const queryParams = searchParams.toString();
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
queryParams ? `?${queryParams}` : ""
}`;
console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/geoserver")) {
const queryParams = searchParams.toString();
const newUrl = `${geoServerUrl}${pathname.replace("/geoserver", "")}${
queryParams ? `?${queryParams}` : ""
}`;
console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/attach-service")) {
const requestHeaders = new Headers(req.headers);
const token = req.cookies.get(CLAIM_NAME)?.value;
const userId = req.cookies.get(AUTH_USER_ID)?.value;
const newUrl = `${serverApiUrl}${pathname}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
console.log(
`[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`,
);
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
});
}
return NextResponse.next();
}
export const config = {
matcher: ["/server/:path*","/geoserver/:path*", "/attach-service/:path*"],
};
+13 -13
View File
@@ -6,7 +6,7 @@ const cnServerUrl = process.env.NEXT_PUBLIC_SERVER_API_URL?.replace(/\/$/, '') ?
const nextConfig: NextConfig = {
basePath: "/sn",
// basePath: "/sn",
typescript: {
ignoreBuildErrors: true, // 이 줄 추가
},
@@ -44,18 +44,18 @@ const nextConfig: NextConfig = {
return config;
},
async rewrites() {
return [
{
source: '/geoserver/:hasudo*',
destination: `${geoServerUrl}/:hasudo*`,
},
{
source: '/attach-service/:path*',
destination: `${cnServerUrl}/attach-service/:path*`,
},
];
},
// async rewrites() {
// return [
// {
// source: '/geoserver/:hasudo*',
// destination: `${geoServerUrl}/:hasudo*`,
// },
// {
// source: '/attach-service/:path*',
// destination: `${cnServerUrl}/attach-service/:path*`,
// },
// ];
// },
};
export default nextConfig;
@@ -2,7 +2,7 @@
export default function MainDiv(props: {children: React.ReactNode}) {
return (
<div className="h-auto bg-container">
<div className="min-h-[89vh] bg-container">
{props.children}
</div>
)
@@ -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
@@ -36,12 +33,76 @@ function Gis({ treeData = [], isSearch = false }) {
};
// SearchModal, GisContent 등에서 호출
const handleFeatureSelect = (data) => {
console.log('check',data)
setFeatureData(data); // 선택된 피처 데이터 저장
setIsOpen(true); // 팝업 열기
setIsSearchModalOpen(false); // SearchModal 닫기
};
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();
// // EPSG:4326 좌표계로 추정되는 경우(값이 위도/경도 범위)
// console.log('before extent', extent);
// const is4326 =
// extent[0] >= -90 && extent[0] <= 90 &&
// extent[1] >= -180 && extent[1] <= 180 &&
// extent[2] >= -90 && extent[2] <= 90 &&
// extent[3] >= -180 && extent[3] <= 180;
// if (is4326) {
// // extent: [위도, 경도, 위도, 경도] → [경도, 위도, 경도, 위도]로 순서 변경
// const reorderedExtent = [extent[1], extent[0], extent[3], extent[2]];
// // EPSG:4326 extent를 EPSG:3857로 변환
// extent = olProj.transformExtent(reorderedExtent, 'EPSG:4326', 'EPSG:3857');
// console.log('after extent', extent);
// }
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;
@@ -117,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]) => ({
@@ -144,10 +205,10 @@ function Gis({ treeData = [], isSearch = false }) {
return (
<>
<GisContext.Provider value={{gisCore, gisMap, treeData, checkedLayerIds}}>
<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
@@ -155,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/>
@@ -165,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>
)}
</>
);
}
}
@@ -6,32 +6,37 @@ import { GisContext } from './Gis';
export default React.memo(function GisSelect({ onFeatureSelect }) {
const { gisMap } = useContext(GisContext);
// useEffect(() => {
// if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함
// const select = new GisOlSelect(gisMap);
// GisApp.Module.select = select;
// select.getSelectedInfo(function (features) {
// if (features?.[0]) {
// // onFeatureClick?.(features[0]); // props로 넘긴 함수 실행
// onFeatureSelect(layer.getProperties()); //피쳐 속성데이터 상위컴포넌트로 전달
// }
// });
// }, [gisMap, onFeatureSelect]);
const select = new GisOlSelect(gisMap);
GisApp.Module.select = select;
select.getSelectedInfo(function (features) {
//clickFeature(features[0]);
if(features.length > 0) {
const layer = features[0];
onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달
useEffect(() => {
if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함
// 이미 select가 있으면 재생성하지 않음
if (!GisApp.Module.select) {
const select = new GisOlSelect(gisMap);
GisApp.Module.select = select;
// 피처 선택 시 콜백 등록
select.getSelectedInfo(function (features) {
if (features?.[0]) {
console.log('GisSelect features', features);
onFeatureSelect(features[0]);
}
});
}
});
}, [gisMap, onFeatureSelect]);
// const select = new GisOlSelect(gisMap);
// GisApp.Module.select = select;
// select.getSelectedInfo(function (features) {
// //clickFeature(features[0]);
// if(features.length > 0) {
// const layer = features[0];
// // console.log('GisSelect layer', layer);
// onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달
// }
// });
return (
<>
@@ -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>
)}
</>
);
});
@@ -28,8 +28,8 @@ GisApp.layerInfos = [];
//기본정보
//GisApp.GEOSERVER_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/ows';
//GisApp.GEOSERVER_WMS_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/wms';
GisApp.GEOSERVER_URL = '/sn/geoserver/hasudo/ows';
GisApp.GEOSERVER_WMS_URL = '/sn/geoserver/hasudo/wms';
GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows';
GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms';
//맵 정보
GisApp.EMAP_URL = '/emap/';
@@ -47,7 +47,7 @@ GisApp.BASE_MAP_TYPES = [];
GisApp.FROM_PROJ = 'EPSG:4326';
GisApp.TO_PROJ = 'EPSG:3857';
GisApp.TO_DATA_PROJ = 'EPSG:4326';
GisApp.CONTEXT_LAYER_PATH = '/sn/images/map/layericon/';
GisApp.CONTEXT_LAYER_PATH = '/images/map/layericon/';
GisApp.layerCode = {};
GisApp.hlLayerId = 'LightFeatureList';
GisApp.hLayerId = 'highLightFeature';