Merge remote-tracking branch 'origin/iipbs/sn/map' into iipbs/common/main
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@common/assets",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -12,13 +12,14 @@
|
||||
--popover-foreground: #0a0a0a;
|
||||
--primary: #2094fa;
|
||||
--primary-foreground: #fafafa;
|
||||
--primary-transparent: rgba(32, 148, 250, 0.1);
|
||||
--secondary: #f5f5f5;
|
||||
--secondary-foreground: #0f1e33;
|
||||
--muted: #f5f5f5;
|
||||
--muted-foreground: #737373;
|
||||
--accent: #f5f5f5;
|
||||
--accent-foreground: #0f1e33;
|
||||
--destructive: #ff4141;
|
||||
--destructive: #ff6c64;
|
||||
--destructive-foreground: #fafafa;
|
||||
--border: #e5e5e5;
|
||||
--input: #e5e5e5;
|
||||
@@ -54,6 +55,7 @@
|
||||
--popover-foreground: #fafafa;
|
||||
--primary: #2094fa;
|
||||
--primary-foreground: #171717;
|
||||
--primary-transparent: rgba(32, 148, 250, 0.1);
|
||||
--secondary: #262626;
|
||||
--secondary-foreground: #fafafa;
|
||||
--muted: #262626;
|
||||
@@ -87,4 +89,13 @@
|
||||
--container: #0a0a0a;
|
||||
--title: #fff;
|
||||
}
|
||||
[data-theme="red"] {
|
||||
--primary: #ff6464;
|
||||
--destructive: #7388ec;
|
||||
--container: #f6f6f9;
|
||||
}
|
||||
[data-theme="green"] {
|
||||
--primary: #00b9b8;
|
||||
--container: #f2f2f3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"axios": "1.8.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import GisApp from './lib/gis.core';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { GisContext, setTree } from './Gis';
|
||||
import { Button } from "@common/components";
|
||||
import { gAlert, gConfirm } from './lib/util';
|
||||
// import { fclApi } from '../../js/apis/fcl/fclApi';
|
||||
|
||||
export default React.memo(function GisEdit() {
|
||||
const { editLayerItems = [], siteSn, locgovCd, checkedLayerIds } = useContext(GisContext);
|
||||
const editModule = GisApp.Module.edit;
|
||||
const ftrIdn = useRef(null);
|
||||
const selectedLyrId = useRef(null);
|
||||
if (editLayerItems.length > 0 && !selectedLyrId.current) {
|
||||
selectedLyrId.current = editLayerItems[0].value;
|
||||
}
|
||||
const [btnEvt, setBtnEvt] = useState({ edit: false, modify: false, add: false, snap: false });
|
||||
const edit = () => {
|
||||
const edit = !btnEvt.edit;
|
||||
if (edit) {
|
||||
setBtnEvt({ ...btnEvt, edit: edit });
|
||||
} else {
|
||||
setBtnEvt({ edit: edit, modify: false, add: false, snap: false });
|
||||
editModule.clearUndoRedoFeature();
|
||||
}
|
||||
editModule.removeInteraction();
|
||||
};
|
||||
|
||||
const modify = () => {
|
||||
const modify = !btnEvt.modify;
|
||||
setBtnEvt({ ...btnEvt, ...{ add: false }, modify });
|
||||
if (modify) {
|
||||
editModule.setModify();
|
||||
} else {
|
||||
editModule.removeModify();
|
||||
}
|
||||
};
|
||||
|
||||
const add = () => {
|
||||
let add = !btnEvt.add;
|
||||
if (add) {
|
||||
if (!checkedLayerIds?.current?.includes(selectedLyrId.current)) {
|
||||
alert('트리에서 레이어 체크 후 추가 가능합니다.');
|
||||
return;
|
||||
}
|
||||
const errMsg = editModule.setDraw(selectedLyrId.current);
|
||||
if (errMsg) {
|
||||
alert(errMsg);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
editModule.removeDraw();
|
||||
}
|
||||
setBtnEvt({ ...btnEvt, ...{ modify: false }, add });
|
||||
};
|
||||
|
||||
const del = () => {
|
||||
editModule.deleteFeature();
|
||||
};
|
||||
|
||||
const undo = () => {
|
||||
GisApp.Module.edit.undo();
|
||||
};
|
||||
|
||||
const redo = () => {
|
||||
GisApp.Module.edit.redo();
|
||||
};
|
||||
|
||||
const snap = () => {
|
||||
const snap = !btnEvt.snap;
|
||||
setBtnEvt({ ...btnEvt, snap });
|
||||
if (snap) {
|
||||
editModule.setSnap();
|
||||
} else {
|
||||
editModule.removeSnap();
|
||||
}
|
||||
};
|
||||
|
||||
const validateEditFeatures = editFeatures => {
|
||||
if (editFeatures.length === 0) {
|
||||
//gAlert('INFO', 'NO_CHANGED_DATA');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
const editFeatureInfo = editModule.getEditFeatureInfo();
|
||||
const editFeatures = Object.values(editFeatureInfo).map(value => ({
|
||||
siteSn,
|
||||
locgovCd,
|
||||
geom: value.geom,
|
||||
flag: value.flag,
|
||||
lyrId: value.lyr_id,
|
||||
}));
|
||||
if (!validateEditFeatures(editFeatures)) {
|
||||
return;
|
||||
}
|
||||
if (!gConfirm('CONFIRM', 'SAVE_QUEST')) return false;
|
||||
// const result = await fclApi.saveEditFeatures(editFeatures);
|
||||
// if (result.code === 200) {
|
||||
// editModule.clearUndoRedo();
|
||||
// }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
//setBtnEvt({ edit: false, modify: false, add: false, snap: false });
|
||||
editModule.setUndoInteraction();
|
||||
editModule.removeInteraction();
|
||||
}, [locgovCd]);
|
||||
if (editLayerItems.length === 0) return;
|
||||
return (
|
||||
<>
|
||||
<div className="edit_control map_control_div ol-unselectable ol-control" style={{ pointerEvents: 'auto', top: '28px', left: '120px', zIndex: '1' }}>
|
||||
<label htmlFor={'selectLyrId'}></label>
|
||||
<select
|
||||
id={'selectLyrId'}
|
||||
onChange={evt => {
|
||||
const lyrId = evt.target.value;
|
||||
selectedLyrId.current = lyrId + '';
|
||||
setBtnEvt({ ...btnEvt, ...{ add: false } });
|
||||
editModule.setInteractionActive(false);
|
||||
}}
|
||||
>
|
||||
{editLayerItems.map(layerItem => (
|
||||
<option value={layerItem?.lyrSn} key={layerItem?.lyrSn}>
|
||||
{layerItem?.lyrNm}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button btn={{ type: `${btnEvt.edit ? 'primary' : 'secondary'}`, fnc: edit }}>{`${btnEvt.edit ? ' 편집종료' : '편집하기'}`}</Button>
|
||||
{btnEvt.edit && (
|
||||
<ul>
|
||||
<li>
|
||||
<div className={`btn_edit_slt${btnEvt.modify ? ' active' : ''}`} title="수정" onClick={modify}>
|
||||
<span>수정</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className={`btn_edit_add${btnEvt.add ? ' active' : ''}`} title="추가" onClick={add}>
|
||||
<span>추가</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="btn_edit_del" title="삭제" onClick={del}>
|
||||
<span>삭제</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="btn_edit_prev" title="이전" onClick={undo}>
|
||||
<span>이전</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="btn_edit_next" title="다음" onClick={redo}>
|
||||
<span>다음</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className={`btn_edit_snap${btnEvt.snap ? ' active' : ''}`} title="스냅" onClick={snap}>
|
||||
<span>스냅</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="btn_edit_save" title="저장" onClick={save}>
|
||||
<span>저장</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import GisApp from './lib/gis.core';
|
||||
import { MousePosition } from 'ol/control';
|
||||
import { useContext, useEffect, useRef } from 'react';
|
||||
import { GisContext } from './Gis';
|
||||
export default React.memo(function GisLonLat() {
|
||||
const { gisCore } = useContext(GisContext);
|
||||
const labelLonLatRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!gisCore?.map || !labelLonLatRef.current) return;
|
||||
|
||||
// 기존 MousePosition 제거를 위한 컨트롤 추적
|
||||
let existingMouseControls = gisCore.map
|
||||
.getControls()
|
||||
.getArray()
|
||||
.filter(control => control instanceof MousePosition);
|
||||
|
||||
// 기존 MousePosition 컨트롤 제거
|
||||
existingMouseControls.forEach(control => {
|
||||
gisCore.map.removeControl(control);
|
||||
});
|
||||
|
||||
// 새로운 MousePosition 컨트롤 추가
|
||||
const mousePosition = new MousePosition({
|
||||
coordinateFormat: function (coordinate) {
|
||||
let lng = coordinate[0];
|
||||
let lat = coordinate[1];
|
||||
return `경도 : ${lng.toFixed(3)} 위도 : ${lat.toFixed(3)}`;
|
||||
},
|
||||
projection: GisApp.FROM_PROJ,
|
||||
className: 'custom-mouse-position',
|
||||
target: labelLonLatRef.current,
|
||||
});
|
||||
gisCore.map.addControl(mousePosition);
|
||||
}, [gisCore.map]);
|
||||
return (
|
||||
<>
|
||||
<ul className="map-scale-area">
|
||||
<li id="label_lonlat" ref={labelLonLatRef} />
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { Button } from "@common/components";
|
||||
import { GisContext } from './Gis';
|
||||
import GisApp from './lib/gis.core';
|
||||
|
||||
export default React.memo(function MapToggleBtn() {
|
||||
const { gisCore } = useContext(GisContext);
|
||||
const [activeMap, setActiveMap] = useState({ label: '일반', type: 'base' }); // 기본 선택값
|
||||
|
||||
const handleSelect = mapObj => {
|
||||
setActiveMap(mapObj);
|
||||
gisCore.changeBaseLayer(mapObj.type);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mapbtn">
|
||||
<button className="sprite_gis viewer" title="지도선택">지도 선택</button>
|
||||
<ul className="choice_map">
|
||||
<li className={activeMap.type === 'base' ? 'on' : ''}>
|
||||
<button className="map1" id="base" title="일반지도" onClick={() => handleSelect({ label: '일반', type: 'base' })}>
|
||||
<div>일반지도</div>
|
||||
</button>
|
||||
</li>
|
||||
<li className={activeMap.type === 'satellite' ? 'on' : ''}>
|
||||
<button className="map2" id="satelite" title="위성지도" onClick={() => handleSelect({ label: '위성', type: 'satellite' })}>
|
||||
<div>위성지도</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import GisApp from './lib/gis.core';
|
||||
import GisOlSelect from './lib/gis.select';
|
||||
import { GisContext } from './Gis';
|
||||
|
||||
export default React.memo(function GisSelect({ onFeatureSelect }) {
|
||||
|
||||
const { gisMap } = useContext(GisContext);
|
||||
|
||||
|
||||
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 (
|
||||
<>
|
||||
{/* <div className="select_control map_control_div ol-unselectable ol-control" style={{ pointerEvents: 'auto', zIndex: '1' }}>
|
||||
<h3>선,면,다각형을 이용한 시설물 선택 영역</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<div className="sprite_gis icon1 btn_select_one" title="단일선택" onClick={reset}>
|
||||
<span>단일선택</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="sprite_gis icon4 btn_select_cancel" title="새로고침" onClick={reset}>
|
||||
<span>새로고침</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div> */}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +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';
|
||||
|
||||
|
||||
// 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>
|
||||
|
||||
// {/* 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>
|
||||
// )} */}
|
||||
|
||||
|
||||
// {/* 🔍 검색 모달 */}
|
||||
// {/* <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>
|
||||
|
||||
// {/* 열기 버튼 */}
|
||||
// {!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);
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import GisApp from './lib/gis.core';
|
||||
import GisOlMeasure from './lib/gis.measure';
|
||||
import { useContext } from 'react';
|
||||
import { useGisContext } from './Gis';
|
||||
export default React.memo(function GisToolBar() {
|
||||
const { gisMap } = useGisContext();
|
||||
if (!gisMap) return <></>;
|
||||
const gisMeasuer = new GisOlMeasure(gisMap);
|
||||
GisApp.Module.measure = gisMeasuer;
|
||||
const zoomIn = () => {
|
||||
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
|
||||
gisMap.getView().animate({
|
||||
zoom: gisMap.getView().getZoom() + 1,
|
||||
duration: 250,
|
||||
});
|
||||
}
|
||||
};
|
||||
const zoomOut = () => {
|
||||
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
|
||||
gisMap.getView().animate({
|
||||
zoom: gisMap.getView().getZoom() - 1,
|
||||
duration: 250,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const measure = type => {
|
||||
gisMeasuer.addInteraction(type);
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
gisMeasuer.removeFeature();
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="custom-toolbar">
|
||||
<div className="tool-bar">
|
||||
<h3>toolbar 영역</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<div className="btn_control_zoomin" title="확대" onClick={zoomIn}>
|
||||
<span>확대</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="btn_control_zoomout" title="축소" onClick={zoomOut}>
|
||||
<span>축소</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="btn_control_ruler btn_control_measure" title="거리 측정" onClick={() => measure(GisApp.measure.LineString)}>
|
||||
<span>거리측정</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="btn_control_area btn_control_measure" title="면적 측정" onClick={() => measure(GisApp.measure.Polygon)}>
|
||||
<span>면적측정</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="sprite_gis icon4 btn_select_cancel" title="리셋" onClick={() => reset()}>
|
||||
<span>리셋</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,235 @@
|
||||
import GisApp from './lib/gis.core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useGisContext } from './Gis';
|
||||
import { DndProvider } from "react-dnd";
|
||||
import {
|
||||
Tree,
|
||||
MultiBackend,
|
||||
getBackendOptions
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
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 [activeTab, setActiveTab] = useState('layers');
|
||||
|
||||
useEffect(() => {
|
||||
if (treeData && treeData.length > 0) {
|
||||
const nodeData = setNodeValues(treeData);
|
||||
setNodes(nodeData);
|
||||
gisCore.setAllLayersOnMap(false);
|
||||
}
|
||||
}, [treeData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nodes.length > 0) {
|
||||
const defaultChecked = getDefaultCheckedNodes(nodes);
|
||||
setChecked(defaultChecked);
|
||||
const defaultExpanded = Array.from(new Set([
|
||||
...defaultChecked,
|
||||
...defaultChecked.flatMap((id) => getAncestors(id, nodes))
|
||||
]));
|
||||
setExpanded(defaultExpanded);
|
||||
gisCore.setLayerOnMap(defaultChecked, true);
|
||||
}
|
||||
}, [nodes]);
|
||||
|
||||
const getDefaultCheckedNodes = (nodes, values = []) => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (node.data.initEprssYn === 'Y') {
|
||||
values.push(node.data.lyrSn);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
};
|
||||
|
||||
const getDescendants = (id, all) => {
|
||||
const children = all.filter((n) => n.parent === id);
|
||||
return children.reduce(
|
||||
(acc, child) => acc.concat(child.id, ...getDescendants(child.id, all)),
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
const getAncestors = (id, all) => {
|
||||
const node = all.find((n) => n.id === id);
|
||||
if (!node || node.parent == null) return [];
|
||||
return [node.parent, ...getAncestors(node.parent, all)];
|
||||
};
|
||||
|
||||
const isEveryChildChecked = (parentId, all, checked) => {
|
||||
const children = all.filter((n) => n.parent === parentId);
|
||||
return children.every((c) => checked.includes(c.id));
|
||||
};
|
||||
|
||||
const handleCheck = (id) => {
|
||||
setChecked((prevChecked) => {
|
||||
const isChecked = prevChecked.includes(id);
|
||||
let newChecked = [...prevChecked];
|
||||
if (isChecked) {
|
||||
const descendants = getDescendants(id, nodes);
|
||||
newChecked = newChecked.filter((cid) => cid !== id && !descendants.includes(cid));
|
||||
const ancestors = getAncestors(id, nodes);
|
||||
ancestors.forEach((p) => {
|
||||
if (newChecked.includes(p)) {
|
||||
newChecked = newChecked.filter((cid) => cid !== p);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const descendants = getDescendants(id, nodes);
|
||||
newChecked = Array.from(new Set([...newChecked, id, ...descendants]));
|
||||
const ancestors = getAncestors(id, nodes);
|
||||
ancestors.forEach((p) => {
|
||||
if (isEveryChildChecked(p, nodes, newChecked)) {
|
||||
newChecked.push(p);
|
||||
}
|
||||
});
|
||||
// setExpanded((prev) => Array.from(new Set([...prev, ...ancestors])));
|
||||
}
|
||||
const allIds = nodes.map((n) => n.id);
|
||||
const unchecked = allIds.filter((nid) => !newChecked.includes(nid));
|
||||
gisCore.setLayerOnMap(newChecked, true);
|
||||
gisCore.setLayerOnMap(unchecked, false);
|
||||
return newChecked;
|
||||
});
|
||||
};
|
||||
|
||||
const setNodeValues = (result) => {
|
||||
return result.map((item) => {
|
||||
const src = makeLegendSrc(item);
|
||||
let label = item.lyrNm;
|
||||
return {
|
||||
id: item.lyrSn,
|
||||
text: label,
|
||||
parent: item.upperLyrSn,
|
||||
droppable: !!item.children,
|
||||
data: item,
|
||||
icon: src ? <img src={src} alt={item.lyrNm} /> : '',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const makeLegendSrc = layerinfo => {
|
||||
if (!layerinfo.gobjTy) return null;
|
||||
|
||||
let src;
|
||||
if (layerinfo.iconNm) {
|
||||
src = GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm;
|
||||
} else {
|
||||
const ss = makeLegendImage(layerinfo);
|
||||
src = ss.src;
|
||||
}
|
||||
return src;
|
||||
};
|
||||
|
||||
const makeLegendImage = layerinfo => {
|
||||
let image = document.createElement('img');
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = 16;
|
||||
canvas.height = 16;
|
||||
let ctx = canvas.getContext('2d');
|
||||
ctx.beginPath();
|
||||
|
||||
if (layerinfo.gobjTy === 'lineLabelPoligon') {
|
||||
ctx.moveTo(1, 1);
|
||||
ctx.lineTo(15, 1);
|
||||
ctx.lineTo(15, 15);
|
||||
ctx.lineTo(1, 15);
|
||||
ctx.lineTo(1, 1);
|
||||
} else if (layerinfo.gobjTy === 'line') {
|
||||
ctx.moveTo(1, 15);
|
||||
ctx.lineTo(5, 1);
|
||||
ctx.lineTo(9, 15);
|
||||
ctx.lineTo(15, 1);
|
||||
} else {
|
||||
ctx.moveTo(1, 1);
|
||||
ctx.lineTo(15, 1);
|
||||
ctx.lineTo(15, 15);
|
||||
ctx.lineTo(1, 15);
|
||||
ctx.lineTo(1, 1);
|
||||
}
|
||||
|
||||
if (layerinfo.gobjTy === 'lineLabelPoligon') {
|
||||
ctx.fillStyle = layerinfo.polgnColor;
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = layerinfo.lnColor;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (layerinfo.gobjTy === 'line') {
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = layerinfo.lnColor;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
image.src = canvas.toDataURL('image/png');
|
||||
return image;
|
||||
};
|
||||
|
||||
if (!treeData || treeData.length === 0) return null;
|
||||
|
||||
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'}`}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
@charset 'utf-8';
|
||||
|
||||
.edit_control {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 610px;
|
||||
}
|
||||
.edit_control h3 {
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
.edit_control ul {
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
height: 40px;
|
||||
margin-left: 10px;
|
||||
background: #fff;
|
||||
font-size: 0;
|
||||
border: 1px solid #9aa7b8;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0px 0px 5px #999;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.edit_control ul li {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-right: 1px solid #eeeeee;
|
||||
}
|
||||
.edit_control ul li:last-child {
|
||||
border-right: 0 none;
|
||||
}
|
||||
/*background 삭제*/
|
||||
/* .edit_control ul li a {overflow:hidden;width: 40px;height: 40px;background: url(../../images/om/ico_gis_sprite.png) no-repeat;} */
|
||||
.edit_control ul li a {
|
||||
overflow: hidden;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
}
|
||||
.edit_control ul li a span {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
.edit_control select {
|
||||
float: left;
|
||||
min-width: 150px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin-left: 10px;
|
||||
padding-left: 10px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0px 0px 5px #999;
|
||||
}
|
||||
|
||||
.edit_control input {
|
||||
float: left;
|
||||
min-width: 150px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin-left: 10px;
|
||||
padding-left: 10px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0px 0px 5px #999;
|
||||
}
|
||||
|
||||
.edit_control button {
|
||||
float: left;
|
||||
min-width: 80px;
|
||||
height: 40px;
|
||||
margin-left: 5px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.edit_control button:hover {
|
||||
background: #1357ce;
|
||||
}
|
||||
|
||||
.edit_toolbar {
|
||||
position: absolute;
|
||||
left: 280px;
|
||||
top: 52px;
|
||||
}
|
||||
.btn_edit_slt {
|
||||
background: url(../images/toolbar/btn_edit_slt.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
.btn_edit_slt:hover {
|
||||
background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
.btn_edit_slt.active {
|
||||
background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
.btn_edit_add {
|
||||
background: url(../images/toolbar/btn_edit_add.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
.btn_edit_add:hover {
|
||||
background: url(../images/toolbar/btn_edit_add_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
.btn_edit_add.active {
|
||||
background: url(../images/toolbar/btn_edit_add_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_del {
|
||||
background: url(../images/toolbar/btn_edit_del.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_del:hover {
|
||||
background: url(../images/toolbar/btn_edit_del_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_prev {
|
||||
background: url(../images/toolbar/btn_edit_prev.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_prev:hover {
|
||||
background: url(../images/toolbar/btn_edit_prev_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_next {
|
||||
background: url(../images/toolbar/btn_edit_next.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_next:hover {
|
||||
background: url(../images/toolbar/btn_edit_next_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_snap {
|
||||
background: url(../images/toolbar/btn_edit_snap.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_snap:hover {
|
||||
background: url(../images/toolbar/btn_edit_snap_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_snap.active {
|
||||
background: url(../images/toolbar/btn_edit_snap_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_save {
|
||||
background: url(../images/toolbar/btn_edit_save.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_save:hover {
|
||||
background: url(../images/toolbar/btn_edit_save_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_transform {
|
||||
background: url(../images/toolbar/btn_edit_slt.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_transform:hover {
|
||||
background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn_edit_transform.active {
|
||||
background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center;
|
||||
height: 40px;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@charset 'utf-8';
|
||||
/* 로드뷰 관련 */
|
||||
.roadview-container {width: 100%; height: 100%; z-index:500; display:block;position: absolute; left: 0px;top: 0px;}
|
||||
.roadview-container .roadview-container-1p {width: 310px; height: 190px; bottom: 15px; left: 15px; top: auto; border-radius: 2px; overflow: hidden; z-index: 1000; position: absolute;}
|
||||
.roadview-container .roadview-container-2p {width: 100%; height:100%; float:left; position:relative;}
|
||||
.roadview-container .roadview {position:absolute; top:0; right:0; left:0; bottom:0; z-index:0; background:#999;}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html,
|
||||
body,
|
||||
div,
|
||||
span,
|
||||
applet,
|
||||
object,
|
||||
iframe,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
a,
|
||||
abbr,
|
||||
acronym,
|
||||
address,
|
||||
big,
|
||||
cite,
|
||||
code,
|
||||
del,
|
||||
dfn,
|
||||
em,
|
||||
img,
|
||||
ins,
|
||||
kbd,
|
||||
q,
|
||||
s,
|
||||
samp,
|
||||
small,
|
||||
strike,
|
||||
strong,
|
||||
sub,
|
||||
sup,
|
||||
tt,
|
||||
var,
|
||||
b,
|
||||
u,
|
||||
i,
|
||||
center,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
fieldset,
|
||||
form,
|
||||
label,
|
||||
legend,
|
||||
table,
|
||||
caption,
|
||||
tbody,
|
||||
tfoot,
|
||||
thead,
|
||||
tr,
|
||||
th,
|
||||
td,
|
||||
article,
|
||||
aside,
|
||||
canvas,
|
||||
details,
|
||||
embed,
|
||||
figure,
|
||||
figcaption,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
output,
|
||||
ruby,
|
||||
section,
|
||||
summary,
|
||||
time,
|
||||
mark,
|
||||
audio,
|
||||
video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote,
|
||||
q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before,
|
||||
blockquote:after,
|
||||
q:before,
|
||||
q:after {
|
||||
content: "";
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #e6e6fc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 50px 0px;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
width: 800px;
|
||||
height: 500px;
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 6px 6px 5px grey;
|
||||
}
|
||||
|
||||
.controls .controls__colors {
|
||||
display: flex;
|
||||
}
|
||||
.controls__colors .controls__color {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 3px 3px 5px grey;
|
||||
}
|
||||
|
||||
.jscolor {
|
||||
color: transparent;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
border-width: 0px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 3px 3px 5px grey;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controls .controls__btns {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.controls__btns button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.controls__btns button {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
padding: 5px 0px;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
box-shadow: 3px 3px 3px grey;
|
||||
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.controls .controls__range {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
@charset 'utf-8';
|
||||
|
||||
.ol-tooltip {
|
||||
position: relative;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ol-tooltip-measure {
|
||||
opacity: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
.ol-tooltip-static {
|
||||
background-color: #ffcc33;
|
||||
color: black;
|
||||
border: 1px solid white;
|
||||
}
|
||||
.ol-tooltip-measure:before,
|
||||
.ol-tooltip-static:before {
|
||||
border-top: 6px solid rgba(0, 0, 0, 0.5);
|
||||
border-right: 6px solid transparent;
|
||||
border-left: 6px solid transparent;
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
margin-left: -7px;
|
||||
left: 50%;
|
||||
}
|
||||
.ol-tooltip-static:before {
|
||||
border-top-color: #ffcc33;
|
||||
}
|
||||
@@ -0,0 +1,978 @@
|
||||
import MousePosition from 'ol/control/MousePosition';
|
||||
import ScaleLine from 'ol/control/ScaleLine';
|
||||
import Feature from 'ol/Feature';
|
||||
import WKT from 'ol/format/WKT';
|
||||
import LinearRing from 'ol/geom/LinearRing';
|
||||
import Draw from 'ol/interaction/Draw';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
import * as olProj from 'ol/proj';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import XYZ from 'ol/source/XYZ';
|
||||
import GeoSJON from 'ol/format/GeoJSON';
|
||||
import { Fill, Stroke, Style } from 'ol/style';
|
||||
import CircleStyle from 'ol/style/Circle';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import { Point } from 'ol/geom';
|
||||
import { getTopLeft } from 'ol/extent';
|
||||
import TileWMTS from 'ol/source/WMTS';
|
||||
import WMTSTileGrid from 'ol/tilegrid/WMTS';
|
||||
// import { get as getProjection } from 'ol/proj';
|
||||
import proj4 from 'proj4';
|
||||
import { register } from 'ol/proj/proj4';
|
||||
import olSourceImageWMS from 'ol/source/ImageWMS';
|
||||
import { GeoJSON } from 'ol/format';
|
||||
import GisOlEditLayer from './gis.edit';
|
||||
import GisOlLayer from './gis.layer';
|
||||
|
||||
const GisApp = {};
|
||||
/**
|
||||
* GisApp 전역변수 생성
|
||||
*/
|
||||
//레이어 정보
|
||||
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 = '/geoserver/hasudo/ows';
|
||||
GisApp.SN_GEOSERVER_URL = '/sewerage-service/api/v1/map/geometry';
|
||||
GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms';
|
||||
|
||||
//맵 정보
|
||||
GisApp.GOOGLE_API_KEY = 'AIzaSyCf1sgeELFalfPuqlykRKBMj6X9u_DQH6M';
|
||||
GisApp.GOOGLE_ROADMAP_URL = `https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
|
||||
GisApp.GOOGLE_SATELLITE_URL = `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
|
||||
GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
|
||||
GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`;
|
||||
|
||||
// TODO: NGII 정보
|
||||
GisApp.NGII_API_KEY = '04trYP9_xwLAfALjwZ-B8g'; // NGII에서 발급받은 API 키
|
||||
GisApp.NGII_WMTS_URL = `https://map.ngii.go.kr/openapi/Gettile.do?apikey=${GisApp.NGII_API_KEY}`;
|
||||
|
||||
GisApp.EMAP_URL = '/emap/';
|
||||
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
|
||||
GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png';
|
||||
GisApp.VWORLD_SATELLITE_URL = 'https://xdworld.vworld.kr/2d/Satellite/service/{z}/{x}/{y}.jpeg';
|
||||
GisApp.OPENSTREET_URL = '//tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
GisApp.BASE_MAP_TYPES = [];
|
||||
|
||||
GisApp.FROM_PROJ = 'EPSG:4326';
|
||||
// GisApp.TO_PROJ = 'EPSG:3857';
|
||||
GisApp.TO_PROJ = 'EPSG:5179'; //ngii 좌표계
|
||||
GisApp.TO_DATA_PROJ = 'EPSG:4326';
|
||||
GisApp.CONTEXT_LAYER_PATH = '/images/map/layericon/';
|
||||
GisApp.layerCode = {};
|
||||
GisApp.hlLayerId = 'LightFeatureList';
|
||||
GisApp.hLayerId = 'highLightFeature';
|
||||
GisApp.wtlkgAnal = 'wtlkgAnal';
|
||||
|
||||
GisApp.Module = {
|
||||
core: null,
|
||||
select: null,
|
||||
measure: null,
|
||||
layer: null,
|
||||
style: null,
|
||||
edit: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* GisApp.layerInfos Set
|
||||
*/
|
||||
GisApp.setLayerInfos = function (layerInfos) {
|
||||
GisApp.layerInfos = layerInfos;
|
||||
};
|
||||
|
||||
/**
|
||||
* GisApp core
|
||||
* @requires gis.layer.js
|
||||
*/
|
||||
GisApp.core = function (options) {
|
||||
GisApp.core.prototype.initialize(options);
|
||||
};
|
||||
|
||||
GisApp.core.prototype = {
|
||||
defaults: {
|
||||
target: '',
|
||||
centerX: 37.2806094,
|
||||
centerY: 127.0032502,
|
||||
zoom: 12,
|
||||
minZoom: 6,
|
||||
maxZoom: 20,
|
||||
},
|
||||
|
||||
options: {
|
||||
/*1: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.3227711, centerY: 129.1836202, dataProj: 'EPSG:5186' },
|
||||
4: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.0140499, centerY: 126.7110757, dataProj: 'EPSG:5186' },
|
||||
14: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 34.9390499, centerY: 127.6960757, dataProj: 'EPSG:5186' },*/
|
||||
},
|
||||
epsg_5179: null,
|
||||
map: null, // ol.Map
|
||||
view: {}, // ol.view
|
||||
interactions: {}, // interaction 모음 객체
|
||||
selectedFeature: null, // 현재 선택된 feature
|
||||
baseLayers: {},
|
||||
baseMiniMapLayers: {},
|
||||
layers: {},
|
||||
initialize: function (options) {
|
||||
let option = this.options['1'];
|
||||
if (option) {
|
||||
Object.assign(this.defaults, option);
|
||||
GisApp.TO_DATA_PROJ = option.dataProj;
|
||||
}
|
||||
Object.assign(this.defaults, options);
|
||||
GisApp.Module.core = this;
|
||||
GisApp.Module.layer = new GisOlLayer(this.defaults);
|
||||
GisApp.Module.edit = new GisOlEditLayer(this.defaults);
|
||||
// TODO: vworld layer 정보 (getNGIIbaseMap)활성화 하면 ngii base map으로 변경됨
|
||||
// this.baseMiniMapLayers['base'] = this.getBaseMap('minimap-base');
|
||||
// this.baseLayers['base'] = this.getBaseMap('base');
|
||||
// this.baseLayers['satellite'] = this.getBaseMap('satellite');
|
||||
// this.baseLayers['3d'] = this.getBaseMap('3d');
|
||||
this.baseLayers['base'] = this.getNGIIBaseMap('base','korean_map');
|
||||
this.baseLayers['satellite'] = this.getNGIIBaseMap('satellite','satellite_map');
|
||||
this.baseMiniMapLayers['base'] = this.getNGIIBaseMap('minimap-base','korean_map');
|
||||
},
|
||||
layerType: {
|
||||
point: 'POINT',
|
||||
line: 'LINE',
|
||||
poligon: 'POLIGON',
|
||||
},
|
||||
chgMngFcltOptions: function (fcltCd) {
|
||||
let option = this.options[fcltCd];
|
||||
Object.assign(this.defaults, option);
|
||||
this.setCenter(Number(this.defaults.centerX), Number(this.defaults.centerY));
|
||||
this.setZoom(15);
|
||||
},
|
||||
// NGII Base Map
|
||||
getNGIIBaseMap: function (_id, _layer) {
|
||||
// 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 projection = olProj.get('EPSG:5179');
|
||||
projection.setExtent([-200000.0, -28024123.62, 31824123.62, 4000000.0]);
|
||||
|
||||
// const resolutions = [2088.96, 1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255];
|
||||
// const matrixIds = ["L05", "L06", "L07", "L08", "L09", "L10", "L11", "L12", "L13", "L14", "L15", "L16", "L17", "L18"];
|
||||
const resolutions = [1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255];
|
||||
const matrixIds = ["L06", "L07", "L08", "L09", "L10", "L11", "L12", "L13", "L14", "L15", "L16", "L17", "L18"];
|
||||
|
||||
const wmtsSource = new TileWMTS({
|
||||
url: GisApp.NGII_WMTS_URL,
|
||||
layer: _layer,
|
||||
matrixSet: 'EPSG:5179',
|
||||
format: 'image/png',
|
||||
projection: projection,
|
||||
style: 'korean',
|
||||
tileGrid: new WMTSTileGrid({
|
||||
origin: getTopLeft(projection.getExtent()),
|
||||
resolutions: resolutions,
|
||||
matrixIds: matrixIds,
|
||||
tileSize: [256, 256]
|
||||
}),
|
||||
requestEncoding: 'KVP',
|
||||
crossOrigin: 'anonymous'
|
||||
});
|
||||
|
||||
let baseMap = new TileLayer({
|
||||
id: _id,
|
||||
visible: true,
|
||||
type: 'base',
|
||||
source: wmtsSource,
|
||||
zIndex: 0
|
||||
});
|
||||
return baseMap;
|
||||
},
|
||||
getBaseMap: function (_id) {
|
||||
let url = GisApp.VWORLD_URL;
|
||||
if (_id === 'satellite') {
|
||||
url = GisApp.VWORLD_SATELLITE_URL;
|
||||
}
|
||||
let baseMap = new TileLayer({
|
||||
id: _id,
|
||||
visible: true,
|
||||
type: 'base',
|
||||
source: new XYZ({
|
||||
url: url,
|
||||
crossOrigin: 'anonymous',
|
||||
}),
|
||||
zIndex: 0,
|
||||
});
|
||||
return baseMap;
|
||||
},
|
||||
/**
|
||||
* [사용]테이블명으로 레이어 아이디 조회
|
||||
*/
|
||||
getLayerCodeByTbNm: function (_tableNm) {
|
||||
let returnLayerCode = null;
|
||||
for (let i in GisApp.layerCode) {
|
||||
let layerCode = GisApp.layerCode[i];
|
||||
if (layerCode.tableName === _tableNm) {
|
||||
returnLayerCode = layerCode;
|
||||
}
|
||||
}
|
||||
return returnLayerCode;
|
||||
},
|
||||
/**
|
||||
* [사용]베이스 레이어 변경
|
||||
* Parameters:
|
||||
* _title - {String} 베이스 레이어 이름
|
||||
*
|
||||
* Returns:
|
||||
*/
|
||||
changeBaseLayer: function (_id) {
|
||||
this.map.getLayers().forEach(function (layer) {
|
||||
let type = layer.get('type');
|
||||
let id = layer.get('id');
|
||||
if (type == 'base') {
|
||||
if (id == _id) {
|
||||
layer.setVisible(true);
|
||||
} else {
|
||||
layer.setVisible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* [사용]지도 중심좌표 설정
|
||||
*/
|
||||
setCenter: function (x, y, transYn) {
|
||||
if (!x || !y) return;
|
||||
let view = this.map.getView();
|
||||
|
||||
if (!transYn) {
|
||||
// 값이 없으면
|
||||
let center = olProj.transform([Number(y), Number(x)], GisApp.FROM_PROJ, GisApp.TO_PROJ);
|
||||
const style = new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 5,
|
||||
fill: new Fill({
|
||||
color: 'rgba(255,0,0,1)',
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: 'rgba(255,0,0,1)',
|
||||
width: 2,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
// 빨간색 점 추가 (스타일 적용)
|
||||
let redPoint = new Feature({
|
||||
geometry: new Point(center),
|
||||
});
|
||||
|
||||
let redPointLayer = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
features: [redPoint],
|
||||
}),
|
||||
style: style,
|
||||
});
|
||||
redPointLayer.set('id', 'redPointLayer');
|
||||
// 이전에 만들었던 빨간색 점 레이어 삭제
|
||||
this.deleteLayer('redPointLayer');
|
||||
|
||||
// 새로운 빨간색 점 레이어를 맵에 추가
|
||||
this.map.addLayer(redPointLayer);
|
||||
view.setCenter(center);
|
||||
} else {
|
||||
view.setCenter([Number(x), Number(y)]);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* [사용]주소검색 결과 지도이동
|
||||
*/
|
||||
setAddressResult: function (x, y, geom, addressName) {
|
||||
if (!x || !y) return;
|
||||
let view = this.map.getView();
|
||||
let center = olProj.transform([Number(y), Number(x)], GisApp.FROM_PROJ, GisApp.TO_PROJ);
|
||||
|
||||
const style = [
|
||||
new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 5,
|
||||
fill: new Fill({
|
||||
color: 'rgba(255,0,0,1)',
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: 'rgba(255,0,0,1)',
|
||||
width: 2,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: 'blue',
|
||||
width: 3,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: 'rgba(0, 0, 255, 0.1)',
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
||||
// 빨간색 점 추가 (스타일 적용)
|
||||
let redPoint = new Feature({
|
||||
geometry: new Point(center),
|
||||
});
|
||||
let redArea = new WKT().readFeature(geom, {
|
||||
dataProjection: GisApp.FROM_PROJ,
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
});
|
||||
|
||||
let redPointLayer = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
features: [redPoint, redArea],
|
||||
}),
|
||||
style: style,
|
||||
});
|
||||
|
||||
redPointLayer.set('id', 'redPointLayer');
|
||||
// 이전에 만들었던 빨간색 점 레이어 삭제
|
||||
this.deleteLayer('redPointLayer');
|
||||
|
||||
// 새로운 빨간색 점 레이어를 맵에 추가
|
||||
this.map.addLayer(redPointLayer);
|
||||
|
||||
view.animate({ center: center, zoom: 19 });
|
||||
return redPointLayer;
|
||||
},
|
||||
/*
|
||||
* [사용]zoom 설정
|
||||
*/
|
||||
setZoom: function (zoom) {
|
||||
if (!zoom) return;
|
||||
this.map.getView().setZoom(zoom);
|
||||
},
|
||||
/*
|
||||
* [사용]zoom 정보 반환
|
||||
*/
|
||||
getZoom: function () {
|
||||
return this.map.getView().getZoom();
|
||||
},
|
||||
/**
|
||||
* [사용]레이어 맵에 추가(외부에서 생성된 layer를 파라미터로 받아 맵에 추가)
|
||||
*/
|
||||
setWfsLayerOnMap: function (layer) {
|
||||
this.deleteLayer(layer.get('id'));
|
||||
this.map.addLayer(layer);
|
||||
return layer;
|
||||
},
|
||||
/**
|
||||
* [사용]레이어 생성(지오서버에서 WFS 방식으로 레이어정보 가져와 레이어 생성)
|
||||
*/
|
||||
getWfsLayer: function (layerCodeObj) {
|
||||
return GisApp.Module.layer.getWfsLayer(layerCodeObj);
|
||||
},
|
||||
/**
|
||||
* [사용]레이어 맵에 추가(getWfsLayer 사용하여 레이어 생성)
|
||||
*/
|
||||
addWfsLayer: function (layerCodeObj) {
|
||||
let layer = this.getWfsLayer(layerCodeObj);
|
||||
this.deleteLayer(layer.get('id'));
|
||||
this.map.addLayer(layer);
|
||||
return layer;
|
||||
},
|
||||
/**
|
||||
* [사용]레이어 맵에 추가(getWmsLayer 사용하여 레이어 생성)
|
||||
*/
|
||||
addWmsLayer: function (layerCodeObj) {
|
||||
let layer = GisApp.Module.layer.getWmsLayer(layerCodeObj);
|
||||
this.deleteLayer(layer.get('id'));
|
||||
this.map.addLayer(layer);
|
||||
return layer;
|
||||
},
|
||||
/**
|
||||
* [사용]레이어 삭제
|
||||
*/
|
||||
deleteLayer: function (layerId) {
|
||||
const map = this.map;
|
||||
if (!layerId) return;
|
||||
map.getLayers().forEach(function (layer) {
|
||||
let deleteLayer = null;
|
||||
if (layer != null && layer.get('id') != null) {
|
||||
let vlayerId = layer.get('id');
|
||||
if (vlayerId) {
|
||||
if (vlayerId === layerId) {
|
||||
deleteLayer = layer;
|
||||
} else if (`${vlayerId}`.includes(`${layerId}`)) {
|
||||
deleteLayer = layer;
|
||||
}
|
||||
if (deleteLayer) {
|
||||
map.removeLayer(deleteLayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* [사용]레이어 전체 삭제
|
||||
*/
|
||||
deleteAllLayer: function () {
|
||||
const map = this.map;
|
||||
map.getLayers().forEach(function (layer) {
|
||||
if (layer?.get('type') !== 'base') {
|
||||
map.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
//맵에 보이지 않는 레이어 삭제
|
||||
GisApp.Module.layer?.layers.forEach(function (layer) {
|
||||
map.removeLayer(layer);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* [사용]레이어이름으로 레이어 조회 반환
|
||||
*/
|
||||
getLayerById: function (layerId) {
|
||||
if (!layerId) return;
|
||||
let layer = null;
|
||||
this.map.getAllLayers().forEach(function (el) {
|
||||
if (el != null && el?.get('id') != null) {
|
||||
let vlayerId = el?.get('id');
|
||||
if (Number(vlayerId) === Number(layerId)) {
|
||||
layer = el;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (layer != null) {
|
||||
GisApp.Module.layer.layers.forEach(function (el) {
|
||||
if (el != null && el?.get('id') != null) {
|
||||
let vlayerId = el?.get('id');
|
||||
if (Number(vlayerId) === Number(layerId)) {
|
||||
layer = el;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return layer;
|
||||
},
|
||||
/**
|
||||
* [사용]오버레이 전체 삭제
|
||||
*/
|
||||
deleteAllOverlay: function () {
|
||||
let map = this.map;
|
||||
let overlays = map.getOverlays();
|
||||
if (overlays != null) {
|
||||
let overlayArray = overlays.getArray();
|
||||
let overlaySize = overlayArray.length;
|
||||
if (overlaySize > 0) {
|
||||
for (let i = overlaySize - 1; i >= 0; i--) {
|
||||
let overlay = overlayArray[i];
|
||||
map.removeOverlay(overlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.values(GisApp.Module.style.overlays).forEach(overlay => {
|
||||
if (overlay) {
|
||||
map.removeOverlay(overlay);
|
||||
}
|
||||
});
|
||||
GisApp.Module.style.overlays = {};
|
||||
},
|
||||
/**
|
||||
* [사용]오버레이 아이디로 삭제
|
||||
*/
|
||||
deleteOverlay: function (_id) {
|
||||
let map = this.map;
|
||||
let overlays = map.getOverlays();
|
||||
if (overlays != null) {
|
||||
let overlayArray = overlays.getArray();
|
||||
let overlaySize = overlayArray.length;
|
||||
if (overlaySize > 0) {
|
||||
for (let i = overlaySize - 1; i >= 0; i--) {
|
||||
let overlay = overlayArray[i];
|
||||
let id = overlay.getId();
|
||||
if (id === _id) {
|
||||
map.removeOverlay(overlay);
|
||||
delete GisApp.Module.style.overlays[_id];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [사용]맵 이벤트 삭제
|
||||
*/
|
||||
removeMapEventFnNm: function (handlerNm, fnNm) {
|
||||
let map = this.map;
|
||||
for (let i in map.listeners_[handlerNm]) {
|
||||
let listener = map.listeners_[handlerNm][i];
|
||||
if (listener.name == fnNm) {
|
||||
map.listeners_[handlerNm].splice(i, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [사용]위경도 -> 시분초 변환
|
||||
*/
|
||||
coordTrans: function (coord) {
|
||||
let str = String(coord); /* String 변환 */
|
||||
let split = str.split('.'); /* 소수점 split */
|
||||
let hour = split[0]; /* 시간 */
|
||||
let minute = parseFloat((coord - parseInt(split[0]).toFixed(6)) * 60).toFixed(6);
|
||||
let min = String(minute).split('.')[0];
|
||||
let second = parseFloat((minute - parseInt(min)) * 60).toFixed(2);
|
||||
return hour + '도 ' + min + '분 ' + second + '초';
|
||||
},
|
||||
/**
|
||||
* [사용]시분초 -> 위경도 변환
|
||||
*/
|
||||
coordTransrate: function (degree, minute, second) {
|
||||
let min = (Number(minute) / 60).toFixed(2);
|
||||
let sec = (Number(second) / 3600).toFixed(4);
|
||||
return degree + Number(min) + Number(sec);
|
||||
},
|
||||
/**
|
||||
* [사용]마우스 좌표 위치
|
||||
*/
|
||||
mousePositionControl: function () {
|
||||
let element = document.getElementById('label_lonlat');
|
||||
let me = this;
|
||||
let mousePosition = new MousePosition({
|
||||
coordinateFormat: function (coordinate) {
|
||||
let text = '';
|
||||
if (element) {
|
||||
let lng = coordinate[0];
|
||||
let lat = coordinate[1];
|
||||
text += '경도 : ' + lng.toFixed(7) + ' ( ' + me.coordTrans(lng) + ' ) ';
|
||||
text += '위도 : ' + lat.toFixed(7) + '( ' + me.coordTrans(lat) + ' )';
|
||||
}
|
||||
return text;
|
||||
},
|
||||
projection: GisApp.FROM_PROJ,
|
||||
className: 'custom-mouse-position',
|
||||
target: document.getElementById('label_lonlat'),
|
||||
});
|
||||
this.map.addControl(mousePosition);
|
||||
},
|
||||
/**
|
||||
* [사용]스케일 표시
|
||||
*/
|
||||
scaleControl: function () {
|
||||
let scale = new ScaleLine({
|
||||
minWidth: 100,
|
||||
units: 'metric',
|
||||
});
|
||||
this.map.addControl(scale);
|
||||
},
|
||||
/**
|
||||
* [사용]레이어 맵에 셋팅
|
||||
*
|
||||
* Parameters:
|
||||
* layerList - {Array} [layerNm : GisApp.layerCode 키]
|
||||
* visible - {Boolean} 레이어 visible 여부
|
||||
*
|
||||
* Returns:
|
||||
*/
|
||||
setLayerOnMap: function (layerIds, visible, addQurryFlter) {
|
||||
for (let i in layerIds) {
|
||||
let id = layerIds[i];
|
||||
let layer = this.getLayerById(id);
|
||||
if (layer) {
|
||||
layer.setVisible(visible);
|
||||
} else if (visible) {
|
||||
const layerCode = GisApp.layerCode[id];
|
||||
const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || '');
|
||||
layerCode['flterSql'] = queryFlter;
|
||||
//layer = this.addWmsLayer(layerCode);
|
||||
layer = this.addWfsLayer(layerCode);
|
||||
}
|
||||
}
|
||||
if (GisApp.Module.select) {
|
||||
this.setSelectInteraction();
|
||||
}
|
||||
if (GisApp.Module.edit.snap) {
|
||||
GisApp.Module.edit.setSnap();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [사용]shp 파일 다운로드
|
||||
*/
|
||||
getShapeLayer: function (option) {
|
||||
GisApp.Module.layer.getShapeLayer(option);
|
||||
},
|
||||
/**
|
||||
* [사용]레이어 셀렉트 가능하게 셋팅
|
||||
*/
|
||||
setSelectInteraction: function () {
|
||||
let layers = this.map.getLayers();
|
||||
if (layers) {
|
||||
for (let i in layers.array_) {
|
||||
let layer = layers.array_[i];
|
||||
if (layer.get('chcYn') === 'Y') {
|
||||
GisApp.Module.select.addLayer(layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [사용]layerInfos(array) To layerCode(Object)
|
||||
*/
|
||||
convertLayerInfosToLayerCode: function (array, key, parentName) {
|
||||
let initialValue = {};
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
let item = {};
|
||||
Object.assign(item, array[i]);
|
||||
if (parentName) {
|
||||
item.parentName = parentName;
|
||||
}
|
||||
initialValue[item[key]] = item;
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
let child = this.convertLayerInfosToLayerCode(item.children, key, item.name);
|
||||
Object.assign(initialValue, child);
|
||||
}
|
||||
if (initialValue[item[key]].children) {
|
||||
delete initialValue[item[key]].children;
|
||||
}
|
||||
}
|
||||
return initialValue;
|
||||
},
|
||||
/**
|
||||
* [사용]피처 좌표값 WKT포맷으로 변경
|
||||
*/
|
||||
convertFeatureGeomToWKT: function (feature) {
|
||||
let geometry = feature.getGeometry();
|
||||
const wktGeom = this.convertGeomToWkt(geometry);
|
||||
return wktGeom;
|
||||
},
|
||||
/**
|
||||
* [사용]WKT포맷 Geometry로 변경
|
||||
*/
|
||||
convertWktToGeom: function (wtkGeom) {
|
||||
let format = new WKT();
|
||||
let geom = format.readGeometry(wtkGeom);
|
||||
return geom;
|
||||
},
|
||||
/**
|
||||
* [사용]좌표와 properties의 키값으로 피처 검색
|
||||
*/
|
||||
getFeatureGeomProp: function (geometry, ftrIds, ftrCde) {
|
||||
let returnFeature;
|
||||
this.map.getLayers().forEach(function (layer) {
|
||||
let type = layer.get('type');
|
||||
if (type != 'base') {
|
||||
let source = layer.getSource();
|
||||
|
||||
let type = geometry.getType();
|
||||
let coordinates = geometry.getCoordinates();
|
||||
if (type.toUpperCase() === 'POINT') {
|
||||
coordinates = [coordinates];
|
||||
} else {
|
||||
coordinates = coordinates[0];
|
||||
}
|
||||
let featureFtrIds;
|
||||
let featureFtrCde;
|
||||
for (let i in coordinates) {
|
||||
let coordinate = coordinates[i];
|
||||
let feature = source.getClosestFeatureToCoordinate(coordinate);
|
||||
if (feature) {
|
||||
let properties = feature.getProperties();
|
||||
featureFtrIds = properties['FTR_IDS'];
|
||||
featureFtrCde = properties['FTR_CDE'];
|
||||
if (ftrIds == featureFtrIds && ftrCde == featureFtrCde) {
|
||||
returnFeature = feature;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return returnFeature;
|
||||
},
|
||||
/**
|
||||
* [사용]관리번호로 피처검색
|
||||
*/
|
||||
getFeatureByFtrIdn: function (typeName, ftrIdn) {
|
||||
let returnFeature;
|
||||
this.map.getLayers().forEach(function (layer) {
|
||||
let layerTypeName = layer.get('typeName');
|
||||
if (layerTypeName === typeName) {
|
||||
layer
|
||||
.getSource()
|
||||
.getFeatures()
|
||||
.forEach(feature => {
|
||||
let featureFtrIdn = feature.get('ftrIdn');
|
||||
if (featureFtrIdn === ftrIdn) {
|
||||
returnFeature = feature;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return returnFeature;
|
||||
},
|
||||
/**
|
||||
* [사용]맵 이벤트 추가
|
||||
*/
|
||||
addMapEvent: function (evtNm, id, callback) {
|
||||
if (id) {
|
||||
let evt = this.map.on(evtNm, callback);
|
||||
evt.listener.id = id;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [사용]맵 이벤트 삭제
|
||||
*/
|
||||
removeMapEvent: function (evtNm, id) {
|
||||
if (id) {
|
||||
let listeners = this.map.listeners_[evtNm];
|
||||
for (let i in listeners) {
|
||||
let listenter = listeners[i];
|
||||
if (listenter.id == id) {
|
||||
listeners.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [사용]마커 그리기 추가
|
||||
*/
|
||||
drawMarker: function (layer, callback) {
|
||||
let map = this.map;
|
||||
let drawSource = layer.getSource();
|
||||
let draw = new Draw({
|
||||
type: 'Point',
|
||||
source: drawSource,
|
||||
condition: function (evt) {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
map.addInteraction(draw);
|
||||
draw.on('drawend', function (event) {
|
||||
callback(event);
|
||||
map.removeInteraction(draw);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* [사용]피처 삭제
|
||||
*/
|
||||
deleteFeature: function (layerId, featureId) {
|
||||
if (!layerId || !featureId) return;
|
||||
let layer = this.getLayerById(layerId);
|
||||
if (layer != null) {
|
||||
let feature = layer.getSource().getFeatureById(featureId);
|
||||
if (feature != null) {
|
||||
layer.getSource().removeFeature(feature);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [사용]레이어
|
||||
*/
|
||||
fitExtent: function (extent, zoom) {
|
||||
let map = this.map;
|
||||
let mapSize = map.getSize();
|
||||
if (!zoom) {
|
||||
zoom = this.getZoom();
|
||||
}
|
||||
if (extent) {
|
||||
//map.getView().fit(extent, { size: [mapSize[0], mapSize[1]] });
|
||||
this.map.getView().fit(extent, {
|
||||
maxZoom: 18, // 너무 확대되지 않도록 최대 줌 제한
|
||||
duration: 500, // 부드러운 애니메이션 효과 (밀리초 단위)
|
||||
});
|
||||
}
|
||||
this.setZoom(zoom);
|
||||
},
|
||||
getGeomLineOfFeature: function (feature) {
|
||||
if (!feature) {
|
||||
return;
|
||||
}
|
||||
let geometry = feature.getGeometry();
|
||||
let type = geometry.getType();
|
||||
if (!(type == 'LineString' || type == 'MultiLineString')) {
|
||||
return;
|
||||
}
|
||||
let geomLienString = geometry;
|
||||
if (type != 'LineString') {
|
||||
//lienString = geometry.getLineString();
|
||||
}
|
||||
return geomLienString;
|
||||
},
|
||||
getCenterGeomLineOfFeature: function (feature) {
|
||||
if (!feature) {
|
||||
return;
|
||||
}
|
||||
let center = [];
|
||||
let geomLienString = this.getGeomLineOfFeature(feature);
|
||||
let coords = geomLienString.getCoordinates();
|
||||
let coordCnt = coords.length;
|
||||
if (coordCnt == 1) {
|
||||
coords = coords[0];
|
||||
}
|
||||
//라인 한개
|
||||
if (coordCnt == 2) {
|
||||
return geomLienString;
|
||||
}
|
||||
//멀티라인
|
||||
let centerCnt = Math.floor(coordCnt / 2);
|
||||
let lineCoordinates = [];
|
||||
lineCoordinates.push(coords[centerCnt - 1]);
|
||||
lineCoordinates.push(coords[centerCnt]);
|
||||
geomLienString = new LinearRing(lineCoordinates);
|
||||
return geomLienString;
|
||||
},
|
||||
getCenterOfLineGeom: function (geom) {
|
||||
let center = [];
|
||||
let extent = geom.getExtent();
|
||||
let x = extent[0] + (extent[2] - extent[0]) / 2;
|
||||
let y = extent[1] + (extent[3] - extent[1]) / 2;
|
||||
center.push(x);
|
||||
center.push(y);
|
||||
return center;
|
||||
},
|
||||
/**
|
||||
* 1 -> 01 두자리수로 변환
|
||||
*/
|
||||
fillZeroToZoomLevel: function (n, digits) {
|
||||
let zero = '';
|
||||
n = n.toString();
|
||||
if (digits > n.length) {
|
||||
for (let i = 0; digits - n.length > i; i++) {
|
||||
zero += '0';
|
||||
}
|
||||
}
|
||||
return zero + n;
|
||||
},
|
||||
convertGeoJsonToFeature: function (data, dataProjection) {
|
||||
let format = new GeoSJON();
|
||||
let feature = format.readFeature(data, {
|
||||
dataProjection: dataProjection || GisApp.TO_DATA_PROJ,
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
});
|
||||
return feature;
|
||||
},
|
||||
convertGeoJsonToFeatures: function (data, dataProjection) {
|
||||
let format = new GeoSJON();
|
||||
const features = [];
|
||||
for (let obj of data) {
|
||||
let feature = null;
|
||||
try {
|
||||
feature = format.readFeature(obj, {
|
||||
dataProjection: dataProjection || GisApp.TO_DATA_PROJ,
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(obj);
|
||||
}
|
||||
features.push(feature);
|
||||
}
|
||||
return features;
|
||||
},
|
||||
|
||||
/**
|
||||
* [사용]WKT포맷 Geometry로 feature 생성
|
||||
*/
|
||||
convertWktToFeature: function (wtkGeom, dataProjection) {
|
||||
let format = new WKT();
|
||||
let feature = format.readFeature(wtkGeom, {
|
||||
dataProjection: dataProjection || GisApp.TO_DATA_PROJ,
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
});
|
||||
return feature;
|
||||
},
|
||||
|
||||
/**
|
||||
* [사용]geometry를 WKT포맷으로 변경
|
||||
*/
|
||||
convertGeomToWkt: function (geometry) {
|
||||
let format = new WKT();
|
||||
let wktGeom = format.writeGeometry(geometry, {
|
||||
dataProjection: GisApp.TO_DATA_PROJ,
|
||||
featureProjection: GisApp.TO_PROJ,
|
||||
});
|
||||
return wktGeom;
|
||||
},
|
||||
/**
|
||||
* [사용]레이어 전체 표시 컨트롤
|
||||
*/
|
||||
setAllLayersOnMap: function (visible) {
|
||||
this.map.getLayers().forEach(function (el) {
|
||||
if (el !== null && el.get('id') !== null) {
|
||||
let layerId = el.get('id');
|
||||
if ('base' !== layerId && 'satellite' !== layerId && 'minimap-base' !== layerId) {
|
||||
el.setVisible(visible);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
fitExtentZoomFeatures: function (features, zoom, paddingX = 0, paddingY = 0) {
|
||||
if (!features.length) return;
|
||||
// Feature들의 경계를 계산하여 extent 생성
|
||||
const extent = this.getExtentFeatures(features);
|
||||
if (zoom) {
|
||||
//const paddingX = 0; // X축 이동량 (양수는 오른쪽, 음수는 왼쪽)
|
||||
//const paddingY = -10000; // Y축 이동량 (양수는 아래쪽, 음수는 위쪽)
|
||||
extent[0] -= paddingX;
|
||||
extent[1] -= paddingY;
|
||||
this.fitExtent(extent, zoom);
|
||||
} else {
|
||||
this.map.getView().fit(extent, {
|
||||
maxZoom: 18, // 너무 확대되지 않도록 최대 줌 제한
|
||||
duration: 500, // 부드러운 애니메이션 효과 (밀리초 단위)
|
||||
});
|
||||
}
|
||||
},
|
||||
getExtentFeatures: function (features) {
|
||||
if (!features.length) return;
|
||||
let extent = [];
|
||||
const allCoordinates = features
|
||||
.map(feature => {
|
||||
const geometry = feature.getGeometry();
|
||||
if (!geometry || typeof geometry.getCoordinates !== 'function') {
|
||||
return null; // geometry가 없거나 getCoordinates() 메서드가 없으면 null 반환
|
||||
}
|
||||
return geometry.getCoordinates();
|
||||
})
|
||||
.filter(coords => coords !== null);
|
||||
function extractCoordinatePairs(arr) {
|
||||
let result = [];
|
||||
|
||||
function recursiveFlatten(subArray) {
|
||||
if (!Array.isArray(subArray)) return;
|
||||
|
||||
for (let item of subArray) {
|
||||
if (Array.isArray(item) && item.length === 2 && typeof item[0] === 'number' && typeof item[1] === 'number') {
|
||||
// [숫자, 숫자] 형태의 배열이면 추가
|
||||
result.push(item);
|
||||
} else {
|
||||
// 그렇지 않다면 재귀 호출
|
||||
recursiveFlatten(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recursiveFlatten(arr);
|
||||
return result;
|
||||
}
|
||||
|
||||
const flattenCoord = extractCoordinatePairs(allCoordinates);
|
||||
extent = boundingExtent(flattenCoord);
|
||||
return extent;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* [사용]피처 레이어 검색
|
||||
*/
|
||||
Feature.prototype.getLayer = function () {
|
||||
let map = GisApp.Module.core.map;
|
||||
let this_ = this,
|
||||
layer_;
|
||||
let sameFeature = function (feature) {
|
||||
return this_ === feature ? true : false;
|
||||
};
|
||||
map.getLayers().forEach(function (layer) {
|
||||
let source = layer.getSource();
|
||||
if (source instanceof VectorSource) {
|
||||
let features = source.getFeatures();
|
||||
if (features.length > 0) {
|
||||
let found = features.some(sameFeature);
|
||||
if (found) {
|
||||
layer_ = layer;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return layer_;
|
||||
};
|
||||
|
||||
export default GisApp;
|
||||
@@ -0,0 +1,521 @@
|
||||
import GisApp from './gis.core';
|
||||
import Draw from 'ol/interaction/Draw.js';
|
||||
import Modify from 'ol/interaction/Modify.js';
|
||||
import Collection from 'ol/Collection.js';
|
||||
import Snap from 'ol/interaction/Snap.js';
|
||||
import { click, always } from 'ol/events/condition';
|
||||
import UndoRedo from 'ol-ext/interaction/UndoRedo.js';
|
||||
import Transform from 'ol-ext/interaction/Transform.js';
|
||||
import { gAlert } from './util';
|
||||
import VectorSource from 'ol/source/Vector.js';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
|
||||
const GisOlEditLayer = function (options) {
|
||||
this.initialize(options);
|
||||
};
|
||||
|
||||
GisOlEditLayer.prototype = {
|
||||
defaults: {},
|
||||
editFlag: false,
|
||||
draw: null,
|
||||
modify: null,
|
||||
snap: null,
|
||||
undoredo: null,
|
||||
transform: null,
|
||||
drawType: {
|
||||
point: 'Point',
|
||||
line: 'MultiLineString',
|
||||
poligon: 'MultiPolygon',
|
||||
},
|
||||
selectedLayerId: null, //셀렉트 박스에서 선택된 레이어 ID
|
||||
targetElement: null,
|
||||
editFeatureInfo: {},
|
||||
addFeatureNo: 1,
|
||||
initialize: function (options) {
|
||||
Object.assign(this.defaults, options);
|
||||
},
|
||||
/**
|
||||
* [공통] Interaction 추가
|
||||
*/
|
||||
addInteration: function () {
|
||||
this.setUndoInteraction();
|
||||
},
|
||||
/**
|
||||
* [공통] Interaction 삭제
|
||||
*/
|
||||
removeInteraction: function () {
|
||||
this.removeDraw();
|
||||
this.removeModify();
|
||||
this.removeSnap();
|
||||
this.clearUndoRedoFeature();
|
||||
},
|
||||
/**
|
||||
* [공통] Interaction Active 컨트롤
|
||||
*/
|
||||
setInteractionActive: function (active) {
|
||||
this.setDrawActive(active);
|
||||
this.setModifyActive(active);
|
||||
this.setActiveUndo(active);
|
||||
},
|
||||
/**
|
||||
* [그리기] 기능 추가
|
||||
*/
|
||||
setDraw: function (lyrId) {
|
||||
if (!lyrId) return '레이어를 선택해주십시오.';
|
||||
const core = GisApp.Module.core;
|
||||
const layer = core.getLayerById(lyrId);
|
||||
const layerCode = GisApp.layerCode[lyrId];
|
||||
if (!layer) return '트리에서 레이어 체크 후 추가 가능합니다.';
|
||||
const lyrTy = layerCode.gobjTy;
|
||||
|
||||
//초기화
|
||||
this.removeModify();
|
||||
this.removeTransform();
|
||||
if (GisApp.Module.select) {
|
||||
GisApp.Module.select.setActive(false);
|
||||
}
|
||||
|
||||
const originalSource = layer.getSource();
|
||||
const drawSource = new VectorSource({
|
||||
features: originalSource.getFeatures(), // 기존 feature들 복사
|
||||
});
|
||||
|
||||
// 새로운 임시 레이어 추가 (draw 전용)
|
||||
const drawLayer = new VectorLayer({
|
||||
source: drawSource,
|
||||
minZoom: 1, // 이 레이어만 minZoom을 1로 설정
|
||||
style: layer.getStyle(),
|
||||
});
|
||||
core.map.addLayer(drawLayer);
|
||||
|
||||
//그리기 기능 추가
|
||||
const me = this;
|
||||
|
||||
if (me.draw) {
|
||||
core.map.removeInteraction(me.draw);
|
||||
}
|
||||
const drawType = this.drawType[lyrTy];
|
||||
this.draw = new Draw({
|
||||
type: drawType,
|
||||
source: drawSource,
|
||||
condition: function (evt) {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
core.map.addInteraction(this.draw);
|
||||
|
||||
//그리기 기능 이벤트
|
||||
this.draw.on('drawstart', function (event) {});
|
||||
|
||||
const drawFeatureCallback = function (event) {
|
||||
me.setActiveUndo(true);
|
||||
const drawFeature = event.feature;
|
||||
drawFeature.setId(lyrId + '_feature_' + me.addFeatureNo++);
|
||||
|
||||
const properties = {};
|
||||
properties['lyr_id'] = lyrId;
|
||||
drawFeature.setProperties(properties);
|
||||
|
||||
me.setAddFeatureUndo(layer, drawFeature);
|
||||
me.setActiveUndo(false);
|
||||
};
|
||||
|
||||
this.draw.on('drawend', function (event) {
|
||||
drawFeatureCallback(event);
|
||||
});
|
||||
|
||||
/*drawSource.listeners_.addfeature = null;
|
||||
let addfeatureCallback = function(event){
|
||||
}
|
||||
drawSource.on('addfeature', addfeatureCallback);*/
|
||||
},
|
||||
/**
|
||||
* [그리기] 기능 제거
|
||||
*/
|
||||
removeDraw: function () {
|
||||
let core = GisApp.Module.core;
|
||||
core.map.removeInteraction(this.draw);
|
||||
if (GisApp.Module.select) {
|
||||
GisApp.Module.select.setActive(true);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [그리기] 기능 활성화
|
||||
*/
|
||||
setDrawActive: function (active) {
|
||||
if (this.draw) {
|
||||
this.draw.setActive(active);
|
||||
if (!active) {
|
||||
/* $('#selectEditLayer').css('display', 'none');
|
||||
$('.btn_edit_add').removeClass('active'); */
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [히스토리] UndoInteraction 추가
|
||||
*/
|
||||
setUndoInteraction: function () {
|
||||
let core = GisApp.Module.core;
|
||||
if (!this.undoredo) {
|
||||
this.undoredo = new UndoRedo();
|
||||
core.map.addInteraction(this.undoredo);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [히스토리] UndoInteraction 제거
|
||||
*/
|
||||
removeUndoInteraction: function () {
|
||||
let core = GisApp.Module.core;
|
||||
if (this.undoredo) {
|
||||
core.map.removeInteraction(this.undoredo);
|
||||
this.undoredo = null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [히스토리] UndoInteraction 활성화
|
||||
*/
|
||||
setActiveUndo: function (active) {
|
||||
if (this.undoredo) {
|
||||
this.undoredo.setActive(active);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [히스토리] 피처 추가
|
||||
*/
|
||||
setAddFeatureUndo: function (layer, addFeature) {
|
||||
if (this.undoredo) {
|
||||
let obj = {};
|
||||
obj.target = layer.getSource();
|
||||
obj.feature = addFeature;
|
||||
obj.type = 'addfeature';
|
||||
obj.feature.set('uploadType', 'addFeature');
|
||||
this.undoredo._onAddRemove(obj);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [히스토리] 피처 삭제
|
||||
*/
|
||||
setRemoveFeatureUndo: function (layer, removeFeature) {
|
||||
if (this.undoredo) {
|
||||
let obj = {};
|
||||
obj.target = layer.getSource();
|
||||
obj.feature = removeFeature;
|
||||
obj.type = 'removefeature';
|
||||
if (obj.feature.get('uploadType') !== 'addFeature') {
|
||||
obj.feature.set('uploadType', 'removeFeature');
|
||||
}
|
||||
this.undoredo._onAddRemove(obj);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [히스토리] 이전
|
||||
*/
|
||||
undo: function () {
|
||||
if (this.undoredo) {
|
||||
if (this.undoredo.hasUndo() === 0) {
|
||||
gAlert('INFO', 'NO_PREV_HISTORY');
|
||||
return;
|
||||
}
|
||||
this.setActiveUndo(true);
|
||||
this.undoredo.undo();
|
||||
this.setActiveUndo(false);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [히스토리] 다음
|
||||
*/
|
||||
redo: function () {
|
||||
if (this.undoredo) {
|
||||
if (this.undoredo.hasRedo() === 0) {
|
||||
gAlert('INFO', 'NO_NEXT_HISTORY');
|
||||
return;
|
||||
}
|
||||
this.setActiveUndo(true);
|
||||
this.undoredo.redo();
|
||||
this.setActiveUndo(false);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [히스토리] 초기화
|
||||
*/
|
||||
clearUndoRedo: function () {
|
||||
if (this.undoredo) {
|
||||
const undoStack = this.undoredo._undoStack.array_;
|
||||
for (let i in undoStack) {
|
||||
const undo = undoStack[i];
|
||||
if (undo.feature) {
|
||||
undo.feature.set('uploadType', null);
|
||||
}
|
||||
}
|
||||
this.undoredo.clear();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [히스토리] 피처 초기화
|
||||
*/
|
||||
clearUndoRedoFeature: function () {
|
||||
if (this.undoredo) {
|
||||
this.setActiveUndo(true);
|
||||
const undoStack = [...this.undoredo._undoStack.array_];
|
||||
for (let i in undoStack) {
|
||||
const undo = undoStack[i];
|
||||
if (undo.feature) {
|
||||
this.undoredo.undo();
|
||||
}
|
||||
}
|
||||
this.setActiveUndo(false);
|
||||
this.undoredo.clear();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [삭제] 피처 삭제
|
||||
*/
|
||||
deleteFeature: function () {
|
||||
//초기화
|
||||
this.removeDraw();
|
||||
this.removeModify();
|
||||
this.removeTransform();
|
||||
|
||||
//삭제 기능 실행
|
||||
let me = this;
|
||||
let core = GisApp.Module.core;
|
||||
let select = GisApp.Module.select;
|
||||
select.getSelectedFeatures().forEach(function (selectedFeature) {
|
||||
let selectedFeatureId = selectedFeature.getId();
|
||||
let layers = core.map.getLayers();
|
||||
for (let i in layers.array_) {
|
||||
let layer = layers.array_[i];
|
||||
if (layer.get('mdfcnYn') === 'Y' && select.getVisible(layer)) {
|
||||
layer
|
||||
.getSource()
|
||||
.getFeatures()
|
||||
.forEach(function (feature) {
|
||||
let featureId = feature.getId();
|
||||
if (selectedFeatureId === featureId) {
|
||||
me.setActiveUndo(true);
|
||||
feature.setProperties({ lyr_id: layer.get('id') });
|
||||
layer.getSource().removeFeature(feature);
|
||||
me.setRemoveFeatureUndo(layer, feature);
|
||||
me.setActiveUndo(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* [수정] 피처 수정 기능 셋팅
|
||||
*/
|
||||
setModify: function () {
|
||||
//초기화
|
||||
this.removeDraw();
|
||||
this.removeTransform();
|
||||
|
||||
//수정기능 추가
|
||||
let me = this;
|
||||
let core = GisApp.Module.core;
|
||||
let select = GisApp.Module.select;
|
||||
let selectedFeatures = select.getSelectedFeatures();
|
||||
|
||||
this.modify = new Modify({
|
||||
features: selectedFeatures,
|
||||
});
|
||||
core.map.addInteraction(this.modify);
|
||||
|
||||
select.selectInteraction.on('change:active', function () {
|
||||
selectedFeatures.forEach(function (each) {
|
||||
selectedFeatures.remove(each);
|
||||
});
|
||||
});
|
||||
|
||||
this.modify.on('modifystart', function (event) {});
|
||||
|
||||
this.modify.on('modifyend', function (event) {
|
||||
let features = event.features.getArray();
|
||||
for (let i = 0; i < features.length; i++) {
|
||||
let feature = features[i];
|
||||
let layer = select.selectInteraction.getLayer(feature);
|
||||
feature.setProperties({ lyr_id: layer.get('id') });
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* [수정] 피처 수정 기능 제거
|
||||
*/
|
||||
removeModify: function () {
|
||||
if (this.modify) {
|
||||
let core = GisApp.Module.core;
|
||||
core.map.removeInteraction(this.modify);
|
||||
this.modify = null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [수정] 기능 활성화
|
||||
*/
|
||||
setModifyActive: function (active) {
|
||||
if (this.modify) {
|
||||
this.modify.setActive(active);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [크기조절] 피처 크기조절 기능 셋팅
|
||||
*/
|
||||
setTransform: function () {
|
||||
//초기화
|
||||
this.removeDraw();
|
||||
this.removeModify();
|
||||
|
||||
let me = this;
|
||||
let core = GisApp.Module.core;
|
||||
let select = GisApp.Module.select;
|
||||
|
||||
this.transform = new Transform({
|
||||
enableRotatedTransform: false,
|
||||
addCondition: click,
|
||||
filter: function (f, l) {
|
||||
return f.getGeometry().getType() !== 'Point';
|
||||
},
|
||||
// layers: [vector],
|
||||
hitTolerance: select.defaults.hitTolerance,
|
||||
translateFeature: true,
|
||||
scale: true,
|
||||
rotate: true,
|
||||
keepAspectRatio: always,
|
||||
translate: true,
|
||||
stretch: true,
|
||||
});
|
||||
this.transform.on(['rotateend', 'translateend', 'scaleend'], function (event) {
|
||||
let features = event.features.getArray();
|
||||
for (let i = 0; i < features.length; i++) {
|
||||
let feature = features[i];
|
||||
let layer = feature.getLayer();
|
||||
feature.setProperties({ lyr_id: layer.get('id') });
|
||||
}
|
||||
});
|
||||
|
||||
core.map.addInteraction(this.transform);
|
||||
},
|
||||
/**
|
||||
* [크기조절] 피처 크기조절 기능 제거
|
||||
*/
|
||||
removeTransform: function () {
|
||||
if (this.transform) {
|
||||
let core = GisApp.Module.core;
|
||||
core.map.removeInteraction(this.transform);
|
||||
this.transform = null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [크기조절] 피처 크기조절 기능 활성화
|
||||
*/
|
||||
setTransformActive: function (active) {
|
||||
if (this.transform) {
|
||||
this.transform.setActive(active);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [스냅] 기능 추가
|
||||
*/
|
||||
setSnap: function (active) {
|
||||
//초기화
|
||||
this.removeSnap();
|
||||
|
||||
//스냅 기능 추가
|
||||
let core = GisApp.Module.core;
|
||||
let select = GisApp.Module.select;
|
||||
core.map.removeInteraction(this.snap);
|
||||
let features = [];
|
||||
let layers = core.map.getLayers();
|
||||
for (let i in layers.array_) {
|
||||
let layer = layers.array_[i];
|
||||
if (select.getVisible(layer) && layer.get('type') !== 'base') {
|
||||
if (layer.getSource().getFeatures().length > 0) {
|
||||
features = features.concat(layer.getSource().getFeatures());
|
||||
}
|
||||
}
|
||||
}
|
||||
let snapCollection = new Collection(features);
|
||||
this.snap = new Snap({
|
||||
features: snapCollection,
|
||||
pixelTolerance: 30,
|
||||
});
|
||||
core.map.addInteraction(this.snap);
|
||||
},
|
||||
/**
|
||||
* [스냅] 기능 제거
|
||||
*/
|
||||
removeSnap: function () {
|
||||
if (this.snap) {
|
||||
let core = GisApp.Module.core;
|
||||
core.map.removeInteraction(this.snap);
|
||||
this.snap = null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [스냅] 스냅에 피처 추가
|
||||
*/
|
||||
addSnap: function (layers) {
|
||||
let me = this;
|
||||
if (me.snap) {
|
||||
for (let i in layers) {
|
||||
let source = layers[i].getSource();
|
||||
source.listeners_.addfeature = null;
|
||||
source.on('addfeature', function (event) {
|
||||
me.snap.addFeature(event.feature);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* [스냅] 기능 활성화
|
||||
*/
|
||||
setSnapActive: function (active) {
|
||||
if (this.snap) {
|
||||
this.snap.setActive(active);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* [저장] 피처 수정 정보 셋팅
|
||||
*/
|
||||
setEditFeatureInfo: function (flag, feature) {
|
||||
const featureId = feature.getId();
|
||||
const properties = feature.getProperties();
|
||||
|
||||
//신규 추가 후 삭제시에 editFeatureInfo에서 제거
|
||||
if (properties.uploadType === 'addFeature' && flag === 'delete') {
|
||||
delete this.editFeatureInfo[featureId];
|
||||
return;
|
||||
}
|
||||
|
||||
//좌표값 WKT포멧으로 변경
|
||||
const pGeom = GisApp.Module.core.convertFeatureGeomToWKT(feature);
|
||||
const param = {
|
||||
flag,
|
||||
...properties,
|
||||
geom: pGeom,
|
||||
};
|
||||
this.editFeatureInfo[featureId] = param;
|
||||
},
|
||||
getEditFeatureInfo: function () {
|
||||
const undoStack = this.undoredo._undoStack.array_;
|
||||
this.editFeatureInfo = {};
|
||||
for (let i in undoStack) {
|
||||
const undo = undoStack[i];
|
||||
if (undo.feature) {
|
||||
const feature = undo.feature;
|
||||
const properties = feature.getProperties();
|
||||
if (properties.uploadType === 'addFeature') {
|
||||
this.setEditFeatureInfo('insert', feature);
|
||||
} else if (properties.uploadType === 'removeFeature') {
|
||||
this.setEditFeatureInfo('delete', feature);
|
||||
} else if (undo.type === 'changegeometry') {
|
||||
this.setEditFeatureInfo('update', feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.editFeatureInfo;
|
||||
},
|
||||
};
|
||||
|
||||
export default GisOlEditLayer;
|
||||
@@ -0,0 +1,564 @@
|
||||
import Feature from 'ol/Feature.js';
|
||||
import GeoJSON from 'ol/format/GeoJSON.js';
|
||||
import Point from 'ol/geom/Point.js';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorImageLayer from 'ol/layer/VectorImage.js';
|
||||
import { all, bbox } from 'ol/loadingstrategy';
|
||||
import { transform } from 'ol/proj';
|
||||
import Cluster from 'ol/source/Cluster.js';
|
||||
import VectorSource from 'ol/source/Vector.js';
|
||||
import VectorTile from 'ol/source/VectorTile.js';
|
||||
import CircleStyle from 'ol/style/Circle.js';
|
||||
import Fill from 'ol/style/Fill.js';
|
||||
import Icon from 'ol/style/Icon.js';
|
||||
import Stroke from 'ol/style/Stroke.js';
|
||||
import Style from 'ol/style/Style.js';
|
||||
import Text from 'ol/style/Text.js';
|
||||
import { createXYZ } from 'ol/tilegrid';
|
||||
import GisApp from './gis.core.js';
|
||||
import GisOlStyle from './gis.style.js';
|
||||
import Tile from 'ol/layer/Tile.js';
|
||||
import TileWMS from 'ol/source/TileWMS.js';
|
||||
import gisCore from './gis.core.js';
|
||||
|
||||
/**
|
||||
* GisApp.layer
|
||||
* @requires gis.core.js
|
||||
* @requires gis.style.js
|
||||
*/
|
||||
const GisOlLayer = function (options) {
|
||||
this.initialize(options);
|
||||
};
|
||||
|
||||
GisOlLayer.prototype = {
|
||||
layers: [],
|
||||
cesiumLayers: [],
|
||||
defaults: {},
|
||||
storageNm: 'hasudo:',
|
||||
defaultLayerOptions: {
|
||||
name: '',
|
||||
zIndex: 2,
|
||||
opacity: 1,
|
||||
flterSql: null,
|
||||
minZoom: '15',
|
||||
},
|
||||
initialize: function (options) {
|
||||
Object.assign(this.defaults, options);
|
||||
this.gisStyle = new GisOlStyle();
|
||||
GisApp.Module.style = this.gisStyle;
|
||||
},
|
||||
gisStyle: null,
|
||||
/**
|
||||
* [사용]빈 레이어 생성
|
||||
*/
|
||||
makeLayer: function (options) {
|
||||
let layerOptions = {
|
||||
...this.defaultLayerOptions,
|
||||
...options,
|
||||
minZoom: options?.minViewSize ? options?.minViewSize : options?.minZoom,
|
||||
zIndex: options?.drawOrdr ? options?.drawOrdr : 1,
|
||||
};
|
||||
let returnLayer = new VectorImageLayer(layerOptions);
|
||||
this.layers.push(returnLayer);
|
||||
return returnLayer;
|
||||
},
|
||||
/**
|
||||
* gisStyle 반환
|
||||
*/
|
||||
getGisStyle: function () {
|
||||
return this.gisStyle;
|
||||
},
|
||||
/**
|
||||
* [사용]WFS 벡터 소스 반환
|
||||
*/
|
||||
getVectorSourceGeoserver: function (options) {
|
||||
let url = GisApp.GEOSERVER_URL;
|
||||
let toProj = GisApp.TO_PROJ;
|
||||
let lyrTyNm = options.lyrTyNm;
|
||||
let typename = this.storageNm + lyrTyNm;
|
||||
|
||||
if(options.lyrTyNm == "om_dt_untrt_swg_rain"){ //중점우수토실실
|
||||
url = GisApp.SN_GEOSERVER_URL;
|
||||
typename = lyrTyNm;
|
||||
}
|
||||
|
||||
const params = {};
|
||||
params['TYPENAME'] = typename;
|
||||
params['SERVICE'] = 'WFS';
|
||||
params['VERSION'] = '1.1.0';
|
||||
params['REQUEST'] = 'GetFeature';
|
||||
params['SRS'] = toProj;
|
||||
params['OUTPUTFORMAT'] = 'application/json';
|
||||
if (options?.flterSql && options?.flterSql?.trim() !== 'null') {
|
||||
params['CQL_FILTER'] = options?.flterSql;
|
||||
}
|
||||
const strParams = new URLSearchParams(params).toString();
|
||||
let source = new VectorSource({
|
||||
format: new GeoJSON(),
|
||||
url: function (extent) {
|
||||
return url + '?' + strParams;
|
||||
},
|
||||
strategy: all,
|
||||
});
|
||||
return source;
|
||||
},
|
||||
/**
|
||||
* [사용]WFS 벡터 소스 반환
|
||||
*/
|
||||
getVectorTileSource: function (options) {
|
||||
let url = GisApp.GEOSERVER_URL;
|
||||
let toProj = GisApp.TO_PROJ;
|
||||
let params = {};
|
||||
params['TYPENAME'] = this.storageNm + options.typeName;
|
||||
params['SERVICE'] = 'WFS';
|
||||
params['VERSION'] = '1.1.0';
|
||||
params['REQUEST'] = 'GetFeature';
|
||||
params['SRS'] = toProj;
|
||||
params['OUTPUTFORMAT'] = 'application/json';
|
||||
let strParams = new URLSearchParams(params).toString();
|
||||
|
||||
let source = new VectorTile({
|
||||
format: new GeoJSON(),
|
||||
|
||||
tileUrlFunction: function (tileCoord, pixelRatio, projection) {
|
||||
let filterStr = '&CQL_FILTER=bbox(GEOM,' + source.getTileGrid().getTileCoordExtent(tileCoord).join(',') + ",'" + projection.getCode() + "')";
|
||||
if (options.flterSql) {
|
||||
filterStr += ' AND ' + options.flterSql;
|
||||
}
|
||||
let paramWithUrl = url + '?' + strParams + filterStr;
|
||||
return paramWithUrl;
|
||||
},
|
||||
tileGrid: createXYZ,
|
||||
});
|
||||
|
||||
return source;
|
||||
},
|
||||
/**
|
||||
* [사용]WFS 레이어 생성
|
||||
*/
|
||||
getWfsLayer: function (option) {
|
||||
option = option || {};
|
||||
//레이어 생성
|
||||
let returnLayer = this.makeLayer(option);
|
||||
//레이어 소스 생성
|
||||
let source = this.getVectorSourceGeoserver(option);
|
||||
returnLayer.setSource(source);
|
||||
//스타일 추가
|
||||
returnLayer.setStyle(this.gisStyle.getLayerStyleFunction(returnLayer, option));
|
||||
returnLayer.set('id', option.id + '');
|
||||
return returnLayer;
|
||||
},
|
||||
/**
|
||||
* [사용]WMS 레이어 생성
|
||||
*/
|
||||
getWmsLayer: function (options) {
|
||||
let layerOptions = Object.assign({}, this.defaultLayerOptions, options);
|
||||
let url = GisApp.GEOSERVER_WMS_URL;
|
||||
let toProj = GisApp.TO_PROJ;
|
||||
const lyrTyNm = options.lyrTyNm;
|
||||
let params = {};
|
||||
params['LAYERS'] = this.storageNm + lyrTyNm;
|
||||
params['SERVICE'] = 'WMS';
|
||||
params['VERSION'] = '1.1.1';
|
||||
params['REQUEST'] = 'GetMap';
|
||||
params['SRS'] = toProj;
|
||||
params['FORMAT'] = 'image/png';
|
||||
params['TILED'] = true;
|
||||
const wmsLayer = new Tile({
|
||||
...layerOptions,
|
||||
id: layerOptions?.id,
|
||||
source: new TileWMS({
|
||||
url,
|
||||
params,
|
||||
serverType: 'geoserver',
|
||||
}),
|
||||
});
|
||||
wmsLayer.setZIndex(options.draw0rdr);
|
||||
wmsLayer.setMinZoom(options.minViewSize);
|
||||
this.layers.push(wmsLayer);
|
||||
return wmsLayer;
|
||||
},
|
||||
/**
|
||||
* [사용]WFS 레이어 생성
|
||||
*/
|
||||
getWfsEmptyLayer: function (option) {
|
||||
option = option || {};
|
||||
//레이어 생성
|
||||
let returnLayer = this.makeLayer(option);
|
||||
let vectorSource = new VectorSource({
|
||||
strategy: all,
|
||||
});
|
||||
returnLayer.setSource(vectorSource);
|
||||
//스타일 추가
|
||||
returnLayer.setStyle(this.gisStyle.getLayerStyle(returnLayer, option));
|
||||
returnLayer.set('id', option.id + '');
|
||||
let map = GisApp.Module.core.map;
|
||||
map.addLayer(returnLayer);
|
||||
return returnLayer;
|
||||
},
|
||||
/**
|
||||
* 피처 리스트로 레이어 생성
|
||||
*/
|
||||
getLayerByFeature: function (feature, layerOptions, layerStyle) {
|
||||
if (!layerOptions || !layerOptions.id) {
|
||||
return null;
|
||||
}
|
||||
let vectorSource = new VectorSource({
|
||||
strategy: all,
|
||||
});
|
||||
if (feature) {
|
||||
if (Object.prototype.toString.call(feature).slice(8, -1).indexOf('Object') > -1) {
|
||||
vectorSource.addFeature(feature);
|
||||
} else {
|
||||
vectorSource.addFeatures(feature);
|
||||
}
|
||||
}
|
||||
GisApp.Module.core.deleteLayer(layerOptions.id);
|
||||
layerOptions.source = vectorSource;
|
||||
let layer = new VectorImageLayer(layerOptions);
|
||||
layer.setStyle(layerStyle);
|
||||
layer.set('id', layerOptions.id + '');
|
||||
return layer;
|
||||
},
|
||||
/**
|
||||
* 피처 하이라이트
|
||||
*/
|
||||
highLightFeature: function (hlFeature, layerOptions, styleOptions, customStyleFunction) {
|
||||
const color = styleOptions.color || 'rgba(135, 116, 243, 0.4)';
|
||||
const width = styleOptions.width || 10;
|
||||
const featureKey = styleOptions.featureKey || '';
|
||||
const fitExtent = layerOptions.fitExtent || 'true';
|
||||
|
||||
const styleFunction = function (feature, resolution) {
|
||||
const featureStyle = feature.getStyle();
|
||||
if (featureStyle) return null;
|
||||
const style = new Style({
|
||||
image: new CircleStyle({
|
||||
fill: new Fill({
|
||||
color: color,
|
||||
}),
|
||||
radius: width,
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: color,
|
||||
width: width,
|
||||
}),
|
||||
});
|
||||
|
||||
const getFeatureText = function (feature) {
|
||||
if (featureKey) {
|
||||
let text = feature.get(featureKey);
|
||||
if (text) return text;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const text = new Text({
|
||||
stroke: new Stroke({
|
||||
color: 'white',
|
||||
width: 2,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: 'black',
|
||||
}),
|
||||
font: '12px Verdana',
|
||||
text: getFeatureText(feature),
|
||||
});
|
||||
style.setText(text);
|
||||
return style;
|
||||
};
|
||||
let actualStyleFunction = customStyleFunction || styleFunction;
|
||||
GisApp.Module.core.deleteLayer(layerOptions.id);
|
||||
let layer = this.getLayerByFeature(hlFeature, layerOptions, actualStyleFunction);
|
||||
if (hlFeature && hlFeature.length > 0) {
|
||||
let map = GisApp.Module.core.map;
|
||||
map.addLayer(layer);
|
||||
if (fitExtent === 'true') {
|
||||
GisApp.Module.core.fitExtent(layer.getSource().getExtent());
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
},
|
||||
/**
|
||||
* 마커 레이어 맵에 추가
|
||||
*/
|
||||
addMarkerLayer: function (lat, lon, fromProj, layerOptions) {
|
||||
let p = transform([Number(lon), Number(lat)], fromProj, GisApp.TO_PROJ);
|
||||
let feature = new Feature({
|
||||
geometry: new Point(p),
|
||||
});
|
||||
let layer = this.getMarkerLayer(layerOptions);
|
||||
layer.getSource().addFeature(feature);
|
||||
let map = GisApp.Module.core.map;
|
||||
map.addLayer(layer);
|
||||
GisApp.Module.core.fitExtent(feature.getGeometry().getExtent());
|
||||
return layer;
|
||||
},
|
||||
/**
|
||||
* 마커 레이어 생성
|
||||
*/
|
||||
getMarkerLayer: function (layerOptions) {
|
||||
let imgSrc = GisApp.CONTEXT_PATH + 'images/marker/marker-icon.png';
|
||||
let style = new Style({
|
||||
image: new Icon({
|
||||
anchor: [0.5, 46],
|
||||
anchorXUnits: 'fraction',
|
||||
anchorYUnits: 'pixels',
|
||||
src: imgSrc,
|
||||
}),
|
||||
});
|
||||
let layer = this.getLayerByFeature(null, layerOptions, style);
|
||||
return layer;
|
||||
},
|
||||
/**
|
||||
* 피처 생성
|
||||
*/
|
||||
getFeatureCoord: function (coord) {
|
||||
let feature = new Feature({
|
||||
geometry: new Point(coord),
|
||||
});
|
||||
return feature;
|
||||
},
|
||||
/**
|
||||
* 피처 생성(lat,lon)
|
||||
*/
|
||||
getFeatureLonLat: function (lat, lon) {
|
||||
let p = transform([Number(lon), Number(lat)], GisApp.FROM_PROJ, GisApp.TO_PROJ);
|
||||
let feature = new Feature({
|
||||
geometry: new Point(p),
|
||||
});
|
||||
return feature;
|
||||
},
|
||||
addPolygonLayer: function (features, option) {
|
||||
const map = GisApp.Module.core.map;
|
||||
option = Object.assign(
|
||||
{
|
||||
gobjTy: 'lineLabelPoligon',
|
||||
minZoom: option?.minZoom || '10',
|
||||
lblAtrbNm: option?.lblAtrbNm || 'fclty_nm',
|
||||
lnWdth: option?.lnWdth || '2',
|
||||
lnColor: option?.lnColor || 'rgba(0, 0, 0, 1)',
|
||||
polgnColor: option?.polgnColor || 'rgba(255, 0, 0, 0.2)',
|
||||
clickPsblYn: option?.clickPsblYn || 'N',
|
||||
fondNm: '18px "맑은 고딕",Malgun Gothic',
|
||||
},
|
||||
option
|
||||
);
|
||||
features[0].set('overLayId', option?.overlayId || null);
|
||||
features[0].set('html', option?.html || null);
|
||||
//레이어 생성
|
||||
let vectorLayer = this.makeLayer(option);
|
||||
//레이어 소스 생성
|
||||
const vectorSource = new VectorSource({});
|
||||
vectorSource.addFeatures(features);
|
||||
vectorLayer.setSource(vectorSource);
|
||||
//스타일 추가
|
||||
vectorLayer.setStyle(this.gisStyle.getLayerStyleFunction(vectorLayer, option));
|
||||
vectorLayer.set('id', option?.id + '');
|
||||
GisApp.Module.core.deleteLayer(option?.id);
|
||||
map.addLayer(vectorLayer);
|
||||
},
|
||||
addLayerByLyrTyNm: function (id, lyrTyNm, addQurryFlter) {
|
||||
const map = GisApp.Module.core.map;
|
||||
const layerCode = GisApp.layerCode;
|
||||
const matched = Object.values(layerCode).find(obj => obj.lyrTyNm === lyrTyNm);
|
||||
matched['id'] = id;
|
||||
let layer = GisApp.Module.core.getLayerById(matched?.id);
|
||||
if (layer) {
|
||||
layer.setVisible(true);
|
||||
} else {
|
||||
const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || '');
|
||||
matched['flterSql'] = queryFlter;
|
||||
layer = this.getWfsLayer(matched);
|
||||
GisApp.Module.core.deleteLayer(id);
|
||||
map.addLayer(layer);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 클러스터 레이어 맵에 추가
|
||||
*/
|
||||
addClusterLayer: function (features, { layerId, color }) {
|
||||
const map = GisApp.Module.core.map;
|
||||
const vectorSource = new VectorSource({});
|
||||
vectorSource.addFeatures(features);
|
||||
const clusterLayerId = layerId + 'Cluster';
|
||||
const vectorLayerId = layerId + 'Vector';
|
||||
// 클러스터 레이어 생성
|
||||
const clusterLayer = new VectorLayer({
|
||||
id: clusterLayerId,
|
||||
source: new Cluster({
|
||||
distance: 100, // 클러스터링 거리
|
||||
source: vectorSource,
|
||||
geometryFunction: function (feature) {
|
||||
let geometry = feature.getGeometry();
|
||||
let type = geometry.getType();
|
||||
let coordinates;
|
||||
if (type === 'Point') {
|
||||
coordinates = geometry.getCoordinates();
|
||||
} else if (type === 'LineString' || type === 'MultiLineString') {
|
||||
let totalX = 0;
|
||||
let totalY = 0;
|
||||
let totalPoints = 0;
|
||||
geometry.getCoordinates().forEach(line => {
|
||||
line.forEach(point => {
|
||||
totalX += point[0];
|
||||
totalY += point[1];
|
||||
totalPoints++;
|
||||
});
|
||||
});
|
||||
coordinates = [totalX / totalPoints, totalY / totalPoints];
|
||||
}
|
||||
return new Point(coordinates);
|
||||
},
|
||||
}),
|
||||
style: feature => {
|
||||
const clusterSize = feature.get('features').length;
|
||||
return new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 10,
|
||||
fill: new Fill({ color }),
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color,
|
||||
width: 10,
|
||||
}),
|
||||
text: new Text({
|
||||
text: clusterSize.toString(),
|
||||
fill: new Fill({
|
||||
color: 'white',
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: 'black',
|
||||
width: 4,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const vectorLayer = new VectorLayer({
|
||||
id: vectorLayerId,
|
||||
zIndex: 1,
|
||||
source: vectorSource,
|
||||
style: new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 10,
|
||||
fill: new Fill({ color }),
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color,
|
||||
width: 10,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
GisApp.Module.core.deleteLayer(clusterLayerId);
|
||||
GisApp.Module.core.deleteLayer(vectorLayerId);
|
||||
map.addLayer(clusterLayer);
|
||||
map.addLayer(vectorLayer);
|
||||
function handleZoomEnd() {
|
||||
const findClusterLayer = map
|
||||
?.getLayers()
|
||||
?.getArray()
|
||||
.find(function (layer) {
|
||||
return layer.get('id') === clusterLayerId;
|
||||
});
|
||||
const findVectorLayer = map
|
||||
?.getLayers()
|
||||
?.getArray()
|
||||
.find(function (layer) {
|
||||
return layer.get('id') === vectorLayerId;
|
||||
});
|
||||
if (!findClusterLayer) {
|
||||
GisApp.Module.core.deleteLayer(vectorLayerId);
|
||||
}
|
||||
if (!findVectorLayer) {
|
||||
GisApp.Module.core.deleteLayer(vectorLayerId);
|
||||
}
|
||||
const zoomLevel = map.getView().getZoom();
|
||||
if (zoomLevel >= 18) {
|
||||
vectorLayer.setVisible(true);
|
||||
clusterLayer.setVisible(false);
|
||||
} else {
|
||||
clusterLayer.setVisible(true);
|
||||
vectorLayer.setVisible(false);
|
||||
}
|
||||
if (!findClusterLayer && !findVectorLayer) {
|
||||
map.getView().un('change:resolution', handleZoomEnd);
|
||||
}
|
||||
}
|
||||
// 줌 이벤트 핸들러 등록
|
||||
map.getView().un('change:resolution', handleZoomEnd);
|
||||
map.getView().on('change:resolution', handleZoomEnd);
|
||||
handleZoomEnd();
|
||||
},
|
||||
setLayersToCesium: function (ol3dInstance) {
|
||||
let me = this;
|
||||
//const { Cesium } = window;
|
||||
const cesiumScene = ol3dInstance.getCesiumScene(); // Cesium Scene 가져오기
|
||||
me.cesiumLayers.forEach(cesiumLayer => {
|
||||
cesiumScene.imageryLayers.remove(cesiumLayer);
|
||||
});
|
||||
const layers = GisApp.Module.core.map
|
||||
.getAllLayers()
|
||||
.filter(layer => layer.get('type') !== 'base')
|
||||
.sort((a, b) => {
|
||||
const orderA = a.getProperties().drawOrdr || 0;
|
||||
const orderB = b.getProperties().drawOrdr || 0;
|
||||
return orderA - orderB; // 낮은 값이 먼저 오도록 정렬
|
||||
});
|
||||
|
||||
layers.forEach(layer => {
|
||||
const properties = layer.getProperties();
|
||||
let cesiumLayer = null;
|
||||
if (properties?.lyrTyNm) {
|
||||
const visible = layer.getVisible();
|
||||
const lyrTyNm = properties.lyrTyNm;
|
||||
// OpenLayers에서 레이어의 가시성 확인
|
||||
const { Cesium } = window;
|
||||
const wmsUrl = GisApp.GEOSERVER_WMS_URL;
|
||||
const wmsProvider = new Cesium.WebMapServiceImageryProvider({
|
||||
url: wmsUrl,
|
||||
layers: GisApp.Module.layer.storageNm + lyrTyNm, // WMS 레이어 이름
|
||||
parameters: {
|
||||
service: 'WMS',
|
||||
version: '1.1.1',
|
||||
request: 'GetMap',
|
||||
format: 'image/png',
|
||||
transparent: true,
|
||||
styles: lyrTyNm,
|
||||
},
|
||||
});
|
||||
cesiumLayer = cesiumScene.imageryLayers.addImageryProvider(wmsProvider);
|
||||
cesiumLayer['properties'] = properties;
|
||||
me.cesiumLayers.push(cesiumLayer);
|
||||
if (properties?.visible) {
|
||||
const zoomLevel = getOpenLayersZoom(cesiumScene);
|
||||
cesiumLayer.show = zoomLevel >= Number(properties?.minZoom);
|
||||
} else {
|
||||
cesiumLayer.show = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (me.cesiumLayers.length > 0) {
|
||||
cesiumScene._moveEndListener = () => {
|
||||
const zoomLevel = getOpenLayersZoom(cesiumScene); // 대략적인 줌 계산
|
||||
me.cesiumLayers.forEach(cesiumLayer => {
|
||||
const properties = cesiumLayer.properties;
|
||||
if (properties?.visible) {
|
||||
console.log('zoomLevel >= minZoom==', properties?.lyrTyNm, zoomLevel >= properties?.minZoom);
|
||||
cesiumLayer.show = zoomLevel >= Number(properties?.minZoom);
|
||||
}
|
||||
});
|
||||
};
|
||||
cesiumScene.camera.moveEnd.addEventListener(cesiumScene._moveEndListener);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function getOpenLayersZoom(cesiumScene) {
|
||||
const cameraHeight = cesiumScene.camera.positionCartographic.height;
|
||||
const maxResolution = 156543.03392804097; // OpenLayers 기본 해상도 (Google Mercator 기준)
|
||||
const tileSize = 256; // 기본 타일 크기
|
||||
return Math.ceil(Math.log2(maxResolution / (cameraHeight / tileSize)));
|
||||
}
|
||||
|
||||
export default GisOlLayer;
|
||||
@@ -0,0 +1,204 @@
|
||||
import Style from 'ol/style/Style';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import Fill from 'ol/style/Fill.js';
|
||||
import Stroke from 'ol/style/Stroke';
|
||||
import CircleStyle from 'ol/style/Circle';
|
||||
import { getLength, getArea } from 'ol/sphere';
|
||||
import Draw from 'ol/interaction/Draw';
|
||||
import Polygon from 'ol/geom/Polygon';
|
||||
import LineString from 'ol/geom/LineString';
|
||||
import { unByKey } from 'ol/Observable';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import GisApp from './gis.core';
|
||||
//import DOMPurify from 'dompurify';
|
||||
const GisOlMeasure = function (options) {
|
||||
GisApp.measure = {};
|
||||
GisApp.measure.helpTooltipElement = null;
|
||||
GisApp.measure.helpTooltip = null;
|
||||
GisApp.measure.sketch = null;
|
||||
GisApp.measure.measureTooltipElement = null;
|
||||
GisApp.measure.measureTooltip = null;
|
||||
GisApp.measure.continueLineMsg = '선 그리기 종료를 위해 마우스를 더블클릭하세요.';
|
||||
GisApp.measure.Polygon = 'Polygon';
|
||||
GisApp.measure.LineString = 'LineString';
|
||||
|
||||
this.initialize(options);
|
||||
};
|
||||
|
||||
GisOlMeasure.prototype = {
|
||||
map: null,
|
||||
draw: null,
|
||||
vector: null,
|
||||
type: null,
|
||||
initialize: function (map) {
|
||||
let me = this;
|
||||
let source = new VectorSource();
|
||||
let vector = new VectorLayer({
|
||||
source: source,
|
||||
style: new Style({
|
||||
fill: new Fill({
|
||||
color: 'rgba(255, 255, 255, 0.2)',
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: '#ffcc33',
|
||||
width: 2,
|
||||
}),
|
||||
image: new CircleStyle({
|
||||
radius: 7,
|
||||
fill: new Fill({
|
||||
color: '#ffcc33',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
me.map = map;
|
||||
me.vector = vector;
|
||||
me.map.addLayer(vector);
|
||||
me.map.removeInteraction(this.draw);
|
||||
},
|
||||
pointerMoveHandler: function (evt) {
|
||||
if (evt.dragging) {
|
||||
return;
|
||||
}
|
||||
let helpMsg = '시작점을 선택하세요';
|
||||
if (GisApp.measure.sketch) {
|
||||
helpMsg = GisApp.measure.continueLineMsg;
|
||||
}
|
||||
if (GisApp.measure.helpTooltipElement) {
|
||||
//GisApp.measure.helpTooltipElement.innerHTML = DOMPurify.sanitize(helpMsg);
|
||||
GisApp.measure.helpTooltipElement.innerHTML = helpMsg;
|
||||
GisApp.measure.helpTooltip.setPosition(evt.coordinate);
|
||||
GisApp.measure.helpTooltipElement.classList.remove('hidden');
|
||||
}
|
||||
},
|
||||
formatLength: function (line) {
|
||||
let length = getLength(line);
|
||||
let output;
|
||||
if (length > 1000) {
|
||||
output = Math.round((length / 1000) * 100) / 100 + ' ' + 'km';
|
||||
} else {
|
||||
output = Math.round(length * 100) / 100 + ' ' + 'm';
|
||||
}
|
||||
return output;
|
||||
},
|
||||
formatArea: function (polygon) {
|
||||
let area = getArea(polygon);
|
||||
let output;
|
||||
if (area > 10000) {
|
||||
output = Math.round((area / 1000000) * 100) / 100 + ' ' + 'km<sup>2</sup>';
|
||||
} else {
|
||||
output = Math.round(area * 100) / 100 + ' ' + 'm<sup>2</sup>';
|
||||
}
|
||||
return output;
|
||||
},
|
||||
addInteraction: function (type) {
|
||||
let me = this;
|
||||
me.map.removeInteraction(me.draw);
|
||||
me.map.on('pointermove', me.pointerMoveHandler);
|
||||
me.type = type;
|
||||
|
||||
let source = me.vector.getSource();
|
||||
me.draw = new Draw({
|
||||
source: source,
|
||||
type: type,
|
||||
style: new Style({
|
||||
fill: new Fill({
|
||||
color: 'rgba(255, 255, 255, 0.2)',
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: 'rgba(0, 0, 0, 0.5)',
|
||||
lineDash: [10, 10],
|
||||
width: 2,
|
||||
}),
|
||||
image: new CircleStyle({
|
||||
radius: 5,
|
||||
stroke: new Stroke({
|
||||
color: 'rgba(0, 0, 0, 0.7)',
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: 'rgba(255, 255, 255, 0.2)',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
me.map.addInteraction(me.draw);
|
||||
me.createMeasureTooltip();
|
||||
me.createHelpTooltip();
|
||||
|
||||
let listener;
|
||||
me.draw.on('drawstart', function (evt) {
|
||||
let tooltipCoord = evt.coordinate;
|
||||
GisApp.measure.sketch = evt.feature;
|
||||
listener = GisApp.measure.sketch.getGeometry().on('change', function (evt) {
|
||||
let geom = evt.target;
|
||||
let output;
|
||||
if (geom instanceof Polygon) {
|
||||
output = me.formatArea(geom);
|
||||
tooltipCoord = geom.getInteriorPoint().getCoordinates();
|
||||
} else if (geom instanceof LineString) {
|
||||
output = me.formatLength(geom);
|
||||
tooltipCoord = geom.getLastCoordinate();
|
||||
}
|
||||
//GisApp.measure.measureTooltipElement.innerHTML = DOMPurify.sanitize(output);
|
||||
GisApp.measure.measureTooltipElement.innerHTML = output;
|
||||
GisApp.measure.measureTooltip.setPosition(tooltipCoord);
|
||||
});
|
||||
});
|
||||
|
||||
me.draw.on('drawend', function () {
|
||||
GisApp.measure.measureTooltipElement.className = 'ol-tooltip ol-tooltip-static';
|
||||
GisApp.measure.measureTooltip.setOffset([0, -7]);
|
||||
GisApp.measure.sketch = null;
|
||||
// unset tooltip so that a new one can be created
|
||||
GisApp.measure.measureTooltipElement = null;
|
||||
me.createMeasureTooltip();
|
||||
unByKey(listener);
|
||||
me.removeInteraction();
|
||||
});
|
||||
},
|
||||
removeInteraction: function () {
|
||||
this.map.removeInteraction(this.draw);
|
||||
this.map.removeOverlay(GisApp.measure.helpTooltip);
|
||||
},
|
||||
removeFeature: function () {
|
||||
let source = this.vector.getSource();
|
||||
let features = this.vector.getSource().getFeatures();
|
||||
for (let i in features) {
|
||||
let feature = features[i];
|
||||
source.removeFeature(feature);
|
||||
}
|
||||
},
|
||||
createHelpTooltip: function () {
|
||||
let me = this;
|
||||
if (GisApp.measure.helpTooltipElement) {
|
||||
GisApp.measure.helpTooltipElement.parentNode.removeChild(GisApp.measure.helpTooltipElement);
|
||||
}
|
||||
GisApp.measure.helpTooltipElement = document.createElement('div');
|
||||
GisApp.measure.helpTooltipElement.className = 'ol-tooltip hidden';
|
||||
GisApp.measure.helpTooltip = new Overlay({
|
||||
element: GisApp.measure.helpTooltipElement,
|
||||
offset: [15, 0],
|
||||
positioning: 'center-left',
|
||||
});
|
||||
me.map.addOverlay(GisApp.measure.helpTooltip);
|
||||
},
|
||||
createMeasureTooltip: function () {
|
||||
let me = this;
|
||||
if (GisApp.measure.measureTooltipElement) {
|
||||
GisApp.measure.measureTooltipElement.parentNode.removeChild(GisApp.measure.measureTooltipElement);
|
||||
}
|
||||
GisApp.measure.measureTooltipElement = document.createElement('div');
|
||||
GisApp.measure.measureTooltipElement.className = 'ol-tooltip ol-tooltip-measure';
|
||||
GisApp.measure.measureTooltip = new Overlay({
|
||||
id: 'measure',
|
||||
element: GisApp.measure.measureTooltipElement,
|
||||
offset: [0, -15],
|
||||
positioning: 'bottom-center',
|
||||
});
|
||||
me.map.addOverlay(GisApp.measure.measureTooltip);
|
||||
},
|
||||
};
|
||||
|
||||
export default GisOlMeasure;
|
||||
@@ -0,0 +1,434 @@
|
||||
import GisApp from './gis.core';
|
||||
import Style from 'ol/style/Style.js';
|
||||
import { click, mouseOnly, never } from 'ol/events/condition';
|
||||
import DragBox from 'ol/interaction/DragBox.js';
|
||||
import Draw from 'ol/interaction/Draw.js';
|
||||
import Select from 'ol/interaction/Select.js';
|
||||
import VectorLayer from 'ol/layer/Vector.js';
|
||||
import Overlay from 'ol/Overlay.js';
|
||||
import VectorSource from 'ol/source/Vector.js';
|
||||
import CircleStyle from 'ol/style/Circle.js';
|
||||
import Fill from 'ol/style/Fill.js';
|
||||
import Stroke from 'ol/style/Stroke.js';
|
||||
|
||||
const GisOlSelect = function (map, options) {
|
||||
GisApp.select = {};
|
||||
GisApp.select.helpTooltipElement = null;
|
||||
GisApp.select.helpTooltip = null;
|
||||
GisApp.select.helpMsg = null;
|
||||
this.initialize(map, options);
|
||||
};
|
||||
|
||||
GisOlSelect.prototype = {
|
||||
map: null,
|
||||
defaults: {
|
||||
hitTolerance: 10,
|
||||
useAreaControl: false,
|
||||
},
|
||||
selectInteraction: null,
|
||||
dragBox: null,
|
||||
dragPolygon: null,
|
||||
selectedFeatures: [],
|
||||
selectFeatureFlag: false,
|
||||
initialize: function (map, options) {
|
||||
this.map = map;
|
||||
_.extend(this.defaults, options);
|
||||
this.setInteraction();
|
||||
this.pointerMoveEventHandler();
|
||||
},
|
||||
/**
|
||||
* SELECT 이벤트 등록
|
||||
*/
|
||||
setInteraction: function () {
|
||||
let me = this;
|
||||
let map = this.map;
|
||||
if (this.selectInteraction) {
|
||||
this.removeInteration();
|
||||
}
|
||||
let baseStyle = new Style({
|
||||
image : new CircleStyle({
|
||||
fill : new Fill({
|
||||
color : 'rgba(135, 116, 243, 0.4)',
|
||||
}),
|
||||
radius : 30
|
||||
}),
|
||||
stroke : new Stroke({
|
||||
color : 'rgba(135, 116, 243, 0.4)',
|
||||
width : 10,
|
||||
})
|
||||
});
|
||||
|
||||
this.selectInteraction = new Select({
|
||||
style: function (feature, resolution) {
|
||||
let styles = [];
|
||||
let layer = feature.getLayer();
|
||||
let styleFn = GisApp.Module.style.getLayerStyleFunction(layer, null);
|
||||
|
||||
if (styleFn) {
|
||||
const style = styleFn(feature, resolution);
|
||||
if (Array.isArray(style)) {
|
||||
styles.push(...style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
styles.push(baseStyle);
|
||||
return styles;
|
||||
},
|
||||
layers: function (layer) {
|
||||
let clickPsblYn = layer?.get('clickPsblYn');
|
||||
return clickPsblYn === 'Y';
|
||||
},
|
||||
condition: click,
|
||||
hitTolerance: me.defaults.hitTolerance,
|
||||
});
|
||||
|
||||
map.addInteraction(this.selectInteraction);
|
||||
this.selectedFeatures = this.selectInteraction.getFeatures();
|
||||
|
||||
// 선택된 피처 중심으로 지도 이동
|
||||
this.selectInteraction.on('select', function (e) {
|
||||
const feature = e.selected[0];
|
||||
if (feature) {
|
||||
const geometry = feature.getGeometry();
|
||||
if (geometry) {
|
||||
const extent = geometry.getExtent();
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 16
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* SELECT 피처 초기화
|
||||
*/
|
||||
reset: function () {
|
||||
let me = this;
|
||||
if (GisApp.Module.edit) {
|
||||
GisApp.Module.edit.setDrawActive(false);
|
||||
}
|
||||
if (GisApp.Module.draw) {
|
||||
GisApp.Module.draw.setDrawActive(false);
|
||||
}
|
||||
me.setActive(true);
|
||||
me.removeHelpTooltip();
|
||||
|
||||
let map = this.map;
|
||||
map.removeInteraction(me.dragBox);
|
||||
map.removeInteraction(me.dragPolygon);
|
||||
|
||||
this.selectInteraction.addCondition_ = never;
|
||||
this.selectFeatureFlag = false;
|
||||
|
||||
if (GisApp.Module.measure) {
|
||||
GisApp.Module.measure.removeFeature();
|
||||
GisApp.Module.core.deleteOverlay('measure');
|
||||
}
|
||||
|
||||
GisApp.Module.core.deleteLayer(GisApp.hLayerId);
|
||||
GisApp.Module.core.deleteLayer(GisApp.hlLayerId);
|
||||
|
||||
this.resetSelectedFeature();
|
||||
},
|
||||
/**
|
||||
* 단일 SELECT 이벤트 등록
|
||||
*/
|
||||
setSingleInteration: function () {
|
||||
this.reset();
|
||||
this.selectInteraction.addCondition_ = click;
|
||||
this.selectFeatureFlag = true;
|
||||
},
|
||||
/**
|
||||
* 박스 SELECT 이벤트 등록
|
||||
*/
|
||||
setBoxInteration: function () {
|
||||
//초기화
|
||||
this.reset();
|
||||
this.selectFeatureFlag = true;
|
||||
|
||||
let me = this;
|
||||
let map = this.map;
|
||||
me.dragBox = new DragBox({
|
||||
condition: mouseOnly,
|
||||
});
|
||||
map.addInteraction(this.dragBox);
|
||||
GisApp.select.helpMsg = '직사각형 시작점을 클릭하고 드래그하세요.';
|
||||
me.createHelpTooltip();
|
||||
me.dragBox.on('boxend', function () {
|
||||
let rotation = map.getView().getRotation();
|
||||
let oblique = rotation % (Math.PI / 2) !== 0;
|
||||
let candidateFeatures = oblique ? [] : me.selectedFeatures;
|
||||
let extent = me.dragBox.getGeometry().getExtent();
|
||||
let layers = map.getLayers();
|
||||
for (let i in layers.array_) {
|
||||
let layer = layers.array_[i];
|
||||
if (layer?.get('clickPsblYn') == 'Y' && me.getVisible(layer)) {
|
||||
layer.getSource().forEachFeatureIntersectingExtent(extent, function (feature) {
|
||||
candidateFeatures.push(feature);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (oblique) {
|
||||
let anchor = [0, 0];
|
||||
let geometry = me.dragBox.getGeometry().clone();
|
||||
geometry.rotate(-rotation, anchor);
|
||||
let extent$1 = geometry.getExtent();
|
||||
candidateFeatures.forEach(function (feature) {
|
||||
let geometry = feature.getGeometry().clone();
|
||||
geometry.rotate(-rotation, anchor);
|
||||
if (geometry.intersectsExtent(extent$1)) {
|
||||
me.selectedFeatures.push(feature);
|
||||
}
|
||||
});
|
||||
}
|
||||
me.selectInteraction.dispatchEvent({
|
||||
type: 'select',
|
||||
selected: me.selectedFeatures,
|
||||
deselected: [],
|
||||
});
|
||||
map.removeInteraction(me.dragBox);
|
||||
});
|
||||
|
||||
me.dragBox.on('boxstart', function () {
|
||||
me.removeHelpTooltip();
|
||||
me.selectedFeatures.clear();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 폴리곤 SELECT 이벤트 등록
|
||||
*/
|
||||
setPolygonInteration: function () {
|
||||
this.reset();
|
||||
this.selectFeatureFlag = true;
|
||||
|
||||
let me = this;
|
||||
let map = this.map;
|
||||
|
||||
//폴리곤 레이어 생성
|
||||
let polygonSource = new VectorSource({
|
||||
useSpatialIndex: false,
|
||||
});
|
||||
let polygonLayer = new VectorLayer({
|
||||
source: polygonSource,
|
||||
});
|
||||
map.addLayer(polygonLayer);
|
||||
|
||||
//폴리곤 draw 객체 생성
|
||||
me.dragPolygon = new Draw({
|
||||
source: polygonSource,
|
||||
type: 'Polygon',
|
||||
condition: mouseOnly,
|
||||
});
|
||||
map.addInteraction(me.dragPolygon);
|
||||
|
||||
//툴팁 추가
|
||||
GisApp.select.helpMsg = '시작점을 클릭하세요.';
|
||||
me.createHelpTooltip();
|
||||
|
||||
me.dragPolygon.on('drawstart', function () {
|
||||
me.removeHelpTooltip();
|
||||
me.selectedFeatures.clear();
|
||||
});
|
||||
|
||||
me.dragPolygon.on('drawend', function (evt) {
|
||||
setTimeout(function () {
|
||||
let polygon = evt.feature.getGeometry();
|
||||
let layers = map.getLayers();
|
||||
for (let i in layers.array_) {
|
||||
let layer = layers.array_[i];
|
||||
if (layer?.get('clickPsblYn') == 'Y' && me.getVisible(layer)) {
|
||||
let features = layer.getSource().getFeatures();
|
||||
for (let j = 0; j < features.length; j++) {
|
||||
if (polygon.intersectsExtent(features[j].getGeometry().getExtent())) {
|
||||
me.selectedFeatures.push(features[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
me.selectInteraction.dispatchEvent({
|
||||
type: 'select',
|
||||
selected: me.selectedFeatures,
|
||||
deselected: [],
|
||||
});
|
||||
map.removeInteraction(me.dragPolygon);
|
||||
map.removeLayer(polygonLayer);
|
||||
}, 300);
|
||||
});
|
||||
return me.dragPolygon;
|
||||
},
|
||||
/**
|
||||
* 레이어 추가
|
||||
*/
|
||||
addLayer: function (layer) {
|
||||
if (layer) {
|
||||
layer.set('clickPsblYn', 'Y');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 레이어 삭제
|
||||
*/
|
||||
removeLayer: function (layer) {
|
||||
if (layer) {
|
||||
layer.set('clickPsblYn', 'N');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 선택된 객체 초기화
|
||||
*/
|
||||
resetSelectedFeature: function () {
|
||||
const selectedFeatures = this.selectedFeatures;
|
||||
selectedFeatures.forEach(feature => {
|
||||
selectedFeatures.remove(feature);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* SELECT 이벤트 활성화
|
||||
*/
|
||||
setActive: function (active) {
|
||||
this.selectInteraction.setActive(active);
|
||||
},
|
||||
/**
|
||||
* SELECT 이벤트 삭제
|
||||
*/
|
||||
removeInteration: function () {
|
||||
let map = this.map;
|
||||
map.removeInteraction(this.selectInteraction);
|
||||
this.selectInteraction = null;
|
||||
|
||||
//포인터 변경 삭제
|
||||
this.removePointerMoveEventHandler('pointerMoveForSelect');
|
||||
},
|
||||
/**
|
||||
* 피처 위에 마우스 오버시 포인터 변경
|
||||
*/
|
||||
pointerMoveEventHandler: function () {
|
||||
let me = this;
|
||||
let map = this.map;
|
||||
let pointerMoveForSelect = function (evt) {
|
||||
let hit = this.forEachFeatureAtPixel(
|
||||
evt.pixel,
|
||||
function (feature, layer) {
|
||||
let returnFlag = true;
|
||||
if (layer?.get('clickPsblYn') === 'N') {
|
||||
returnFlag = false;
|
||||
}
|
||||
return returnFlag;
|
||||
},
|
||||
{ hitTolerance: me.defaults.hitTolerance }
|
||||
);
|
||||
|
||||
if (hit && me.selectInteraction.getActive()) {
|
||||
this.getTargetElement().style.cursor = 'pointer';
|
||||
} else {
|
||||
this.getTargetElement().style.cursor = '';
|
||||
}
|
||||
};
|
||||
map.on('pointermove', pointerMoveForSelect);
|
||||
},
|
||||
/**
|
||||
* 포인터 이벤트 삭제
|
||||
*/
|
||||
removePointerMoveEventHandler: function (fnNm) {
|
||||
GisApp.Module.core.removeMapEventFnNm('pointermove', fnNm);
|
||||
},
|
||||
/**
|
||||
* 선택된 피처 정보 반환(callback)
|
||||
*/
|
||||
getSelectedInfo: function (callback) {
|
||||
let me = this;
|
||||
this.selectInteraction.on('select', function (e) {
|
||||
if (!e.auto) {
|
||||
let features = me.selectInteraction.getFeatures();
|
||||
if(features.getArray().length == 0) return;
|
||||
let layers = [];
|
||||
for (let i in features.array_) {
|
||||
let feature = features.array_[0];
|
||||
let layer = me.selectInteraction.getLayer(feature);
|
||||
layers.push(layer);
|
||||
}
|
||||
callback(features.array_, layers);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 선택된 피처 정보 반환
|
||||
*/
|
||||
getSelectedFeatures: function () {
|
||||
return this.selectInteraction.getFeatures();
|
||||
},
|
||||
/**
|
||||
* 마지막으로 선택된 피처 정보 반환
|
||||
*/
|
||||
getLastSelectedFeatures: function () {
|
||||
return this.selectInteraction.getFeatures();
|
||||
},
|
||||
/**
|
||||
* 툴팁 생성
|
||||
*/
|
||||
createHelpTooltip: function () {
|
||||
let me = this;
|
||||
let map = this.map;
|
||||
GisApp.select.helpTooltipElement = document.createElement('div');
|
||||
GisApp.select.helpTooltipElement.className = 'ol-tooltip hidden';
|
||||
GisApp.select.helpTooltip = new Overlay({
|
||||
element: GisApp.select.helpTooltipElement,
|
||||
offset: [15, 0],
|
||||
positioning: 'center-left',
|
||||
});
|
||||
map.addOverlay(GisApp.select.helpTooltip);
|
||||
map.on('pointermove', me.pointerMoveTooltipHandler);
|
||||
},
|
||||
/**
|
||||
* 툴팁 삭제
|
||||
*/
|
||||
removeHelpTooltip: function () {
|
||||
if (GisApp.select.helpTooltipElement) {
|
||||
let map = this.map;
|
||||
this.removePointerMoveEventHandler('pointerMoveTooltipHandler');
|
||||
map.removeOverlay(GisApp.measure.helpTooltip);
|
||||
GisApp.select.helpTooltipElement.remove();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 툴팁용 포인터 move 이벤트
|
||||
*/
|
||||
pointerMoveTooltipHandler: function (evt) {
|
||||
if (evt.dragging) {
|
||||
return;
|
||||
}
|
||||
//GisApp.select.helpTooltipElement.innerHTML = DOMPurify.sanitize(GisApp.select.helpMsg);
|
||||
GisApp.select.helpTooltipElement.innerHTML = GisApp.select.helpMsg;
|
||||
GisApp.select.helpTooltip.setPosition(evt.coordinate);
|
||||
GisApp.select.helpTooltipElement.classList.remove('hidden');
|
||||
},
|
||||
getVisible: function (layer) {
|
||||
let maxZoom = layer.getMaxZoom();
|
||||
let minZoom = layer.getMinZoom();
|
||||
let map = this.map;
|
||||
let curZoom = map.getView().getZoom();
|
||||
if (layer.getVisible() && maxZoom > curZoom && minZoom < curZoom) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* 피처 셀렉트
|
||||
*/
|
||||
selectFeature: function (feature, auto) {
|
||||
let me = this;
|
||||
me.selectedFeatures.clear();
|
||||
me.selectedFeatures.push(feature);
|
||||
me.selectInteraction.dispatchEvent({
|
||||
type: 'select',
|
||||
selected: me.selectedFeatures,
|
||||
deselected: [],
|
||||
auto: auto,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default GisOlSelect;
|
||||
@@ -0,0 +1,402 @@
|
||||
import GisApp from './gis.core.js';
|
||||
import Style from 'ol/style/Style.js';
|
||||
import Fill from 'ol/style/Fill.js';
|
||||
import Icon from 'ol/style/Icon.js';
|
||||
import Circle from 'ol/style/Circle.js';
|
||||
import Stroke from 'ol/style/Stroke.js';
|
||||
import Text from 'ol/style/Text.js';
|
||||
import Point from 'ol/geom/Point.js';
|
||||
import RegularShape from 'ol/style/RegularShape.js';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import _ from 'lodash';
|
||||
//import DOMPurify from 'dompurify';
|
||||
import { createPortal } from 'react-dom';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import React from 'react';
|
||||
|
||||
const GisOlStyle = function () {
|
||||
this.initialize();
|
||||
};
|
||||
|
||||
GisOlStyle.prototype = {
|
||||
overlays: {},
|
||||
hiddenOverlays: {},
|
||||
initialize: function () {
|
||||
this.layericonPath = GisApp.CONTEXT_LAYER_PATH;
|
||||
},
|
||||
getRGBAToString: function (r, g, b, a) {
|
||||
let rgba = 'rgba({r}, {g}, {b}, {a})';
|
||||
return rgba.replace('{r}', r).replace('{g}', g).replace('{b}', b).replace('{a}', a);
|
||||
},
|
||||
getFillColor: function (layer) {
|
||||
let r = layer.get('fillColorR');
|
||||
let g = layer.get('fillColorG');
|
||||
let b = layer.get('fillColorB');
|
||||
let a = layer.get('fillColorA');
|
||||
return this.getRGBAToString(r, g, b, a);
|
||||
},
|
||||
getFontLineColor: function (layer) {
|
||||
let r = layer.get('fontOutlineColorR');
|
||||
let g = layer.get('fontOutlineColorG');
|
||||
let b = layer.get('fontOutlineColorB');
|
||||
let a = layer.get('fontOutlineColorA');
|
||||
return this.getRGBAToString(r, g, b, a);
|
||||
},
|
||||
getFontFillColor: function (layer) {
|
||||
let r = layer.get('fontColorR');
|
||||
let g = layer.get('fontColorG');
|
||||
let b = layer.get('fontColorB');
|
||||
let a = layer.get('fontColorA');
|
||||
return this.getRGBAToString(r, g, b, a);
|
||||
},
|
||||
point: function (layer, options, feature, resolution) {
|
||||
let symbol = layer.get('iconNm');
|
||||
let maticKey = layer.get('maticKey');
|
||||
let maticVal1 = layer.get('maticVal1');
|
||||
let image;
|
||||
let style = new Style({
|
||||
fill: new Fill(),
|
||||
});
|
||||
|
||||
let text = this.getText(layer, feature, resolution);
|
||||
style.setText(text);
|
||||
|
||||
if (symbol) {
|
||||
let rotation = 0;
|
||||
if (feature) {
|
||||
let angDir = feature.get('angDir');
|
||||
if (angDir) {
|
||||
rotation = (angDir * Math.PI) / 180;
|
||||
}
|
||||
}
|
||||
image = new Icon({
|
||||
crossOrigin: 'anonymous',
|
||||
scale: 1.2,
|
||||
src: this.layericonPath + symbol,
|
||||
rotation: rotation,
|
||||
});
|
||||
} else {
|
||||
image = new Circle({
|
||||
fill: new Fill({
|
||||
color: '#4848B4',
|
||||
}),
|
||||
radius: 5,
|
||||
});
|
||||
}
|
||||
|
||||
const overlayHtml = feature.get('html');
|
||||
if (overlayHtml) {
|
||||
this.setOverlay(layer, feature, overlayHtml);
|
||||
}
|
||||
|
||||
let baseResolution = 20;
|
||||
let minScale = 0.4;
|
||||
let maxScale = 1.5;
|
||||
let scale = baseResolution / resolution > maxScale ? maxScale : baseResolution / resolution;
|
||||
scale = Math.max(minScale, Math.min(maxScale, scale));
|
||||
image.setScale(scale);
|
||||
style.setImage(image);
|
||||
_.extend(style, options);
|
||||
return style;
|
||||
},
|
||||
line: function (layer, options, feature, resolution) {
|
||||
let linewidth = layer.get('lnWdth');
|
||||
let lineColor = layer.get('lnColor');
|
||||
let style = new Style({
|
||||
stroke: new Stroke({
|
||||
color: lineColor,
|
||||
width: linewidth,
|
||||
}),
|
||||
text: new Text(),
|
||||
});
|
||||
|
||||
if (GisApp.Module.toolbar && GisApp.Module.toolbar.defaults.blockDiagramVisible) {
|
||||
let blockDiagramLayerId = GisApp.Module.toolbar.getBlockDiagramLayerId(layer.get('lyrId'));
|
||||
if (blockDiagramLayerId) {
|
||||
style = this.getArrowStyle(feature, lineColor, linewidth);
|
||||
}
|
||||
}
|
||||
return style;
|
||||
},
|
||||
poligon: function (layer, options, feature, resolution) {
|
||||
const style = new Style();
|
||||
const color = layer.get('polgnColor');
|
||||
const fill = new Fill({
|
||||
color,
|
||||
});
|
||||
style.setFill(fill);
|
||||
_.extend(style, options);
|
||||
return style;
|
||||
},
|
||||
lineLabelPoligon: function (layer, options, feature, resolution) {
|
||||
let style = new Style();
|
||||
const polgnColor = layer.get('polgnColor');
|
||||
let fill = new Fill({
|
||||
color: polgnColor,
|
||||
});
|
||||
let lnWdth = layer.get('lnWdth');
|
||||
let lineColor = layer.get('lnColor');
|
||||
let lineDash = layer.get('lineDash');
|
||||
let stroke = new Stroke({
|
||||
color: lineColor || '#000000',
|
||||
width: lnWdth,
|
||||
lineDash: lineDash ? [10, 5] : null,
|
||||
});
|
||||
let text = this.getText(layer, feature, resolution);
|
||||
if (text) {
|
||||
style.setText(text);
|
||||
}
|
||||
const overlayHtml = feature.get('html');
|
||||
if (overlayHtml) {
|
||||
this.setOverlay(layer, feature, overlayHtml);
|
||||
}
|
||||
style.setFill(fill);
|
||||
style.setStroke(stroke);
|
||||
_.extend(style, options);
|
||||
return style;
|
||||
},
|
||||
imagePoligon: function (layer, options, feature, resolution) {
|
||||
let style = new Style();
|
||||
let symbol = layer.get('iconNm');
|
||||
let retGeom = feature.getGeometry().getInteriorPoint();
|
||||
let image = new Icon({
|
||||
crossOrigin: 'anonymous',
|
||||
imgSize: [20, 20],
|
||||
src: this.layericonPath + symbol,
|
||||
});
|
||||
|
||||
let scale = 0.5 / resolution > 1 ? 1 : 0.5 / resolution;
|
||||
image.setScale(1);
|
||||
style.setGeometry(retGeom);
|
||||
style.setImage(image);
|
||||
|
||||
_.extend(style, options);
|
||||
return style;
|
||||
},
|
||||
setOverlay: function (layer, feature, overlayHtml) {
|
||||
const layerFeature = layer.getSource().getFeatures()[0];
|
||||
const layerCoordinate = layerFeature ? layerFeature.getGeometry().getCoordinates() : null;
|
||||
|
||||
const properties = feature.getProperties();
|
||||
const overLayId = feature?.get('overLayId');
|
||||
const overLayMinZoom = layer.get('overLayMinZoom') || '12';
|
||||
const overLayOffset = layer?.get('overLayOffset');
|
||||
let findoOverlay = this.overlays[overLayId];
|
||||
if (findoOverlay) {
|
||||
const zoom = GisApp.Module.core.map.getView().getZoom();
|
||||
if (Number(overLayMinZoom) < zoom) {
|
||||
findoOverlay.options.element.style.display = 'block';
|
||||
} else {
|
||||
findoOverlay.options.element.style.display = 'none';
|
||||
}
|
||||
return findoOverlay;
|
||||
}
|
||||
let overlayContainer = document.createElement('div');
|
||||
document.body.appendChild(overlayContainer);
|
||||
|
||||
if (typeof overlayHtml === 'string') {
|
||||
//const sanitizedHtml = DOMPurify.sanitize(overlayHtml);
|
||||
overlayContainer.innerHTML = sanitizedHtml;
|
||||
}
|
||||
// React 컴포넌트일 경우 createPortal() 사용
|
||||
else if (React.isValidElement(overlayHtml)) {
|
||||
ReactDOM.createRoot(overlayContainer).render(createPortal(overlayHtml, overlayContainer));
|
||||
}
|
||||
|
||||
// 오버레이 생성
|
||||
let overlay = new Overlay({
|
||||
element: overlayContainer,
|
||||
positioning: 'bottom-center',
|
||||
stopEvent: false,
|
||||
offset: overLayOffset ? overLayOffset : [0, 0], // 오프셋 설정 (위치 조정)
|
||||
});
|
||||
this.overlays[overLayId] = overlay;
|
||||
|
||||
// feature의 좌표 가져오기
|
||||
let geometry = feature.getGeometry();
|
||||
let coordinate;
|
||||
// Point인 경우
|
||||
if (geometry.getType() === 'Point') {
|
||||
coordinate = geometry.getCoordinates();
|
||||
}
|
||||
// LineString 또는 MultiLineString인 경우
|
||||
else if (geometry.getType() === 'LineString' || geometry.getType() === 'MultiLineString') {
|
||||
coordinate = geometry.getFlatMidpoint(); // 선의 중간 좌표를 가져옴
|
||||
}
|
||||
// Polygon 또는 MultiPolygon인 경우
|
||||
else if (geometry.getType() === 'Polygon' || geometry.getType() === 'MultiPolygon') {
|
||||
coordinate = geometry.getExtent();
|
||||
const center = [(coordinate[0] + coordinate[2]) / 2, (coordinate[1] + coordinate[3]) / 2];
|
||||
coordinate = center;
|
||||
} else {
|
||||
console.error('Unsupported geometry type: ' + geometry.getType());
|
||||
return;
|
||||
}
|
||||
overlay.setPosition(coordinate);
|
||||
GisApp.Module.core.map.addOverlay(overlay);
|
||||
const zoom = GisApp.Module.core.map.getView().getZoom();
|
||||
if (Number(overLayMinZoom) < zoom) {
|
||||
overlay.options.element.style.display = 'block';
|
||||
} else {
|
||||
overlay.options.element.style.display = 'none';
|
||||
}
|
||||
},
|
||||
label: function (layer, options, feature, resolution) {
|
||||
let textBaseline = layer.get('textBaseline') || 'bottom';
|
||||
let textAlign = layer.get('textAlign') || 'center';
|
||||
let fontExtnColor = layer.get('fontExtnColor');
|
||||
let fontExtnWdth = Number(layer.get('fontExtnWdth') || '3.5');
|
||||
let fontIntnColor = layer.get('fontIntnColor');
|
||||
let style = new Style();
|
||||
let prop = feature.getProperties();
|
||||
let label = prop[layer.get('lblAtrbNm')];
|
||||
|
||||
let text = new Text({
|
||||
stroke: new Stroke({
|
||||
width: fontExtnWdth,
|
||||
color: fontExtnColor,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: fontIntnColor,
|
||||
}),
|
||||
font: layer.get('font'),
|
||||
textBaseline: textBaseline,
|
||||
textAlign: textAlign,
|
||||
text: label,
|
||||
});
|
||||
style.setStroke(null);
|
||||
style.setFill(null);
|
||||
style.setText(text);
|
||||
_.extend(style, options);
|
||||
|
||||
return style;
|
||||
},
|
||||
getText: function (layer, feature, resolution) {
|
||||
let textBaseline = layer.get('textBaseline') || 'middle';
|
||||
let textAlign = layer.get('textAlign') || 'center';
|
||||
let fontExtnColor = layer.get('fontExtnColor');
|
||||
let fontExtnWdth = Number(layer.get('fontExtnWdth') || '1');
|
||||
let fontIntnColor = layer.get('fontIntnColor');
|
||||
let minViewSize = layer.get('minViewSize');
|
||||
if (feature == null) {
|
||||
return;
|
||||
}
|
||||
let prop = feature.getProperties();
|
||||
let label = prop[layer.get('lblAtrbNm')];
|
||||
let scaleFactor = 1 / resolution; // 해상도가 낮아질수록 글자 크기 증가
|
||||
const zoom = GisApp.Module.core.map.getView().getZoom();
|
||||
if (zoom < minViewSize - 3) {
|
||||
label = '';
|
||||
}
|
||||
let text = new Text({
|
||||
stroke: new Stroke({
|
||||
color: '#FFFFFF', // 흰색 테두리
|
||||
width: fontExtnWdth,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: fontIntnColor,
|
||||
width: fontExtnWdth,
|
||||
}),
|
||||
font: `${layer.get('fontNm')}`, // 동적으로 폰트 크기 적용
|
||||
textBaseline: textBaseline,
|
||||
textAlign: textAlign,
|
||||
overflow: true,
|
||||
text: label,
|
||||
offsetY: 20,
|
||||
});
|
||||
|
||||
return text;
|
||||
},
|
||||
getArrowStyle: function (feature, lineColor, linewidth) {
|
||||
let styles = [
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: lineColor,
|
||||
width: linewidth,
|
||||
}),
|
||||
}),
|
||||
];
|
||||
let geomLienString = GisApp.Module.core.getGeomLineOfFeature(feature);
|
||||
if (geomLienString) {
|
||||
let startCoord = geomLienString.getFirstCoordinate();
|
||||
let endCoord = geomLienString.getLastCoordinate();
|
||||
|
||||
let dx = endCoord[0] - startCoord[0];
|
||||
let dy = endCoord[1] - startCoord[1];
|
||||
let rotation = Math.atan2(dy, dx);
|
||||
|
||||
let centerCoord = GisApp.Module.core.getCenterOfLineFeature(feature);
|
||||
styles.push(
|
||||
new Style({
|
||||
geometry: new Point(centerCoord),
|
||||
image: new RegularShape({
|
||||
fill: new Fill({ color: lineColor }),
|
||||
points: 3,
|
||||
radius: 6,
|
||||
rotation: -rotation,
|
||||
angle: Math.PI / 2,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
return styles;
|
||||
},
|
||||
//레이어 스타일 반환
|
||||
getLayerStyle: function (layer, options) {
|
||||
let gobjTy = layer.get('gobjTy');
|
||||
if (!gobjTy) return;
|
||||
let style = this[gobjTy](layer, options);
|
||||
return style;
|
||||
},
|
||||
getLayerStyleFunction: function (layer, options) {
|
||||
let gobjTy = layer.get('gobjTy');
|
||||
let styleFunction = function (feature, resolution) {
|
||||
return this[gobjTy](layer, options, feature, resolution);
|
||||
}.bind(this);
|
||||
return styleFunction;
|
||||
},
|
||||
adjustOverlayPosition: function (overlay, coordinate) {
|
||||
const overlays = this.overlays;
|
||||
let adjustedCoordinate = [...coordinate]; // 초기 좌표 설정
|
||||
const minDistance = 100000; // 최소 거리 (필요시 조정)
|
||||
let offset = overlay.getOffset() || [0, 0];
|
||||
|
||||
let closestOverlay = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
// 모든 오버레이의 위치를 비교하여 가장 가까운 오버레이 찾기
|
||||
Object.values(overlays).forEach(existingOverlay => {
|
||||
if (existingOverlay) {
|
||||
const existingCoordinate = existingOverlay.getPosition();
|
||||
const existingOffset = existingOverlay.getOffset() || [0, 0];
|
||||
|
||||
// 현재 오버레이의 좌표 가져오기
|
||||
const overlayCoordinate = overlay.getPosition();
|
||||
if (!overlayCoordinate || !existingCoordinate) return;
|
||||
|
||||
// 거리 계산 (유클리드 거리)
|
||||
const distance = Math.sqrt(Math.pow(overlayCoordinate[0] - existingCoordinate[0], 2) + Math.pow(overlayCoordinate[1] - existingCoordinate[1], 2));
|
||||
|
||||
// 가장 가까운 오버레이 업데이트
|
||||
if (distance > 0 && distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestOverlay = existingOverlay;
|
||||
} else {
|
||||
offset = [50, 50];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 가장 가까운 오버레이가 있으면 offset 조정
|
||||
if (closestOverlay && closestDistance < minDistance) {
|
||||
const closestOffset = closestOverlay.getOffset() || [0, 0];
|
||||
// X, Y 좌표를 기존 offset 값의 반대로 설정
|
||||
offset = [-closestOffset[0], -closestOffset[1]];
|
||||
}
|
||||
|
||||
// 최종 offset 적용
|
||||
overlay.setOffset(offset);
|
||||
},
|
||||
};
|
||||
|
||||
export default GisOlStyle;
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,596 @@
|
||||
|
||||
//공통
|
||||
export const util = {
|
||||
getOneToArrayByObjectKey: function (arr, key, value) {
|
||||
let result = {};
|
||||
arr.forEach(item => {
|
||||
if (item[key] === value) {
|
||||
result = item;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
},
|
||||
groupByProperty: (arr, prop, parentProp, group) => {
|
||||
const tree = [];
|
||||
const mappedArr = {};
|
||||
let arrElem;
|
||||
let mappedElem;
|
||||
// 먼저 배열의 노드를 개체에 매핑하고 -> 해시 테이블을 생성
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arrElem = arr[i];
|
||||
if (group && arrElem.mnuGroup !== group) continue;
|
||||
mappedArr[arrElem[prop]] = arrElem;
|
||||
mappedArr[arrElem[prop]].children = [];
|
||||
}
|
||||
|
||||
for (const mnuCd in mappedArr) {
|
||||
if (mappedArr.hasOwnProperty(mnuCd)) {
|
||||
mappedElem = mappedArr[mnuCd];
|
||||
// 요소가 루트 수준에 없으면 자식의 부모 배열에 추가.
|
||||
if (mappedElem[parentProp] && mappedArr[mappedElem[parentProp]]?.children) {
|
||||
mappedArr[mappedElem[parentProp]].children.push(mappedElem);
|
||||
}
|
||||
// 요소가 루트 수준에 있는 경우 첫 번째 수준 요소 배열에 추가
|
||||
else {
|
||||
tree.push(mappedElem);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
},
|
||||
getLowestChildren: function (obj) {
|
||||
if (!obj.children || obj.children.length === 0) {
|
||||
return obj;
|
||||
} else {
|
||||
return this.getLowestChildren(obj.children[0]);
|
||||
}
|
||||
},
|
||||
hasClass: (e, clazz) => {
|
||||
return e.target.classList.contains(clazz);
|
||||
},
|
||||
isNull: function (str) {
|
||||
if (typeof str === 'undefined' || str === null || str === 'null' || str === '' || str === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
nvl: function (str, returnVal) {
|
||||
if (this.isNull(str)) {
|
||||
return returnVal;
|
||||
}
|
||||
return str;
|
||||
},
|
||||
isNumeric: function (str) {
|
||||
const pattern = /\d/;
|
||||
return pattern.test(str);
|
||||
},
|
||||
isAlpha: function (str) {
|
||||
const pattern = /[a-zA-Z]/;
|
||||
return pattern.test(str);
|
||||
},
|
||||
isAlphaNumeric: function (str) {
|
||||
const pattern = /^[a-zA-Z0-9]+$/;
|
||||
return pattern.test(str);
|
||||
},
|
||||
isSpecialChar: function (str) {
|
||||
const pattern = /[~!@#$%^&*()_+|<>?:{}]/;
|
||||
return pattern.test(str);
|
||||
},
|
||||
isKoreanAplhaNumeric: function (str) {
|
||||
const pattern = /^[ㄱ-ㅎ|ㅏ-ㅣ|가-힣|a-zA-Z0-9]+$/;
|
||||
return pattern.test(str);
|
||||
},
|
||||
isValidPassword: function (str) {
|
||||
const pattern = /^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*(),.?":{}|<>])[a-zA-Z0-9!@#$%^&*(),.?":{}|<>]{8,20}$/;
|
||||
return pattern.test(str);
|
||||
},
|
||||
isValidExt: function (fileName, allowedExtensions) {
|
||||
const fileExtension = fileName.split('.').pop().toLowerCase();
|
||||
return allowedExtensions.includes(fileExtension);
|
||||
},
|
||||
isValidAllDate: function (dateString) {
|
||||
const formats = ['YYYY', 'YYYYMM', 'YYYYMMDD'];
|
||||
for (let format of formats) {
|
||||
if (moment(dateString, format, true).isValid()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isEmptyObject: function (param) {
|
||||
return Object.keys(param).length === 0 && param.constructor === Object;
|
||||
},
|
||||
castArrayObjectToList: function (array, key) {
|
||||
var newArray = [];
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
newArray.push(array[i][key]);
|
||||
}
|
||||
return newArray;
|
||||
},
|
||||
arraysMatch: function (arr1, arr2) {
|
||||
// 길이가 같은지 확인
|
||||
if (arr1.length !== arr2.length) return false;
|
||||
|
||||
// 각 요소가 일치하는지 확인
|
||||
return arr1.every((element, index) => element === arr2[index]);
|
||||
},
|
||||
//상위뎁스 객체들 조회
|
||||
findObjectByKeyAndValue: function ({ array, find: { key: findKey, value: findValue }, result: { key: resultKey }, parents = [] }) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
let obj = array[i];
|
||||
let siblings = [...array];
|
||||
//siblings.splice(i, 1); // 현재 메뉴를 제거하여 형제 메뉴들만 남김
|
||||
let currentParents = [...parents, { [resultKey]: obj, siblings: siblings }];
|
||||
|
||||
if (obj[findKey] === findValue) {
|
||||
let formattedResult = currentParents.reduce((acc, cur, idx) => {
|
||||
acc[`${resultKey}${idx + 1}`] = { active: cur[resultKey], list: cur.siblings };
|
||||
return acc;
|
||||
}, {});
|
||||
return formattedResult;
|
||||
}
|
||||
if (obj.children) {
|
||||
let result = this.findObjectByKeyAndValue({ array: obj.children, find: { key: findKey, value: findValue }, result: { key: resultKey }, parents: currentParents });
|
||||
if (result !== null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
autoComma(val, returnVal) {
|
||||
if (!val && val !== 0) return returnVal ? returnVal : '';
|
||||
|
||||
// 문자열로 변환하고, 숫자와 소수점만을 허용
|
||||
val = String(val).replace(/[^0-9.-]/g, '');
|
||||
|
||||
// 소수점을 기준으로 숫자를 분리
|
||||
let [integerPart, decimalPart] = val.split('.');
|
||||
|
||||
// 정수 부분에 대해 쉼표 추가
|
||||
integerPart = integerPart.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
||||
|
||||
// 소수점 이하 숫자가 있다면 다시 합치기
|
||||
return decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
|
||||
},
|
||||
removeComma: function (str) {
|
||||
str = String(str);
|
||||
return str.replace(/[^\d]+/g, '');
|
||||
},
|
||||
removeSpaceAndTrim: function (str) {
|
||||
if (str) {
|
||||
str = String(str);
|
||||
return str.replace(/\s/g, '');
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
},
|
||||
isNotWhiteSpace: function (str) {
|
||||
const hasWhitespace = /\s/.test(str);
|
||||
return !hasWhitespace;
|
||||
},
|
||||
//숫자만 남기고 모두 제거해서 반환
|
||||
retainOnlyNumbers: function (str) {
|
||||
str = String(str);
|
||||
return str.replace(/\D+/g, '');
|
||||
},
|
||||
proxyToObject: function (proxy) {
|
||||
return JSON.parse(JSON.stringify(Object.assign({}, proxy)));
|
||||
},
|
||||
dateFormatter: function ({ input, isHan = false }) {
|
||||
const value = this.retainOnlyNumbers(input);
|
||||
|
||||
if (value.length === 6) {
|
||||
const date = moment(value, 'YYYYMM');
|
||||
return date.format(isHan ? 'YYYY년 MM월' : 'YYYY-MM');
|
||||
} else if (value.length === 8) {
|
||||
const date = moment(value, 'YYYYMMDD');
|
||||
return date.format(isHan ? 'YYYY년 MM월 DD일' : 'YYYY-MM-DD');
|
||||
} else if (value.length === 14) {
|
||||
const date = moment(value, 'YYYYMMDDHHmmss');
|
||||
return date.format(isHan ? 'YYYY년 MM월 DD일 HH시 mm분 ss초' : 'YYYY-MM-DD HH:MM:SS');
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
},
|
||||
generateYearArray: function (num) {
|
||||
const currentYear = moment().year();
|
||||
if (num <= 1 || isNaN(num)) {
|
||||
return [(currentYear - 2).toString(), (currentYear - 1).toString(), currentYear.toString()];
|
||||
}
|
||||
|
||||
const years = [];
|
||||
for (let i = num - 1; i >= 0; i--) {
|
||||
years.push((currentYear - i).toString());
|
||||
}
|
||||
|
||||
return years;
|
||||
},
|
||||
getFileId: function () {
|
||||
let timeStamp = moment().format('YYYYMMDDHHmmssSSS'); // YYYYMMDDHHmmssSSS 형태로 타임스탬프를 생성
|
||||
return 'CFM_' + timeStamp.slice(-12); // slice(-12)는 문자열의 끝에서 12자리를 추출합니다.
|
||||
},
|
||||
lpad: function (number, pad, length) {
|
||||
let str = number.toString();
|
||||
while (str.length < length) {
|
||||
str = pad + str;
|
||||
}
|
||||
return str;
|
||||
},
|
||||
convertKeysToCamelCase: function (obj) {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
return Object.keys(obj).reduce((acc, key) => {
|
||||
const camelCaseKey = key.replace(/([-_]\w)/g, match => match.replace(/[-_]/, '').toUpperCase());
|
||||
acc[camelCaseKey] = typeof obj[key] === 'object' ? this.convertKeysToCamelCase(obj[key]) : obj[key];
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
blobUrlToBlob: async blobUrl => {
|
||||
try {
|
||||
const response = await fetch(blobUrl);
|
||||
return await response.blob();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return new Blob();
|
||||
}
|
||||
},
|
||||
hexToRgba: hex => {
|
||||
const r = parseInt(hex.substring(1, 3), 16);
|
||||
const g = parseInt(hex.substring(3, 5), 16);
|
||||
const b = parseInt(hex.substring(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b} , 0.5)`;
|
||||
},
|
||||
rgbaToHex: rgba => {
|
||||
/*rgba값을 가져와서 rgb 로 추출 후 hex 값 반환 */
|
||||
if (!rgba) return false;
|
||||
const rgbArray = rgba.match(/\d+/g).slice(0, 3).map(Number);
|
||||
return `#${((1 << 24) | (rgbArray[0] << 16) | (rgbArray[1] << 8) | rgbArray[2]).toString(16).slice(1).toUpperCase()}`;
|
||||
},
|
||||
rgbAddOpacity: (rgb, opacity) => {
|
||||
const rgbValues = rgb.match(/\d+/g); // rgb 값을 배열로 추출
|
||||
const colorWithOpacity = `rgba(${rgbValues[0]}, ${rgbValues[1]}, ${rgbValues[2]}, ${opacity})`; // rgba로 변환
|
||||
return colorWithOpacity;
|
||||
},
|
||||
generateMonthArray: (pSmonth, pEmonth) => {
|
||||
let startYear = Math.floor(pSmonth / 100);
|
||||
let startMonth = pSmonth % 100;
|
||||
let endYear = Math.floor(pEmonth / 100);
|
||||
let endMonth = pEmonth % 100;
|
||||
|
||||
let months = [];
|
||||
|
||||
for (let year = startYear; year <= endYear; year++) {
|
||||
let monthStart = year === startYear ? startMonth : 1;
|
||||
let monthEnd = year === endYear ? endMonth : 12;
|
||||
|
||||
for (let month = monthStart; month <= monthEnd; month++) {
|
||||
let monthString = month < 10 ? `0${month}` : `${month}`;
|
||||
months.push(`${year}${monthString}`);
|
||||
}
|
||||
}
|
||||
|
||||
return months;
|
||||
},
|
||||
findDuplicatesForArr: (arr, key) => {
|
||||
const map = new Map();
|
||||
const duplicates = [];
|
||||
|
||||
arr.forEach(item => {
|
||||
const keyValue = item[key];
|
||||
if (map.has(keyValue)) {
|
||||
if (!duplicates.includes(keyValue)) {
|
||||
duplicates.push(keyValue);
|
||||
}
|
||||
} else {
|
||||
map.set(keyValue, true);
|
||||
}
|
||||
});
|
||||
|
||||
return duplicates;
|
||||
},
|
||||
createHtmlFromJson: (html, data) => {
|
||||
if (!html || util.isEmptyObject(html)) return null;
|
||||
const { classNm, headerKey, rows } = JSON.parse(html);
|
||||
if (!rows) return null;
|
||||
const tableRows = rows
|
||||
.map(row => {
|
||||
const value = data?.[row.key] ?? null;
|
||||
let displayValue;
|
||||
if (typeof value === 'number') {
|
||||
displayValue = util.autoComma(value, 0);
|
||||
} else if (typeof value === 'string') {
|
||||
displayValue = util.nvl(value, '-');
|
||||
} else {
|
||||
displayValue = '-';
|
||||
}
|
||||
return `
|
||||
<tr>
|
||||
<th>${row.th}</th>
|
||||
<td>${displayValue}</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
return `
|
||||
<div class="${classNm}">
|
||||
<table>
|
||||
<tr>
|
||||
<th colspan="2" style="position: relative; text-align: center; background-color: #DEEAF3;">
|
||||
${data?.[headerKey] ?? '-'}
|
||||
</th>
|
||||
</tr>
|
||||
${tableRows}
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* 다이렉트로 import하여 사용하기 위해 export로 수정중
|
||||
*
|
||||
* */
|
||||
|
||||
//객체 배열에서 특정 key의 value인 것을 추출
|
||||
export const getArrayByObjectKey = function (arr, key, value) {
|
||||
return arr.filter(item => item[key] === value);
|
||||
};
|
||||
|
||||
//소수점 1자리를 기준으로 점을 언더스코어로 변경
|
||||
export const numberDotToUnderscore = function (num) {
|
||||
if (Number.isInteger(num)) {
|
||||
return num.toString();
|
||||
} else {
|
||||
return num.toFixed(1).replace('.', '_');
|
||||
}
|
||||
};
|
||||
|
||||
export const stripHTML = function (htmlString) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
return doc.body.textContent || '';
|
||||
};
|
||||
|
||||
//URL에서 매개변수 추출
|
||||
export function getParameterByName(name, url = window.location.search) {
|
||||
name = name.replace(/[\[\]]/g, '\\$&');
|
||||
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
|
||||
results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
export function dateFormatForDB({ date: isoString, format = 'YYYY-MM-DD HH:MI:SS' }) {
|
||||
if (!isoString) return '';
|
||||
|
||||
let date = new Date(isoString);
|
||||
|
||||
let year = date.getFullYear();
|
||||
let month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
let day = date.getDate().toString().padStart(2, '0');
|
||||
|
||||
let hours = date.getHours().toString().padStart(2, '0');
|
||||
let minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
let seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
return (
|
||||
format
|
||||
//
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('MI', minutes)
|
||||
.replace('SS', seconds)
|
||||
);
|
||||
}
|
||||
|
||||
//blob 다운로드
|
||||
//blob 다운로드
|
||||
export function downloadFromBlob(res, data) {
|
||||
const url = URL.createObjectURL(new Blob([res.data], { type: res.data.type }));
|
||||
const link = document.createElement('a');
|
||||
|
||||
const contentDisposition = res.headers?.['content-disposition']; // 파일 이름
|
||||
let fileName = data?.fileName || 'export'; //기본 파일 이름
|
||||
|
||||
if (contentDisposition) {
|
||||
const [fileNameMatch] = contentDisposition.split(';').filter(str => str.includes('filename'));
|
||||
if (fileNameMatch) {
|
||||
[, fileName] = fileNameMatch.split('=');
|
||||
// 파일명이 "UTF-8''" 등으로 인코딩되어 있을 수 있으니 decodeURIComponent 추가
|
||||
fileName = decodeURIComponent(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
link.href = url;
|
||||
// 브라우저 인코딩 관련 이슈 방지를 위해 파일명에 대한 추가 decodeURI 처리
|
||||
link.download = decodeURIComponent(fileName).replace(/"/g, '').trim();
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function downloadBlobAsFile(blob, filename = 'export') {
|
||||
// 브라우저에 따라 인코딩 처리 햔재는 파이어폭스만
|
||||
const url = URL.createObjectURL(blob);
|
||||
const encodedFilename = encodeURIComponent(filename);
|
||||
const downloadLink = document.createElement('a');
|
||||
|
||||
downloadLink.href = url;
|
||||
downloadLink.download = decodeURIComponent(encodedFilename);
|
||||
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
|
||||
document.body.removeChild(downloadLink);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function viewFile(res, data) {
|
||||
// Blob URL 생성
|
||||
const url = URL.createObjectURL(new Blob([res.data], { type: res.data.type }));
|
||||
|
||||
let fileType = null;
|
||||
let fileName = data?.fileName || 'export'; // 기본 파일 이름
|
||||
|
||||
// PDF 및 이미지 타입의 MIME 타입 확인
|
||||
if (res.data.type === 'application/pdf') {
|
||||
fileType = 'pdf';
|
||||
} else if (res.data.type.startsWith('image/')) {
|
||||
fileType = 'img';
|
||||
} else if (res.data.type === 'application/octet-stream') {
|
||||
// Content-Disposition 헤더에서 파일 이름 추출
|
||||
const contentDisposition = res.headers?.['content-disposition'];
|
||||
if (contentDisposition) {
|
||||
const fileNameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (fileNameMatch && fileNameMatch[1]) {
|
||||
fileName = fileNameMatch[1].replace(/['"]/g, ''); // 파일 이름 추출
|
||||
fileName = decodeURIComponent(fileName); // URL 인코딩된 파일 이름을 디코딩
|
||||
}
|
||||
}
|
||||
|
||||
// 파일 확장자 추출
|
||||
const fileExtension = fileName.split('.').pop().toLowerCase();
|
||||
|
||||
// 확장자를 통해 파일 타입 결정
|
||||
if (fileExtension === 'pdf') {
|
||||
fileType = 'pdf';
|
||||
} else if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'].includes(fileExtension)) {
|
||||
fileType = 'img';
|
||||
} else {
|
||||
console.error('지원하지 않는 파일 형식입니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// URL과 파일 타입 반환
|
||||
return { url, fileType, fileName };
|
||||
}
|
||||
|
||||
export function getToday() {
|
||||
return getDate('today');
|
||||
}
|
||||
|
||||
export function getDate(type, diff) {
|
||||
const date = new Date();
|
||||
|
||||
switch (type) {
|
||||
case 'year':
|
||||
date.setFullYear(date.getFullYear() + diff);
|
||||
break;
|
||||
case 'month':
|
||||
date.setMonth(date.getMonth() + diff);
|
||||
break;
|
||||
case 'day':
|
||||
date.setDate(date.getDate() + diff);
|
||||
break;
|
||||
case 'today':
|
||||
date.setDate(date.getDate());
|
||||
break;
|
||||
default:
|
||||
console.log('잘못된 타입입니다. 올바른 타입: 년, 월, 일');
|
||||
return;
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
|
||||
return `${year}${month}${day}`;
|
||||
}
|
||||
|
||||
//promise 처리
|
||||
export const byPromise = function (actions) {
|
||||
return actions.reduce((promise, action) => {
|
||||
return promise.then(async () => {
|
||||
if (typeof action === 'function') {
|
||||
return await action();
|
||||
} else {
|
||||
return await action;
|
||||
}
|
||||
});
|
||||
}, Promise.resolve());
|
||||
};
|
||||
|
||||
export const getMessage = (msgGroup, msgKey, innerText, fix) => {
|
||||
let resultString = GLOBAL.MESSAGE[msgGroup][msgKey];
|
||||
const prefix = fix?.prefix || '';
|
||||
const suffix = fix?.suffix || '';
|
||||
|
||||
if (innerText) {
|
||||
for (const [key, value] of Object.entries(innerText)) {
|
||||
const regex = new RegExp(`\\$\{${key}\}`, 'g');
|
||||
resultString = resultString.replace(regex, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultString) resultString = resultString.replace(/\\n/g, '\n');
|
||||
return resultString ? `${prefix}${resultString}${suffix}` : '안내 메시지 로딩 오류 - 관리자에게 문의하세요.';
|
||||
};
|
||||
|
||||
// global.js의 message를 읽어오는 alert
|
||||
export const gAlert = (msgGroup, msgKey, innerText, fix) => {
|
||||
alert(getMessage(msgGroup, msgKey, innerText, fix));
|
||||
};
|
||||
|
||||
// message를 confirm
|
||||
export const gConfirm = (msgGroup, msgKey, innerText, fix) => {
|
||||
return window.confirm(getMessage(msgGroup, msgKey, innerText, fix));
|
||||
};
|
||||
|
||||
// message를 함수 리턴
|
||||
export const gMessage = getMessage;
|
||||
|
||||
// message를 save후 응답까지 처리
|
||||
export const gSaveCommonConfirm = async ({ message, api = async () => {}, param = {}, success, fail, prefix }) => {
|
||||
try {
|
||||
if (!gConfirm('CONFIRM', `${message}_QUEST`, null, { prefix })) return;
|
||||
|
||||
const result = await api(param, { isAlert: false });
|
||||
|
||||
if (result?.code !== 200) return;
|
||||
|
||||
const rep = message === 'UPLOAD' ? result.data.data : result.data;
|
||||
|
||||
if (rep > 0) {
|
||||
gAlert('INFO', `${message}_COMPLETE`);
|
||||
await success?.();
|
||||
} else {
|
||||
gAlert('ERROR', `${message}_FAIL`);
|
||||
await fail?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('오류 발생:', error); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
|
||||
}
|
||||
};
|
||||
|
||||
export const nullToValue = (text, { value = '-', prefix = '', suffix = '' } = {}) => {
|
||||
if (!text || text === value) {
|
||||
return value;
|
||||
}
|
||||
return `${prefix}${text}${suffix}`;
|
||||
};
|
||||
|
||||
export const filterArrayOfObjectsByKeys = (arrayOfObjects, keys) => {
|
||||
return arrayOfObjects.map(obj => {
|
||||
return keys.reduce((filteredObj, key) => {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
filteredObj[key] = obj[key];
|
||||
}
|
||||
return filteredObj;
|
||||
}, {});
|
||||
});
|
||||
};
|
||||
|
||||
export const isMobile = () => {
|
||||
const width = window.innerWidth > 0 ? window.innerWidth : window.screen.width;
|
||||
|
||||
return width < GLOBAL.MOBILE_SIZE;
|
||||
};
|
||||
@@ -5,5 +5,5 @@ function SearchDiv ({children}:{ children: React.ReactNode; }) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export { SearchDiv };
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export default function SearchIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6 text-gray-700"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21 21l-4.35-4.35M17 11A6 6 0 105 11a6 6 0 0012 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @remarks
|
||||
* 이 버전은 SearchModal에서 사용하기 위해 `SelectContent`의 z-index를 `z-[101]`로 수정하였습니다.
|
||||
*/
|
||||
import { Label } from "@common/components";
|
||||
import React from "react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./lib/selectBox";
|
||||
|
||||
/** 차트 기본 스타일 */
|
||||
interface SelectBox1 {
|
||||
text:string,
|
||||
val:string|number
|
||||
}
|
||||
|
||||
const SelectBox2 = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
label:string,
|
||||
selectClass:string|undefined,
|
||||
className:string,
|
||||
selectArray:SelectBox1[],
|
||||
onSelectValue:(val:string|number)=>void,
|
||||
value?: string|number
|
||||
}
|
||||
>(({ label, className, selectClass, selectArray, onSelectValue, value }, ref)=>{
|
||||
const placeHolder = selectArray?.[0]?.text ?? "";
|
||||
|
||||
return (
|
||||
<div className={className+" flex items-center flex-nowrap"} ref={ref}>
|
||||
{label && <Label className="inline-block w-auto whitespace-nowrap text-xl text-gray-600 mr-2">{label}</Label>}
|
||||
<Select onValueChange={onSelectValue} value={value !== undefined ? String(value) : undefined}>
|
||||
<SelectTrigger className={selectClass?selectClass:"flex-1 p-2 border rounded"}>
|
||||
<SelectValue placeholder={placeHolder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[101]">
|
||||
{selectArray.map((selectArray) => (
|
||||
<SelectItem key={selectArray.val} value={String(selectArray.val)}>{selectArray.text}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export { SelectBox2 };
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Tabs, TabsList, TabsTrigger } from '@common/components/ui';
|
||||
import React from 'react';
|
||||
|
||||
interface TabItem {
|
||||
value: string;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface UITabProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
defaultValue?: string;
|
||||
tabList: TabItem[];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const UITab2 = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
UITabProps
|
||||
>(({defaultValue, tabList, children}, ref)=>{
|
||||
return (
|
||||
<Tabs defaultValue={defaultValue || tabList[0]?.value} className={"w-full"}>
|
||||
<TabsList className="flex">
|
||||
{tabList.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
onClick={tab.onClick}
|
||||
className="py-2 px-4 text-sm font-medium border-b-2 focus:outline-none data-[state=active]:border-blue-500 data-[state=active]:text-blue-600 data-[state=inactive]:border-transparent data-[state=inactive]:text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{children}
|
||||
</Tabs>
|
||||
)
|
||||
})
|
||||
export { UITab2 };
|
||||
@@ -0,0 +1,157 @@
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
import { useEffect, useState } from "react";
|
||||
import UiPageNations from "./uiPageNations";
|
||||
|
||||
type UiTableProps = {
|
||||
headName: React.ReactNode | undefined;
|
||||
tableData: { [key: string]: string | number | boolean | object }[];
|
||||
headlist: UiHeadProps[];
|
||||
pageSize: number;
|
||||
total: number;
|
||||
children: React.ReactNode | undefined;
|
||||
cellClick: ((index: number | undefined, headId: string) => void) | undefined;
|
||||
onPageChange: (page: number) => void;
|
||||
className?: string;
|
||||
resetPageNo?: boolean;
|
||||
cur?: number;
|
||||
height?: string; // ✅ 추가된 prop
|
||||
};
|
||||
|
||||
type UiHeadProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
align?: string;
|
||||
};
|
||||
|
||||
export function UiTable2({
|
||||
tableData,
|
||||
headlist,
|
||||
pageSize,
|
||||
total,
|
||||
children,
|
||||
headName,
|
||||
cellClick,
|
||||
onPageChange,
|
||||
className = '',
|
||||
resetPageNo = false,
|
||||
cur,
|
||||
height = '45vh', // ✅ 기본값 설정
|
||||
}: UiTableProps) {
|
||||
const [idx, setIdx] = useState<number>(-1);
|
||||
const [curState, setCurState] = useState<number>(cur ?? 1);
|
||||
|
||||
useEffect(() => {
|
||||
setCurState(cur ?? 1);
|
||||
}, [cur]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetPageNo) {
|
||||
setCurState(1);
|
||||
onPageChange(1);
|
||||
}
|
||||
}, [onPageChange, resetPageNo]);
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurState(page);
|
||||
onPageChange(page);
|
||||
};
|
||||
|
||||
const handleCellClick = (index: number, headId: string) => {
|
||||
if (cellClick) {
|
||||
cellClick(index, headId);
|
||||
setIdx(index);
|
||||
}
|
||||
};
|
||||
|
||||
const startIdx = (curState - 1) * pageSize;
|
||||
|
||||
return (
|
||||
<>
|
||||
{headName && (
|
||||
<div className="card-header">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">{headName}</h3>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`card !m-[1.2rem] !p-0 relative ${className}`}>
|
||||
{/* ✅ 외부에서 조절 가능한 높이 적용 */}
|
||||
<div className="overflow-auto" style={{ height }}>
|
||||
<Table variant="secondary" className="w-full">
|
||||
<TableHeader className="bg-white">
|
||||
{children ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<TableRow>
|
||||
{headlist.map((head) => (
|
||||
<TableHead
|
||||
key={head.id}
|
||||
className="min-w-[120px] max-w-[180px] w-[150px]
|
||||
text-center whitespace-nowrap overflow-hidden
|
||||
text-ellipsis bg-white"
|
||||
>
|
||||
{head.title}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody className="text-center">
|
||||
{tableData.length > 0 ? (
|
||||
tableData.map((el, index) => (
|
||||
<TableRow key={index + "row"}>
|
||||
{headlist.map((head) => (
|
||||
<TableCell
|
||||
key={head.id + index}
|
||||
className={`
|
||||
${head.align === 'right'
|
||||
? 'text-right'
|
||||
: head.align === 'center'
|
||||
? 'text-center'
|
||||
: typeof el[head.id] === 'number'
|
||||
? 'text-right'
|
||||
: 'text-center'
|
||||
}
|
||||
${cellClick && idx === index ? '!font-[1000] cursor-pointer' :
|
||||
cellClick ? 'cursor-pointer' : ''}
|
||||
`}
|
||||
onClick={() => handleCellClick(startIdx + index, head.id)}
|
||||
>
|
||||
{typeof el[head.id] === 'number'
|
||||
? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||
: String(el[head.id] ?? '')}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={headlist.length} className="p-0">
|
||||
{/* ✅ 높이 보장 + 가운데 정렬용 div */}
|
||||
<div className="flex justify-center items-center text-gray-500" style={{ height: `calc(${height} - 70px)` }}>
|
||||
<span className="text-xl">🔍 검색 결과가 없습니다.</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 하단 고정 */}
|
||||
{ (
|
||||
<div className="sticky left-0 right-0 bottom-0 bg-white z-20 mt-5">
|
||||
<UiPageNations
|
||||
total={total}
|
||||
cur={curState}
|
||||
pageSize={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -30,5 +30,5 @@
|
||||
"**/*.mjs",
|
||||
"**/*.jsx",
|
||||
"**/*.json"
|
||||
]
|
||||
, "../../../apps/sn/app/map/SearchModal.tsx" ]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@common/components",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
@@ -40,6 +40,7 @@
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./client";
|
||||
export * from "./menu";
|
||||
export * from "./popup";
|
||||
export * from "./site";
|
||||
export * from "./user";
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { getApiClient } from "./client";
|
||||
import { ENDPOINT } from "../config";
|
||||
|
||||
export interface PopupInfo {
|
||||
popupSn: number;
|
||||
popupTypeCd: string;
|
||||
popupTypeNm: string;
|
||||
contsTypeCd: string;
|
||||
contsTypeNm: string;
|
||||
popupTtl: string;
|
||||
popupCn: string;
|
||||
popupWdth: number;
|
||||
popupHgt: number;
|
||||
popupVtclPstn: number;
|
||||
popupHrzntltyPstn: number;
|
||||
popupEndUseYn: string;
|
||||
popupLnkgUrl: string;
|
||||
atflCd: string;
|
||||
useYn: string;
|
||||
regDt: string;
|
||||
mdfcnDt: string;
|
||||
rgTrNm: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
interface SortConfig {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
}
|
||||
|
||||
interface PageableConfig {
|
||||
sort: SortConfig;
|
||||
offset: number;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
paged: boolean;
|
||||
unpaged: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const client = getApiClient();
|
||||
|
||||
export const getPopupList = async () => {
|
||||
const response = await client.get<PageableList<PopupInfo>>(
|
||||
`${ENDPOINT.CMS_SERVICE.POPUPS}`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getPopupInfo = async (popupSn: number) => {
|
||||
const response = await client.get<PopupInfo>(
|
||||
`${ENDPOINT.CMS_SERVICE.POPUPS}/${popupSn}`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
@@ -37,9 +37,9 @@ export interface IUser {
|
||||
|
||||
const client = getApiClient();
|
||||
|
||||
export const getUserInfo = async (userUniqId: string) => {
|
||||
export const getUserInfo = async () => {
|
||||
const response = await client.get<IUser>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/${userUniqId}`,
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ export const ENDPOINT = {
|
||||
CMS_SERVICE: {
|
||||
MENU_ROLES: "/cms-service/api/v1/menu-roles",
|
||||
SITES: "/cms-service/api/v1/sites",
|
||||
POPUPS: "/cms-service/api/v1/popups",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
USERS: "/user-service/api/v1/users",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
function PopupProvider({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export { PopupProvider };
|
||||
@@ -2,11 +2,11 @@ import "./button-group.css";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { RadioGroup, RadioGroupItem } from "../radio-group";
|
||||
import { ButtonGroup, ButtonGroupItem } from "../button-group";
|
||||
|
||||
const meta: Meta<typeof RadioGroup> = {
|
||||
const meta: Meta<typeof ButtonGroup> = {
|
||||
title: "Elements/ButtonGroup",
|
||||
component: RadioGroup,
|
||||
component: ButtonGroup,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
@@ -19,49 +19,49 @@ ButtonGroup 컴포넌트는 RadioGroup 컴포넌트를 기반으로 버튼 모
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof RadioGroup>;
|
||||
type Story = StoryObj<typeof ButtonGroup>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="comfortable" variant="button">
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroup defaultValue="comfortable" variant="button">
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="comfortable" variant="button" color="secondary">
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroup defaultValue="comfortable" variant="button" color="secondary">
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
export const Rounded: Story = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="comfortable" variant="button" shape="rounded">
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroup defaultValue="comfortable" variant="button" shape="rounded">
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
export const SecondaryRounded: Story = {
|
||||
render: () => (
|
||||
<RadioGroup
|
||||
<ButtonGroup
|
||||
defaultValue="comfortable"
|
||||
variant="button"
|
||||
color="secondary"
|
||||
shape="rounded"
|
||||
>
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import "./button-group.css";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
|
||||
const ButtonGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> & {
|
||||
variant?: "radio" | "button";
|
||||
color?: "primary" | "secondary";
|
||||
shape?: "square" | "rounded";
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ variant = "radio", color = "primary", shape = "square", ...props },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
{...props}
|
||||
ref={ref}
|
||||
data-variant={variant}
|
||||
data-color={color}
|
||||
data-shape={shape}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
ButtonGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const ButtonGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ ...props }, ref) => {
|
||||
return <RadioGroupPrimitive.Item ref={ref} {...props} />;
|
||||
});
|
||||
ButtonGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { ButtonGroup, ButtonGroupItem };
|
||||
@@ -2,36 +2,36 @@
|
||||
|
||||
@layer components {
|
||||
button[role="button"] {
|
||||
@apply text-[1.3rem];
|
||||
@apply text-[1.3rem] transition-opacity hover:opacity-90;
|
||||
}
|
||||
button[role="button"].primary {
|
||||
@apply bg-[rgba(32,148,250,1)] hover:bg-[rgba(32,148,250,0.9)];
|
||||
@apply bg-primary;
|
||||
}
|
||||
button[role="button"].green {
|
||||
@apply bg-[rgba(31,208,120,1)] hover:bg-[rgba(31,208,120,0.9)];
|
||||
@apply bg-[rgba(31,208,120,1)];
|
||||
}
|
||||
button[role="button"].sky {
|
||||
@apply bg-[rgba(50,154,203,1)] hover:bg-[rgba(50,154,203,0.9)];
|
||||
@apply bg-[rgba(50,154,203,1)];
|
||||
}
|
||||
button[role="button"].yellow {
|
||||
@apply bg-[rgba(243,201,0,1)] hover:bg-[rgba(243,201,0,0.9)];
|
||||
@apply bg-[rgba(243,201,0,1)];
|
||||
}
|
||||
button[role="button"].orange {
|
||||
@apply bg-[rgba(255,149,0,1)] hover:bg-[rgba(255,149,0,0.9)];
|
||||
@apply bg-[rgba(255,149,0,1)];
|
||||
}
|
||||
button[role="button"].emerald {
|
||||
@apply bg-[rgba(90,200,250,1)] hover:bg-[rgba(90,200,250,0.9)];
|
||||
@apply bg-[rgba(90,200,250,1)];
|
||||
}
|
||||
button[role="button"].white {
|
||||
@apply border border-border bg-[rgba(255,255,255,1)] text-[#666] hover:bg-[rgba(255,255,255,0.9)];
|
||||
@apply border border-border bg-[rgba(255,255,255,1)] text-[#666];
|
||||
}
|
||||
button[role="button"].red {
|
||||
@apply bg-[rgba(232,93,85,1)] hover:bg-[rgba(232,93,85,0.9)];
|
||||
@apply bg-destructive;
|
||||
}
|
||||
button[role="button"].gray {
|
||||
@apply bg-[rgba(153,153,153,1)] hover:bg-[rgba(153,153,153,0.9)];
|
||||
@apply bg-[rgba(153,153,153,1)];
|
||||
}
|
||||
button[role="button"].lightGray {
|
||||
@apply bg-[rgba(235,235,235,1)] text-[#929292] hover:bg-[rgba(235,235,235,0.9)];
|
||||
@apply bg-[rgba(235,235,235,1)] text-[#929292];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { HTMLAttributes } from "react";
|
||||
declare module "@tanstack/react-table" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
width?: number;
|
||||
align?: "left" | "center" | "right";
|
||||
rowSpan?: number;
|
||||
}
|
||||
@@ -41,22 +42,31 @@ function DataTable<T>({
|
||||
<Table className={cn("w-full", className)} variant={variant}>
|
||||
<THead table={table} />
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
className={cn(onRowClick && "cursor-pointer hover:bg-accent")}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const { meta } = cell.column.columnDef;
|
||||
return (
|
||||
<TableCell key={cell.id} align="center" {...meta}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
const isSelectedRow = row.getIsSelected();
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
className={cn(
|
||||
onRowClick && "cursor-pointer hover:bg-accent",
|
||||
isSelectedRow && "!bg-primary-transparent",
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const { meta } = cell.column.columnDef;
|
||||
return (
|
||||
<TableCell key={cell.id} align="center" {...meta}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
{viewPagination && <Pagination table={table} />}
|
||||
@@ -90,7 +100,7 @@ function THead<T>({ table }: { table: TableInstance<T> }) {
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className="sticky top-0"
|
||||
className="sticky top-0 z-10"
|
||||
colSpan={header.colSpan}
|
||||
rowSpan={rowSpan}
|
||||
{...meta}
|
||||
@@ -152,7 +162,7 @@ function Pagination<T>({ table }: { table: TableInstance<T> }) {
|
||||
key={pageNumber}
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"inline-block size-10 !bg-transparent p-0 text-foreground hover:!bg-accent",
|
||||
"inline-block h-10 min-w-10 !bg-transparent p-1 text-foreground hover:!bg-accent",
|
||||
currentPage === pageNumber &&
|
||||
"!bg-primary !font-bold !text-primary-foreground hover:!bg-primary",
|
||||
)}
|
||||
|
||||
@@ -9,21 +9,30 @@ import { Button } from "../button";
|
||||
import { Calendar } from "../calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
|
||||
type DatePickerProps = DayPickerSingleProps;
|
||||
type DatePickerProps = {
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
} & DayPickerSingleProps;
|
||||
|
||||
export function DatePicker({ className, ...props }: DatePickerProps) {
|
||||
export function DatePicker({
|
||||
className,
|
||||
placeholder = "날짜 선택",
|
||||
disabled,
|
||||
...props
|
||||
}: DatePickerProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("date-picker-trigger", className)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<CalendarIcon className="!size-[1.6rem]" />
|
||||
{props.selected ? (
|
||||
format(props.selected, "yyyy-MM-dd")
|
||||
) : (
|
||||
<span>날짜 선택</span>
|
||||
<span>{placeholder}</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import type { DateRange } from "react-day-picker";
|
||||
@@ -11,20 +11,38 @@ import { Calendar } from "../calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
|
||||
type DateRangePickerProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
value?: DateRange;
|
||||
defaultValue?: DateRange;
|
||||
disabled?: boolean;
|
||||
handleDateChange?: (date: DateRange | undefined) => void;
|
||||
onValueChange?: (date: DateRange | undefined) => void;
|
||||
};
|
||||
|
||||
export function DateRangePicker({
|
||||
value,
|
||||
defaultValue,
|
||||
disabled,
|
||||
className,
|
||||
handleDateChange,
|
||||
children,
|
||||
onValueChange,
|
||||
}: DateRangePickerProps) {
|
||||
const [date, setDate] = useState<DateRange>();
|
||||
const [dateState, setDateState] = useState<DateRange | undefined>(
|
||||
defaultValue,
|
||||
);
|
||||
|
||||
const isControlled = value !== undefined;
|
||||
const dateValue = isControlled ? value : dateState;
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled) {
|
||||
setDateState(value);
|
||||
}
|
||||
}, [isControlled, value]);
|
||||
|
||||
const handleSelect = (date: DateRange | undefined) => {
|
||||
setDate(date);
|
||||
handleDateChange?.(date);
|
||||
if (!isControlled) {
|
||||
setDateState(date);
|
||||
}
|
||||
onValueChange?.(date);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -36,19 +54,19 @@ export function DateRangePicker({
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-[3.2rem] min-w-[12rem] justify-start border-input !bg-background px-4 py-2 text-left text-[1.3rem] font-normal text-muted-foreground hover:!bg-accent",
|
||||
!date && "text-muted-foreground",
|
||||
!dateValue && "text-muted-foreground",
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<CalendarIcon className="!size-[1.6rem]" />
|
||||
{date?.from ? (
|
||||
date.to ? (
|
||||
{dateValue?.from ? (
|
||||
dateValue.to ? (
|
||||
<>
|
||||
{format(date.from, "yyyy-MM-dd")} ~{" "}
|
||||
{format(date.to, "yyyy-MM-dd")}
|
||||
{format(dateValue.from, "yyyy-MM-dd")} ~{" "}
|
||||
{format(dateValue.to, "yyyy-MM-dd")}
|
||||
</>
|
||||
) : (
|
||||
format(date.from, "yyyy-MM-dd")
|
||||
format(dateValue.from, "yyyy-MM-dd")
|
||||
)
|
||||
) : (
|
||||
<span>기간 선택</span>
|
||||
@@ -56,14 +74,17 @@ export function DateRangePicker({
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={handleSelect}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
<div>
|
||||
{children}
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={dateValue?.from}
|
||||
selected={dateValue}
|
||||
onSelect={handleSelect}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
[data-theme="blue"] {
|
||||
header {
|
||||
background: #00afe5;
|
||||
border: none;
|
||||
}
|
||||
header h1 span {
|
||||
color: #fff;
|
||||
}
|
||||
[data-trigger="dropdown"] {
|
||||
background: transparent !important;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
header {
|
||||
background: #008e8d;
|
||||
border: none;
|
||||
}
|
||||
header h1 span {
|
||||
color: #fff;
|
||||
}
|
||||
[data-trigger="dropdown"] {
|
||||
background: transparent !important;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
import "./header.css";
|
||||
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { images } from "@common/assets";
|
||||
import { getConfig } from "@common/config";
|
||||
import { ENDPOINT } from "../../config";
|
||||
import { getCookie, setCookie } from "../../lib";
|
||||
import { getSiteInfo, getUserInfo } from "../../api";
|
||||
import { setCookie, getCookie } from "../../lib";
|
||||
import {
|
||||
getPopupInfo,
|
||||
getPopupList,
|
||||
getSiteInfo,
|
||||
getUserInfo,
|
||||
type PopupInfo,
|
||||
} from "../../api";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@@ -12,14 +21,12 @@ import {
|
||||
} from "../dropdown-menu";
|
||||
import { Button } from "../button";
|
||||
|
||||
const { AUTH_USER_ID, SITE_ID } = getConfig();
|
||||
const { SITE_ID } = getConfig();
|
||||
|
||||
export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
const userUniqId = getCookie(AUTH_USER_ID ?? "") ?? "";
|
||||
|
||||
const { data: userInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS],
|
||||
queryFn: () => getUserInfo(userUniqId),
|
||||
queryFn: () => getUserInfo(),
|
||||
enabled: !viewOnly,
|
||||
});
|
||||
|
||||
@@ -37,6 +44,51 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
enabled: !viewOnly,
|
||||
});
|
||||
|
||||
const { data: popupList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS],
|
||||
queryFn: () => getPopupList(),
|
||||
});
|
||||
|
||||
const openPopup = useCallback(async (popup: PopupInfo) => {
|
||||
try {
|
||||
const cookieName = `cs-popup-hide-for-day-${popup.popupSn}`;
|
||||
const hasCookie = getCookie(cookieName);
|
||||
if (hasCookie) return;
|
||||
|
||||
const response = await getPopupInfo(popup.popupSn);
|
||||
if (!response) return;
|
||||
|
||||
const width = response.popupWdth || 400;
|
||||
const height = response.popupHgt || 300;
|
||||
const left = (response.popupHrzntltyPstn || 0) + window.screenX;
|
||||
const top = (response.popupVtclPstn || 0) + window.screenY;
|
||||
|
||||
const popupWindow = window.open(
|
||||
`/view/popup/${popup.popupSn}`,
|
||||
"",
|
||||
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,status=no,location=no,menubar=no`,
|
||||
);
|
||||
|
||||
if (popupWindow) {
|
||||
popupWindow.focus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to open popup ${popup.popupSn}:`, error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!popupList?.content.length) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
popupList.content.forEach((popup) => {
|
||||
openPopup(popup);
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [popupList, openPopup]);
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 box-border flex h-[5rem] w-full items-center justify-between border-b border-solid border-border bg-background">
|
||||
<div className="flex items-center">
|
||||
@@ -52,7 +104,10 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label">
|
||||
<Button
|
||||
className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label"
|
||||
data-trigger="dropdown"
|
||||
>
|
||||
<i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" />
|
||||
{userInfo?.userName}
|
||||
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { cn } from "../../lib";
|
||||
import { Button } from "../button";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./accordion";
|
||||
export * from "./badge";
|
||||
export * from "./button";
|
||||
export * from "./button-group";
|
||||
export * from "./calendar";
|
||||
export * from "./carousel";
|
||||
export * from "./checkbox";
|
||||
@@ -19,6 +20,7 @@ export * from "./page-title";
|
||||
export * from "./popover";
|
||||
export * from "./progress";
|
||||
export * from "./radio-group";
|
||||
export * from "./scroll-area";
|
||||
export * from "./select";
|
||||
export * from "./slider";
|
||||
export * from "./skeleton";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import type { MenuItem } from "../../api";
|
||||
import { QueryProvider } from "../../provider/query-provider";
|
||||
import { PopupProvider } from "../../provider/popup-provider";
|
||||
import type { MenuItem } from "../../api";
|
||||
import { SidebarProvider } from "../sidebar";
|
||||
import { LNB } from "../lnb";
|
||||
import { Header } from "../header";
|
||||
import { Footer } from "./footer";
|
||||
import { SidebarProvider } from "../sidebar";
|
||||
import "./common.css";
|
||||
|
||||
export function BaseLayout({
|
||||
@@ -20,20 +21,25 @@ export function BaseLayout({
|
||||
}) {
|
||||
return (
|
||||
<QueryProvider queryClient={queryClient}>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
<div className="flex h-[calc(100vh-50px)]">
|
||||
<LNB currentPath={currentPath} handleMenuClick={handleMenuClick} />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="max-h-[calc(100%-50px)] flex-1 overflow-auto">
|
||||
{children}
|
||||
<PopupProvider>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
<div className="flex h-[calc(100vh-50px)]">
|
||||
<LNB
|
||||
currentPath={currentPath}
|
||||
handleMenuClick={handleMenuClick}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="max-h-[calc(100%-50px)] flex-1 overflow-auto">
|
||||
{children}
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</SidebarProvider>
|
||||
</PopupProvider>
|
||||
</QueryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,3 +206,21 @@
|
||||
color: #ffffff;
|
||||
background: #40a5ff;
|
||||
}
|
||||
|
||||
[data-theme="blue"] {
|
||||
footer {
|
||||
background: #6ec05d;
|
||||
}
|
||||
footer p {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
footer {
|
||||
background: #24ec7d;
|
||||
}
|
||||
footer p {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
@@ -63,6 +65,7 @@ export function LNB({
|
||||
"h-auto py-4 text-[1.4rem] text-label transition-colors hover:bg-accent",
|
||||
isMatched && "bg-accent !text-primary",
|
||||
)}
|
||||
data-matched={isMatched}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<i className={cn("diveicon ml-2", `di-${item.iconNm}`)} />
|
||||
@@ -94,19 +97,21 @@ export function LNB({
|
||||
<Collapsible open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="group flex items-center justify-between rounded-sm py-4 pr-2 hover:bg-accent">
|
||||
<SidebarMenuButton className="group/menu-button text-[1.4rem] group-data-[state=open]:bg-transparent">
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon ml-2 text-label",
|
||||
`di-${item.iconNm}`,
|
||||
)}
|
||||
/>
|
||||
<span className="text-label group-hover/menu-button:text-foreground">
|
||||
{item.menuKornNm}
|
||||
</span>
|
||||
<div className="group">
|
||||
<SidebarMenuButton className="group/menu-button flex h-auto items-center justify-between py-4 pr-2 text-[1.4rem] group-data-[state=open]:bg-transparent">
|
||||
<div className="flex items-center gap-2">
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon ml-2 text-label",
|
||||
`di-${item.iconNm}`,
|
||||
)}
|
||||
/>
|
||||
<span className="text-label group-hover/menu-button:text-foreground">
|
||||
{item.menuKornNm}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight className="!size-6 text-label group-data-[state=open]:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
<ChevronRight className="size-6 text-label group-data-[state=open]:rotate-90" />
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./sidebar.css";
|
||||
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
@@ -248,6 +250,7 @@ const Sidebar = React.forwardRef<
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-sidebar="sidebar-wrapper"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
[data-theme="blue"] {
|
||||
[data-sidebar="sidebar-wrapper"] {
|
||||
background: none;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
#00afe5 0%,
|
||||
#48bfad 65%,
|
||||
#6ec05d 100%
|
||||
) !important;
|
||||
border: none;
|
||||
}
|
||||
[data-sidebar="sidebar"],
|
||||
[data-sidebar="content"] {
|
||||
background: transparent !important;
|
||||
}
|
||||
[data-sidebar="menu-item"] * {
|
||||
color: #fff !important;
|
||||
}
|
||||
[data-sidebar="menu-item"] *:hover {
|
||||
background: none !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
[data-sidebar="menu-button"]:hover {
|
||||
background: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
[data-sidebar="menu-button"][data-matched="true"] {
|
||||
background: rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="red"] {
|
||||
[data-sidebar="menu-button"][data-matched="true"] {
|
||||
@apply bg-primary !text-white;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
[data-sidebar="sidebar-wrapper"] {
|
||||
background: none;
|
||||
background-image: linear-gradient(
|
||||
0.4turn,
|
||||
#008e8d 20%,
|
||||
#24ec7d 100%
|
||||
) !important;
|
||||
border: none;
|
||||
}
|
||||
[data-sidebar="sidebar"],
|
||||
[data-sidebar="content"] {
|
||||
background: transparent !important;
|
||||
}
|
||||
[data-sidebar="menu-item"] * {
|
||||
color: #fff !important;
|
||||
}
|
||||
[data-sidebar="menu-item"] *:hover {
|
||||
background: none !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
[data-sidebar="menu-button"]:hover {
|
||||
background: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
[data-sidebar="menu-button"][data-matched="true"] {
|
||||
background: rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,4 +47,22 @@
|
||||
@apply text-[#fff];
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="red"] {
|
||||
.table-ui.default th {
|
||||
@apply bg-[#8d8d94];
|
||||
}
|
||||
.table-ui.secondary th {
|
||||
@apply bg-[#f6f6f9] text-[#808080];
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
.table-ui.default th {
|
||||
@apply bg-[#6bafae];
|
||||
}
|
||||
.table-ui.secondary th {
|
||||
@apply bg-[#ebf7f7] text-[#64908f];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[6rem] w-full rounded-md border border-input bg-transparent p-3 text-base placeholder:text-placeholder focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"flex min-h-[6rem] w-full rounded-md border border-input bg-transparent p-3 text-base text-foreground placeholder:text-placeholder focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@common/config",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./initialize-config";
|
||||
export * from "./initializer";
|
||||
|
||||