ranCardGroupDetailModal.vue 17 KB

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