| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687 |
- <template>
- <v-dialog
- v-model="isModal"
- persistent
- width="62.5rem"
- >
- <div class="v-common-dialog-wrapper custom-dialog alert">
- <div class="modal-tit">
- <strong>장비 현황</strong>
- <button
- class="btn-close"
- @click="fnClose"
- />
- </div>
-
- <div class="v-common-dialog-content pa-0">
- <div class="core--list--component">
- <h2 class="fw--500">
- 이벤트 현황
- </h2>
- <ul class="event--stat">
- <li class="critical">
- <i class="ico" /><span>CRITICAL</span><span>{{ neInfoItem.criCnt || 0 }}</span>
- </li>
- <li class="major">
- <i class="ico" /><span>MAJOR</span><span>{{ neInfoItem.majCnt || 0 }}</span>
- </li>
- <li class="minor">
- <i class="ico" /><span>MINOR</span><span>{{ neInfoItem.minCnt || 0 }}</span>
- </li>
- <li class="disconnected">
- <i class="ico" /><span>DISCONNECTED</span><span>0</span>
- </li>
- </ul>
- </div>
- <div class="core--list--component--grid mt--0">
- <div class="title">
- <h2>
- NE 그룹 목록 <span>({{ neGroupObj.count }}건)</span>
- </h2>
- </div>
- <div class="tbl-wrapper">
- <div class="tbl-wrap">
- <!-- ag grid -->
- <ag-grid-vue
- style="width:100%; height:calc(23vh);"
- class="ag-theme-quartz"
- :row-data="neGroupObj.list"
- :grid-options="neGroupGirdOptions"
- @grid-ready="fnOnNeGroupGirdReady"
- @row-clicked="fnGroupRowClick"
- />
- </div>
- </div>
- </div>
- <div class="core--list--component">
- <div class="map--area">
- <div
- v-if="neObj.neGroup"
- class="side--title"
- style="zIndex:10"
- >
- {{ neObj.neGroup }}
- </div>
- <!--맵 영역-->
- <div
- id="ran_detail_map"
- ref="refMap"
- style="height: 100%;"
- />
- </div>
- </div>
-
- <div class="core--list--component--grid mt--0">
- <div class="title">
- <h2 v-if="neObj.neGroup && neObj.count > 0">
- [{{ neObj.neGroup }}] NE 목록 <span>({{ neObj.count }}건)</span>
- </h2>
- <h2 v-else>
- NE 목록 <span>(0건)</span>
- </h2>
- </div>
- <div class="tbl-wrapper">
- <div class="tbl-wrap">
- <ag-grid-vue
- style="width:100%; height:22vh;"
- class="ag-theme-quartz"
- :grid-options="neGirdOptions"
- @grid-ready="fnOnNeGirdReady"
- />
- <!-- :row-data="neObj.neList" -->
- </div>
- </div>
- </div>
- </div>
- <div
- class="btn-wrap nw--btn--wrap"
- style="padding-top:1.88rem"
- >
- <div />
- <div class="inner--btn--wrap">
- <v-btn
- class="custom-btn btn-gray mini"
- @click="fnClose"
- >
- <i class="ico" />
- 닫기
- </v-btn>
- </div>
- </div>
- </div>
- </v-dialog>
- </template>
- <script setup>
- /***********************
- * import
- ************************/
- import "ag-grid-community/styles/ag-grid.css";
- import "ag-grid-community/styles/ag-theme-quartz.css";
- import { AgGridVue } from "ag-grid-vue3";
- import { useI18n } from "vue-i18n";
- import useUtil from "@/composables/useUtil";
- import { filename } from 'pathe/utils'
- import testJson from "@/components/home/dashboard/test.json"
- /***********************
- * plugins inject
- ************************/
- const { $toast, $log, $dayjs, $eventBus } = useNuxtApp()
- // props
- const props = defineProps({
- tenantName: {
- type: String,
- required: true
- },
- })
- // 참조가능 데이터 설정
- defineExpose({})
- // 발신 이벤트 선언
- const emit = defineEmits(["closeModal"])
- const i18n = useI18n()
- /***********************
- * data & created
- ************************/
- const isModal = ref(true)
- const refMap = ref();
- const map = ref(null) // 카카오 맵 객체
- const markers = ref([]) // 카카오맵 마커(핀) 객체
- // 카카오맵 센터좌표
- const centerPosition = ref({
- lat: 33.450701,
- lng: 126.570667
- });
- const remToPx = () => parseFloat(getComputedStyle(document.documentElement).fontSize)
- const rowHeightRem = 2.5 // 원하는 rem 값
- const rowHeightPx = rowHeightRem * remToPx()
- const neGroupGridApi = shallowRef()
- const neGridApi = shallowRef()
- // Ran DetailInfo
- const neInfoItem = ref({});
-
- // neGroupObj
- const neGroupObj = ref({
- count: 0,
- list: []
- });
- // neObj
- const neObj = ref({
- count: 0,
- neGroup: '',
- list: []
- })
-
- // ne 그룹 목록 그리드 옵션
- const neGroupGirdOptions = {
- columnDefs: [
- { headerName: 'No', valueGetter: "node.rowIndex + 1", sortable: false, maxWidth: 100},
- { headerName: 'NE 그룹명', field: 'neGroup', sortable: false, minWidth: 300},
- { headerName: 'NE 수', field: 'neCnt', sortable: false,},
- { headerName: '알림', field: 'cls', sortable: false,cellRenderer: alarmCellRenderer}
- ],
- rowData: neGroupObj.value.list, // 테이블 데이터
- suppressMovableColumns: true,
- autoSizeStrategy: {
- type: "fitGridWidth", // width맞춤
- },
- headerHeight : rowHeightPx,
- rowHeight: rowHeightPx,
- pagination: true,
- suppressPaginationPanel: true, // 하단 default 페이징 컨트롤 숨김
- suppressRowClickSelection: true, // 행 클릭 체크박스 무시
- localeText: {
- noRowsToShow: i18n.t('common.noData')
- },
- }
- // ne 목록 그리드 옵션
- const neGirdOptions = {
- defaultColDef: {
- lockVisible: true,
- },
- columnDefs: [
- { headerName: 'No', valueGetter: "node.rowIndex + 1", sortable: false, maxWidth: 100},
- { headerName: 'NE 이름', field: 'neName', sortable: false, cellRenderer: neNameCellRenderer,},
- { headerName: '이벤트', field: 'eventStatus', sortable: false, cellRenderer: eventCellRenderer, width: 250,}
- ],
- rowData: neObj.value.neList, // 테이블 데이터
- autoSizeStrategy: {
- type: "fitGridWidth", //fitCellContents
- },
- headerHeight : rowHeightPx,
- rowHeight: rowHeightPx,
- pagination: true,
- suppressPaginationPanel: true, // 하단 default 페이징 컨트롤 숨김
- suppressRowClickSelection: true, // 행 클릭 체크박스 무시
- localeText: {
- noRowsToShow: 'NE 그룹을 선택하세요'
- },
- loading: false
- }
- /***********************
- * Methods
- ************************/
- const fnInit = async () => {
- await nextTick();
- // Ran 정보 조회
- await fnGeoTenantNeInfo();
- // 카카오 지도 load
- if (window.kakao && window.kakao.maps) {
- fnLoadKakakoMap()
- } else {
- fnAppendKakaoScript()
- }
- }
- function fnClose(){
- isModal.value = false
- setTimeout(() => {
- emit('closeModal')
- }, 250);
- }
- /**
- * P5G RAN Ne 정보 조회
- */
- function fnGeoTenantNeInfo() {
- return new Promise((resolve, reject) => {
- const params = {
- tenantName: props.tenantName
- }
- useAxios().post(useApi.tenantNeInfo, params).then((res) => {
- const {resCode, resMsg, data} = res.data;
- if(resCode == 200) {
- // 테스트를 위한 테스트 코드 (삭제 필수)
- // fnParseData([...(data?.items || []), ...testJson.ranNeGroupList.data.items])
- fnParseData(data.items || [])
- $log.debug("[tenantDashboard][fnGeoTenantNeInfo][success]")
- resolve();
- } else {
- $log.debug("[tenantDashboard][fnGeoTenantNeInfo][error]", `[${resCode}] ${resMsg}`);
- reject()
- }
- }).catch((error)=>{
- // 테스트를 위한 테스트 코드 (삭제 필수)
- // fnParseData([...testJson.ranNeGroupList.data.items])
- $log.debug("[tenantDashboard][fnGeoTenantNeInfo][error]")
- useErrorHandler().fnSetCommErrorHandle(error, fnGeoTenantNeInfo)
- reject()
- }).finally(()=>{
- $log.debug("[tenantDashboard][fnGeoTenantNeInfo][finished]")
- })
- })
- }
- const fnParseData = (items = []) => {
- // main > criCnt, majCnt, minCnt, neCnt, neGroupCnt, neGroupList
- // neGroupList > criCnt, majCnt, minCnt, neCnt, neGroup, tenantName, neList
- // neList > 모든 데이터
- // "tenantName": "SAMSUNGSDS",
- // "neGroup": "tenant01Group01",
- // "neName": "tenant01Group01ne02",
- // "neId": "CPC_100",
- // "neType": "UPF",
- // "upfNum": 10,
- // "customerType": 1,
- // "lastUpdateTime": "2024-09-10 00:00:00",
- // "neAddress": "서울시",
- // "neLocLatitude": "37.55203063",
- // "neLocLongitude": "126.98081697",
- // "familyName": "PUD",
- // "initTime": "2024-09-01 18:15:00",
- // "minCnt": 0,
- // "majCnt": 0,
- // "criCnt": 0,
- // "kpi": "{\"UL_RX_AVG_KBPS\":\"0.00\",\"UL_TX_AVG_KBPS\":\"0.00\",\"DL_RX_AVG_KBPS\":\"0.00\",\"DL_TX_AVG_KBPS\":\"0.00\"}",
- // "cpu": "{\"AVG_CPU_L\":\"10\",\"PEAK_CPU_L\":\"11\"}",
- // "mem": "{\"AVG_MEM_L\":\"17\",\"PEAK_MEM_L\":\"17\"}",
- // "areaCode": 11
- neInfoItem.value = {};
- if(!items || items.length < 1) {
- return;
- }
- // 테넌트 필터링
- const tenantNeItems = items.filter((i) => i.tenantName == props.tenantName);
- // const tenantNeItems = items;
- // 데이터 그룹핑
- let objInfo = {};
- objInfo = tenantNeItems.reduce((acc, curr) => {
- // 심각도 카운트 셋팅
- if(acc.criCnt) acc.criCnt = acc.criCnt + curr.criCnt;
- else acc.criCnt = curr.criCnt;
- if(acc.majCnt) acc.majCnt = acc.majCnt + curr.majCnt;
- else acc.majCnt = curr.majCnt;
- if(acc.minCnt) acc.minCnt = acc.minCnt + curr.minCnt;
- else acc.minCnt = curr.minCnt;
- if(!acc.neGroupList) acc.neGroupList = [];
- // ne 카운트 셋팅
- if(acc.neCnt) acc.neCnt = acc.neCnt + 1;
- else acc.neCnt = 1;
- // neGroupList 셋팅
- const groupIndex = acc.neGroupList.findIndex((g) => g.neGroup == curr.neGroup);
- if(groupIndex > -1) {
- acc.neGroupList[groupIndex].neCnt = acc.neGroupList[groupIndex].neCnt + 1;
- acc.neGroupList[groupIndex].criCnt = acc.neGroupList[groupIndex].criCnt + curr.criCnt;
- acc.neGroupList[groupIndex].majCnt = acc.neGroupList[groupIndex].majCnt + curr.majCnt;
- acc.neGroupList[groupIndex].minCnt = acc.neGroupList[groupIndex].minCnt + curr.minCnt;
- acc.neGroupList[groupIndex].neList.push(curr);
- } else {
- acc.neGroupList.push({
- tenantName: curr.tenantName,
- neGroup: curr.neGroup,
- criCnt: curr.criCnt,
- majCnt: curr.majCnt,
- minCnt: curr.minCnt,
- neCnt: 1,
- neList: [curr]
- })
- }
- //neGroup 카운트 셋팅
- if(acc.neGroupCnt) acc.neGroupCnt = acc.neGroupList?.length;
- else acc.neGroupCnt = 1;
- return acc;
- }, {})
- // 전체 목록 셋팅
- neInfoItem.value = objInfo;
- // NE 그룹 목록 셋팅
- neGroupObj.value.count = objInfo.neGroupList?.length;
- neGroupObj.value.list = objInfo.neGroupList;
- console.log('::::: neGroup 정보 셋팅 완료 :::::', neInfoItem.value)
- }
- /**
- * 카카오 지도 관련
- */
- // 카카오 지도 script 추가
- const fnAppendKakaoScript = () => {
- const script = document.createElement('script')
- script.async = true
- script.onload = () => {
- window.kakao.maps.load(fnLoadKakakoMap)
- }
- script.src = `//dapi.kakao.com/v2/maps/sdk.js?autoload=false&libraries=services,clusterer,drawing&appkey=${import.meta.env.VITE_APP_KAKAO_APP_KEY}`
- document.head.appendChild(script)
- }
- // 카카오 지도 Load
- const fnLoadKakakoMap = () => {
- const mapContainer = refMap.value;
- const mapOption = {
- center: new kakao.maps.LatLng(centerPosition.value.lat, centerPosition.value.lng), // 지도의 중심좌표
- level: 12, // 지도의 확대 레벨
- }
- map.value = new kakao.maps.Map(mapContainer, mapOption)
- }
- /** 마커 관련 */
- // 마커 이미지
- // 이미지 가져오기 (vite문법)
- const glob = import.meta.glob('~/assets/img/ico_*.{png,svg}', { eager: true })
- const getImages = Object.fromEntries(
- Object.entries(glob).map(([key, value]) => [filename(key), value.default])
- )
- const fnGetNePinIco = (item) => {
- const { criCnt, majCnt, minCnt } = item;
- if(criCnt && criCnt > 0) return 'red'
- else if(majCnt && majCnt > 0) return 'blue'
- else if(minCnt && minCnt > 0) return 'black'
- else return 'gray'
- }
-
- // 마커 생성
- const fnCreateMaker = (item, position, index) => {
- const markerImageSrc = getImages[`ico_${fnGetNePinIco(item)}_pin`]; // assets 폴더의 이미지 경로
- const markerImageSize = new kakao.maps.Size(40, 40); // 마커 이미지 사이즈
- const markerImage = new kakao.maps.MarkerImage(markerImageSrc, markerImageSize);
- const markerOption = {
- map: map.value, // 마커를 표시할 지도
- position: position, // 마커의 위치
- clickable: true,
- image: markerImage,
- zIndex: 1,
- }
- return new kakao.maps.Marker(markerOption);
- }
- // 마커 그리기
- const fnDrawMaker = () => {
- // 마커 초기화
- fnClearMarker();
- // 마커 생성
- neObj.value.neList.forEach((item, index) => {
- const position = new kakao.maps.LatLng(item.neLocLatitude, item.neLocLongitude)
- const marker = fnCreateMaker(item, position, index);
- markers.value.push({
- overlay: null,
- markers: marker,
- data: item,
- click: false,
- position: position
- })
- // 마커 클릭 이벤트등록
- kakao.maps.event.addListener(marker, 'click', fnClickMarkerEvent(index))
- // 첫번째 마커로 이동
- if(index == 0) map.value.panTo(position)
- })
- }
- // 마커 초기화
- const fnClearMarker = () => {
- markers.value.forEach(marker => marker.markers.setMap(null));
- fnClearMarkerOverlay()
- markers.value = []
- }
- // 마커 click 이벤트 등록
- function fnClickMarkerEvent(index) {
- const marker = markers.value[index];
- const openOverlay = () => {
- fnClickMaker(marker)
- fnOpenOverlay(marker)
- }
- return openOverlay;
- }
- // 실제 마커 클릭시 동작하는 함수
- const fnClickMaker = (marker) => {
- fnClearMakerClick(); // 마커 클릭 초기화
- marker.click = true;
- marker.markers.setZIndex(3)
- map.value.panTo(marker.position);
- }
- // 마커 Click 초기화
- const fnClearMakerClick = () => {
- markers.value.forEach((item) => {
- item.click = false;
- item.markers.setZIndex(2);
- })
- }
- /** 오버레이 윈도우 관련 */
- // 오버레이 open
- const fnOpenOverlay = (marker) => {
- // 모든 오버레이 초기화
- fnClearMarkerOverlay();
- // 오버레이 생성
- marker.overlay = fnCreateCustomOverlay(marker);
- // 오버레이 zindex 설정
- marker.overlay.setZIndex(999);
- // 오버레이 지도에 적용
- marker.overlay.setMap(map.value);
- // 오버레이 닫기 버튼
- const closeBtn = document.querySelector('#overlayCloseBtn')
- closeBtn.addEventListener('click', fnCloseOverlay)
- }
- // 오버레이 생성
- const fnCreateCustomOverlay = (marker) => {
- //kpi 정보 element 생성
- const kpiEls = (fnGetKPIInfo(marker?.data) || {}).map((kpi)=> {
- return `<li><i class="ico ${kpi.ico}"></i><span>${kpi.label}</span><span class="">${parseFloat(kpi.val|| 0).toFixed(2)}%</span></li>`
- });
- // custom overlay html 생성
- const contents = ` <div class="area--info" style="top: -10.7rem; right: -6rem;">
- <div class="area--info--title">
- <p style="overflow:hidden; text-overflow: ellipsis;">${marker?.data?.neName}</p>
- <button class="btn-close" id="overlayCloseBtn"></button>
- </div>
- <ul>
- <li><i class="ico green"></i><span>STATUS</span><span class="active">ACTIVE</span></li>
- ${kpiEls.join('')}
- </ul>
- </div>`;
- // overlay 생성
- return new kakao.maps.CustomOverlay({
- position: marker.position,
- content: contents
- })
- }
- // 오버레이에 표시할 KPI 정보 생성
- const fnGetKPIInfo = (data) => {
- // KPI 정보 셋팅 - TODO: CPU, MEM 외의 값이 추가되면 여기에 추가
- const { cpu, mem } = data;
- const kpiItems = [];
- const getBodyLevelClass = (val) => {
- if(val >= 95) return 'red'; // red
- else if(val >= 90) return 'blue'; // blue
- else if(val >= 85) return 'gray'; // gray
- else return 'green'; // green
- }
- if(cpu) {
- const objCpu = typeof cpu === 'string' ? JSON.parse(cpu) : cpu;
- if(objCpu && objCpu[`AVG_CPU_L`]){
- kpiItems.push({label: 'CPU', val: objCpu[`AVG_CPU_L`] || 0, ico: getBodyLevelClass(objCpu[`AVG_CPU_L`])})
- }
- }
- if(mem) {
- const objMem = typeof mem === 'string' ? JSON.parse(mem) : mem ;
- if(objMem && objMem['AVG_MEM_L']){
- kpiItems.push({label: 'MEMORY', val: objMem['AVG_MEM_L'] || 0, ico: getBodyLevelClass(objMem[`AVG_MEM_L`])})
- }
- }
- return kpiItems;
- }
- // 오버레이 닫기 이벤트
- const fnCloseOverlay = () => {
- fnClearMarkerOverlay();
- fnClearMakerClick();
- }
- // 모든 마커 오버레이 초기화
- const fnClearMarkerOverlay = () => {
- markers.value.forEach((marker) => {
- if(marker.overlay) {
- marker.overlay.setMap(null);
- marker.overlay = null;
- }
- })
- }
- /**
- * NE 그룹 목록 그리드 관련
- */
- function fnOnNeGroupGirdReady(params) {
- neGroupGridApi.value = params.api
- }
- // 알림 Cell 렌더러
- function alarmCellRenderer(params) {
- let _cls = "";
- if(params.data.criCnt > 0){
- _cls = 'alarm--red';
- } else if(params.data.majCnt > 0){
- _cls = 'alarm--blue';
- } else if(params.data.minCnt > 0){
- _cls = 'alarm--gray'
- }
- return `<span class="${_cls}"></span>`;
- }
-
- //row clikc
- function fnGroupRowClick(params) {
- const { neList, neGroup } = params.data
- neObj.value.count = neList.length;
- neObj.value.neList = neList;
- // neObj.value.neList = neList.sort((a, b) => {
- // return a.criCnt < b.criCnt ? 1 : a.criCnt > b.criCnt ? -1 : (a.majCnt < b.majCnt ? 1 : a.majCnt > b.majCnt ? -1 : (a.minCnt < b.minCnt ? 1 : a.minCnt > b.minCnt ? -1 : 0));
- // });
- neObj.value.neGroup = neGroup;
- // 그리드
- neGridApi.value.setGridOption('rowData', neObj.value.neList)
- // 지도에 마커 생성
- fnDrawMaker();
- }
-
- /**
- * NE 목록 그리드 관련
- */
- function fnOnNeGirdReady(params) {
- neGridApi.value = params.api
- }
- // Ne 이름 Cell 렌더러
- function neNameCellRenderer(params) {
- let _cls = "";
- if(params.data.criCnt > 0){
- _cls = 'pin--red';
- } else if(params.data.majCnt > 0){
- _cls = 'pin--blue';
- } else if(params.data.minCnt > 0){
- _cls = 'pin--black'
- } else {
- _cls = 'pin--gray'
- }
- return `<span class="${_cls}">${params.data.neName}</span>`;
- }
- // 이벤트 Cell 렌더러
- function eventCellRenderer(params) {
- const { criCnt, majCnt, minCnt } = params.data;
- const events = [];
- if(criCnt && criCnt > 0) {
- events.push(`<span class="evt--critical">CRITICAL (${criCnt}) </span>`)
- }
- if(majCnt && majCnt > 0) {
- events.push(`<span class="evt--major">MAJOR (${majCnt}) </span>`)
- }
- if(minCnt && minCnt > 0) {
- events.push(`<span class="evt--minor">MINOR (${minCnt}) </span>`)
- }
- if(events.length > 0) {
- return events.join('');
- } else {
- return `<span class="evt--none">-</span>`;
- }
- }
- onMounted(() => fnInit())
- </script>
|