ranMapGroupDetailModal.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. <template>
  2. <!-- 지도형 3Depth 정보 팝업 : S -->
  3. <v-dialog
  4. v-model="isModal"
  5. persistent
  6. width="62.5rem"
  7. >
  8. <div class="v-common-dialog-wrapper custom-dialog alert">
  9. <div class="modal-tit">
  10. <strong>{{neGroupInfo.neGroup}}</strong>
  11. <button class="btn-close" @click="fnClose"></button>
  12. </div>
  13. <div class="v-common-dialog-content pa-0">
  14. <div class="core--list--component">
  15. <div class="map--area big--map">
  16. <!-- <div class="area--info" style="top: 0.94rem; right: 0.94rem; z-index:10;">
  17. <div class="area--info--title">
  18. <p>NE001</p>
  19. <button class="btn-close"></button>
  20. </div>
  21. <ul>
  22. <li><i class="ico green"></i><span>STATUS</span><span class="active">ACTIVE</span></li>
  23. <li><i class="ico red"></i><span>CPU</span><span class="">60.00%</span></li>
  24. <li><i class="ico green"></i><span>MEMORY</span><span class="">80%</span></li>
  25. <li><i class="ico green"></i><span>DISK</span><span class="">20%</span></li>
  26. </ul>
  27. </div> -->
  28. <!--맵 영역-->
  29. <div id="ran_detail_map" style="height: 100%;"></div>
  30. </div>
  31. </div>
  32. <!-- NE 목록 grid : S -->
  33. <div class="core--list--component--grid mt--0" >
  34. <div class="title">
  35. <h2>
  36. NE 목록 <span>({{getNeGroup.length}}건)</span>
  37. </h2>
  38. </div>
  39. <div class="tbl-wrapper">
  40. <div class="tbl-wrap">
  41. <!-- ag grid -->
  42. <ag-grid-vue
  43. style="width:100%; height:22vh;"
  44. class="ag-theme-quartz"
  45. :gridOptions="gridOptions"
  46. @grid-ready="fnOnGridReady"
  47. >
  48. </ag-grid-vue>
  49. </div>
  50. </div>
  51. </div>
  52. <!-- NE 목록 grid : E -->
  53. </div>
  54. <div class="btn-wrap nw--btn--wrap" style="padding-top:1.88rem">
  55. <div></div>
  56. <div class="inner--btn--wrap">
  57. <v-btn
  58. class="custom-btn btn-gray mini"
  59. @click="fnClose"
  60. >
  61. <i class="ico"></i>
  62. 닫기
  63. </v-btn>
  64. </div>
  65. </div>
  66. </div>
  67. </v-dialog>
  68. <!-- 지도형 3Depth 정보 팝업 : E -->
  69. </template>
  70. <script setup>
  71. /***********************
  72. * import
  73. ************************/
  74. import "ag-grid-community/styles/ag-grid.css";
  75. import "ag-grid-community/styles/ag-theme-quartz.css";
  76. import { AgGridVue } from "ag-grid-vue3";
  77. import { useI18n } from "vue-i18n";
  78. import useUtil from "@/composables/useUtil";
  79. /***********************
  80. * plugins inject
  81. ************************/
  82. const { $toast, $log, $dayjs, $eventBus } = useNuxtApp()
  83. // props
  84. const props = defineProps({
  85. neGroupInfo: Object,
  86. centerPosition: Object
  87. })
  88. // 참조가능 데이터 설정
  89. defineExpose({})
  90. // 발신 이벤트 선언
  91. const emit = defineEmits(["closeModal"])
  92. const i18n = useI18n()
  93. /***********************
  94. * data & created
  95. ************************/
  96. const isModal = ref(true)
  97. const map = ref(null) // 카카오 맵 객체
  98. const marker = ref(null) // 카카오맵 마커(핀) 객체
  99. const remToPx = () => parseFloat(getComputedStyle(document.documentElement).fontSize)
  100. const rowHeightRem = 2.5 // 원하는 rem 값
  101. const rowHeightPx = rowHeightRem * remToPx()
  102. const gridApi = shallowRef()
  103. const getNeGroup = computed(() => {
  104. let dataObj = _cloneDeep(props.neGroupInfo)
  105. let result = []
  106. dataObj.neList.forEach((ne, idx) => {
  107. let tempObj = ne
  108. tempObj.no = idx+1,
  109. tempObj.color = getNeEventCls.value(ne),
  110. tempObj.eventStatus= [ne.criCnt, ne.majCnt, ne.minCnt],
  111. result.push(tempObj)
  112. })
  113. return result
  114. })
  115. // NE 그룹 수 > 이벤트 단계 클래스
  116. const getNeEventCls = computed(() => {
  117. return (obj) => {
  118. let eventCls = 'gray'
  119. if(!_isEmpty(obj)) {
  120. if(obj.minCnt > 0) eventCls = 'black'
  121. if(obj.majCnt > 0) eventCls = 'blue'
  122. if(obj.criCnt > 0) eventCls = 'red'
  123. }
  124. return eventCls
  125. }
  126. })
  127. const gridOptions = {
  128. defaultColDef: {
  129. lockVisible: true,
  130. },
  131. columnDefs: [
  132. {
  133. headerName: 'No',
  134. field: 'no',
  135. width: 50
  136. },
  137. {
  138. headerName: 'NE 이름',
  139. field: 'neName',
  140. cellRenderer: actionCellRenderer2,
  141. },
  142. {
  143. headerName: '테넌트',
  144. field: 'tenantName',
  145. },
  146. {
  147. headerName: '이벤트',
  148. field: 'eventStatus',
  149. cellRenderer: actionCellRenderer3,
  150. width: 250,
  151. }
  152. ],
  153. rowData: getNeGroup.value, // 테이블 데이터
  154. autoSizeStrategy: {
  155. type: "fitGridWidth", //fitCellContents
  156. },
  157. headerHeight : rowHeightPx,
  158. rowHeight: rowHeightPx,
  159. pagination: true,
  160. suppressPaginationPanel: true, // 하단 default 페이징 컨트롤 숨김
  161. suppressRowClickSelection: true, // 행 클릭 체크박스 무시
  162. localeText: {
  163. noRowsToShow: i18n.t('common.noData')
  164. },
  165. }
  166. const markers = ref([])
  167. onMounted(() => {
  168. fnInit()
  169. })
  170. /***********************
  171. * Methods
  172. ************************/
  173. /**
  174. * RAN NE 그룹 상세 팝업
  175. */
  176. function fnGetNeGroupDetailInfo(){
  177. useAxios().post('/dashboard/geoNeGroupInfo/list.do').then((res) => {
  178. $log.debug("[dashboard][fnGetNeGroupDetailInfo][success]")
  179. }).catch((error)=>{
  180. $log.debug("[dashboard][fnGetNeGroupDetailInfo][error]")
  181. useErrorHandler().fnSetCommErrorHandle(error, fnGetNeGroupDetailInfo)
  182. }).finally(()=>{
  183. $log.debug("[dashboard][fnGetNeGroupDetailInfo][finished]")
  184. })
  185. }
  186. /**
  187. * 초기 실행
  188. */
  189. function fnInit(){
  190. fnGetNeGroupDetailInfo()
  191. nextTick().then(() => {
  192. if (window.kakao && window.kakao.maps) {
  193. loadMap()
  194. } else {
  195. loadScript()
  196. }
  197. })
  198. }
  199. /**
  200. * kakao 스크립트 로드
  201. */
  202. function loadScript() {
  203. const script = document.createElement('script')
  204. script.async = true
  205. script.onload = () => {
  206. window.kakao.maps.load(loadMap)
  207. }
  208. script.src = `//dapi.kakao.com/v2/maps/sdk.js?autoload=false&libraries=services,clusterer,drawing&appkey=${import.meta.env.VITE_APP_KAKAO_APP_KEY}`
  209. document.head.appendChild(script)
  210. }
  211. /**
  212. * kakao 지도 로드
  213. */
  214. async function loadMap() {
  215. const mapContainer = document.getElementById('ran_detail_map')
  216. let mapOption = {
  217. center: new kakao.maps.LatLng(props.centerPosition.lat, props.centerPosition.lng), // 지도의 중심좌표
  218. level: 3, // 지도의 확대 레벨
  219. }
  220. map.value = new kakao.maps.Map(mapContainer, mapOption)
  221. let zoomControl = new kakao.maps.ZoomControl()
  222. // map.value.addControl(zoomControl, kakao.maps.ControlPosition.BOTTOMRIGHT)
  223. // fnSetEventListener() // 지도 이벤트 등록
  224. fnDrawMarker()
  225. }
  226. /**
  227. * 상세팝업 닫기
  228. */
  229. function fnClose(){
  230. isModal.value = false
  231. setTimeout(() => {
  232. emit('closeModal')
  233. }, 250);
  234. }
  235. function fnGetNeList(){
  236. //
  237. }
  238. /**
  239. * 마커 생성 데이터 세팅
  240. */
  241. function fnDrawMarker(){
  242. getNeGroup.value.map((item, index) => {
  243. let position = new kakao.maps.LatLng(item.neLocLatitude, item.neLocLongitude)
  244. let markerObj = {
  245. overlay: null,
  246. markers: null,
  247. data: item,
  248. click: false,
  249. position: position
  250. }
  251. markers.value.push(markerObj)
  252. markers.value[index].markers = fnCreateMarker(position, index)
  253. if(index == 0) {
  254. map.value.panTo(position)
  255. }
  256. })
  257. }
  258. import { filename } from 'pathe/utils'
  259. // 이미지 가져오기 (vite문법)
  260. const glob = import.meta.glob('~/assets/img/ico_*.{png,svg}', { eager: true })
  261. const getImages = Object.fromEntries(
  262. Object.entries(glob).map(([key, value]) => [filename(key), value.default])
  263. )
  264. function fnGetMarkerImage(index){
  265. let color = getNeGroup.value[index].color
  266. if(color == 'red') return 'ico_red_pin'
  267. else if(color == 'blue') return 'ico_blue_pin'
  268. else if(color == 'black') return 'ico_black_pin'
  269. else return 'ico_gray_pin'
  270. }
  271. /**
  272. * 마커 생성
  273. */
  274. function fnCreateMarker(position, index) {
  275. // 마커 이미지 설정
  276. const markerImageSrc = getImages[fnGetMarkerImage(index)] // assets 폴더의 이미지 경로
  277. const markerImageSize = new kakao.maps.Size(40, 40); // 마커 이미지 사이즈
  278. const markerImage = new kakao.maps.MarkerImage(markerImageSrc, markerImageSize);
  279. let markerOption = {
  280. map: map.value, // 마커를 표시할 지도
  281. position: position, // 마커의 위치
  282. clickable: true,
  283. image: markerImage,
  284. zIndex: 1,
  285. }
  286. let marker = new kakao.maps.Marker(markerOption)
  287. // 마커 클릭 이벤트등록
  288. kakao.maps.event.addListener(marker,'click', fnClickMarker(index))
  289. return marker
  290. }
  291. /**
  292. * 마커 클릭
  293. */
  294. function fnClickMarker(index) {
  295. const clickData = markers.value[index].data
  296. const openInfoWindow = function () {
  297. // 마커 클릭 > 상태변경 > z-index 및 맵 이동
  298. fnMarkerClickChk(index)
  299. fnGetInfo(clickData, index)
  300. }
  301. return openInfoWindow
  302. }
  303. /**
  304. * 마커 클릭 이벤트
  305. */
  306. function fnMarkerClickChk(index) {
  307. fnSetUnClickMarker()
  308. markers.value[index].click = true
  309. markers.value[index].markers.setZIndex(3)
  310. let moveLatLng = markers.value[index].position
  311. map.value.panTo(moveLatLng)
  312. }
  313. /**
  314. * 마커 클릭 상태 해제 및 z인덱스 초기화
  315. */
  316. function fnSetUnClickMarker() {
  317. markers.value.forEach((item) => {
  318. item.click = false
  319. item.markers.setZIndex(2)
  320. })
  321. }
  322. function fnGetInfo(data, index){
  323. // 열려 있던 오버레이 닫기
  324. fnCloseMarkerInfoOverlay()
  325. // 오버레이 생성
  326. markers.value[index].overlay = fnCreateCustomOverlay(data, index)
  327. // 오버레이 z-index 설정
  328. markers.value[index].overlay.setZIndex(999)
  329. // 오버레이 열기
  330. markers.value[index].overlay.setMap(map.value)
  331. // 오버레이 이벤트 등록
  332. fnCustomOverlayAddEventListener()
  333. }
  334. /**
  335. * 커스텀 오버레이 생성
  336. */
  337. function fnCreateCustomOverlay(data, index) {
  338. let cpu = JSON.parse(data.cpu)
  339. let memory = JSON.parse(data.mem)
  340. let content = `
  341. <div class="area--info" style="top: -10.7rem; right: -6rem;">
  342. <div class="area--info--title">
  343. <p style="overflow:hidden; text-overflow: ellipsis;">${data.neName}</p>
  344. <button class="btn-close" id="overlayCloseBtn"></button>
  345. </div>
  346. <ul>
  347. <li><i class="ico green"></i><span>STATUS</span><span class="active">ACTIVE</span></li>
  348. <li><i class="ico red"></i><span>CPU</span><span class="">${cpu.AVG_CPU_L}%</span></li>
  349. <li><i class="ico green"></i><span>MEMORY</span><span class="">${memory.AVG_MEM_L}%</span></li>
  350. <li><i class="ico green"></i><span>DISK</span><span class="">${data.disk}</span></li>
  351. </ul>
  352. </div>`
  353. // 커스텀 오버레이 생성
  354. let customOverlay = new kakao.maps.CustomOverlay({
  355. position: markers.value[index].position,
  356. content: content
  357. })
  358. return customOverlay
  359. }
  360. /**
  361. * 커스텀 오버레이 이벤트 등록
  362. */
  363. function fnCustomOverlayAddEventListener(){
  364. // 오버레이 닫기 버튼
  365. const closeBtn = document.querySelector('#overlayCloseBtn')
  366. closeBtn.addEventListener('click', fnCloseBtnOverlay)
  367. }
  368. /**
  369. * 마커 정보 오버레이 닫기 버튼
  370. */
  371. function fnCloseBtnOverlay() {
  372. fnCloseMarkerInfoOverlay()
  373. fnSetUnClickMarker()
  374. }
  375. /**
  376. * 모든 마커 정보 오버레이 닫기
  377. */
  378. function fnCloseMarkerInfoOverlay() {
  379. for (var i = 0; i < markers.value.length; i++) {
  380. if (markers.value[i].overlay) {
  381. markers.value[i].overlay.setMap(null)
  382. markers.value[i].overlay = null
  383. }
  384. }
  385. }
  386. /*********************
  387. * grid 관련
  388. *********************/
  389. // Grid 데이터 바인딩
  390. function fnOnGridReady(params){
  391. gridApi.value = params.api
  392. }
  393. function actionCellRenderer2(params) {
  394. let _cls = "";
  395. if(params.data.color == 'red'){
  396. _cls = 'pin--red';
  397. } else if(params.data.color == 'blue'){
  398. _cls = 'pin--blue';
  399. } else if(params.data.color == 'gray'){
  400. _cls = 'pin--gray';
  401. } else if(params.data.color == 'black'){
  402. _cls = 'pin--black';
  403. }
  404. let pin = `<span class="${_cls}">${params.data.neName}</span>`;
  405. return pin;
  406. }
  407. function actionCellRenderer3(params) {
  408. let _cls0 = "";
  409. let _cls1 = "";
  410. let _cls2 = "";
  411. let _cls3 = "";
  412. if(params.data.eventStatus[0]){_cls0 = 'evt--critical';}
  413. if(params.data.eventStatus[1]){_cls1 = 'evt--major';}
  414. if(params.data.eventStatus[2]){_cls2 = 'evt--minor';}
  415. let pin = "";
  416. if (_cls0) {
  417. pin += `<span class="${_cls0}">CRITICAL (${params.data.eventStatus[0]}) </span>`;
  418. }
  419. if (_cls1) {
  420. pin += `<span class="${_cls1}">MAJOR (${params.data.eventStatus[1]}) </span>`;
  421. }
  422. if (_cls2) {
  423. pin += `<span class="${_cls2}">MINOR (${params.data.eventStatus[2]}) </span>`;
  424. }
  425. if(pin == ""){
  426. pin += `<span class="evt--none">-</span>`;
  427. }
  428. return pin;
  429. }
  430. </script>