diff --git a/src/main/resources/geoinfo/sqlmap/mappers/admins/mgmt/Mgmt_Api_Outdata_SQL.xml b/src/main/resources/geoinfo/sqlmap/mappers/admins/mgmt/Mgmt_Api_Outdata_SQL.xml index 6a98884..2ddbe8e 100644 --- a/src/main/resources/geoinfo/sqlmap/mappers/admins/mgmt/Mgmt_Api_Outdata_SQL.xml +++ b/src/main/resources/geoinfo/sqlmap/mappers/admins/mgmt/Mgmt_Api_Outdata_SQL.xml @@ -37,58 +37,64 @@ - WITH date_range AS ( - SELECT TO_DATE(#{COUNT_FROM_DT}, 'YYYY-MM-DD') + LEVEL - 1 AS dt - FROM dual - CONNECT BY LEVEL TO_DATE(#{COUNT_TO_DT}, 'YYYY-MM-DD') - TO_DATE(#{COUNT_FROM_DT}, 'YYYY-MM-DD') + 1 - ) - SELECT - TO_CHAR(dr.dt, 'YYYY-MM-DD') AS period, - NVL(COUNT(wal.access_dt), 0) || '건' AS access_count - FROM - date_range dr - LEFT JOIN GEOINFO.WEB_API_LOG wal - ON TRUNC(wal.access_dt) = dr.dt - GROUP BY dr.dt - ORDER BY dr.dt + SELECT TO_CHAR(TRUNC(wal.access_dt), 'YYYY-MM-DD') AS period + ,wai.name AS api_name + ,COUNT(*) AS access_count + FROM WEB_API_LOG WAL + INNER JOIN WEB_API_INBOUND WAI ON WAL.API_SEQ = WAI.IDX + WHERE WAL.ACCESS_DT BETWEEN TO_DATE(#{COUNT_FROM_DT}, 'YYYY-MM-DD') + AND TO_DATE(#{COUNT_TO_DT}, 'YYYY-MM-DD') + 0.99999 + GROUP BY TRUNC(WAL.ACCESS_DT) + ,WAI.NAME + ,WAI.IDX + HAVING COUNT(*) > 0 + ORDER BY TRUNC(WAL.ACCESS_DT) DESC + ,wai.idx ASC + ,wai.NAME - WITH month_range AS ( - SELECT ADD_MONTHS(TRUNC(TO_DATE(#{COUNT_FROM_DT}, 'YYYY-MM'), 'MM'), LEVEL - 1) AS month_start - FROM dual - CONNECT BY LEVEL MONTHS_BETWEEN(TRUNC(TO_DATE(#{COUNT_TO_DT}, 'YYYY-MM'), 'MM'), - TRUNC(TO_DATE(#{COUNT_FROM_DT}, 'YYYY-MM'), 'MM')) + 1 - ) - SELECT - TO_CHAR(mr.month_start, 'YYYY-MM') AS period, - NVL(COUNT(wal.access_dt), 0) || '건' AS access_count - FROM - month_range mr - LEFT JOIN GEOINFO.WEB_API_LOG wal - ON TRUNC(wal.access_dt, 'MM') = mr.month_start - GROUP BY mr.month_start - ORDER BY mr.month_start + WITH MONTH_RANGE AS ( + SELECT ADD_MONTHS(TRUNC(TO_DATE(#{COUNT_FROM_DT}, 'YYYY-MM'), 'MM'), LEVEL - 1) AS MONTH_START + FROM DUAL + CONNECT BY LEVEL + MONTHS_BETWEEN( + TRUNC(TO_DATE(#{COUNT_TO_DT}, 'YYYY-MM'), 'MM'), + TRUNC(TO_DATE(#{COUNT_FROM_DT}, 'YYYY-MM'), 'MM') + ) + 1 + ) + SELECT TO_CHAR(MR.MONTH_START, 'YYYY-MM') AS period + ,wai.name AS api_name + ,NVL(COUNT(WAL.ACCESS_DT), 0) AS access_count + FROM MONTH_RANGE MR + LEFT JOIN GEOINFO.WEB_API_LOG WAL ON TRUNC(WAL.ACCESS_DT, 'MM') = MR.MONTH_START + INNER JOIN GEOINFO.WEB_API_INBOUND WAI ON WAL.API_SEQ = WAI.IDX + GROUP BY MR.MONTH_START, wai.name, wai.idx + ORDER BY MR.MONTH_START DESC, wai.idx ASC, wai.name - WITH year_range AS ( - SELECT ADD_MONTHS(TRUNC(TO_DATE(#{COUNT_FROM_DT}, 'YYYY'), 'YYYY'), (LEVEL - 1) * 12) AS year_start - FROM dual - CONNECT BY LEVEL FLOOR(MONTHS_BETWEEN(TRUNC(TO_DATE(#{COUNT_TO_DT}, 'YYYY'), 'YYYY'), - TRUNC(TO_DATE(#{COUNT_FROM_DT}, 'YYYY'), 'YYYY')) / 12) + 1 - ) - SELECT - TO_CHAR(yr.year_start, 'YYYY') AS period, - NVL(COUNT(wal.access_dt), 0) || '건' AS access_count - FROM - year_range yr - LEFT JOIN GEOINFO.WEB_API_LOG wal - ON TRUNC(wal.access_dt, 'YYYY') = yr.year_start - GROUP BY yr.year_start - ORDER BY yr.year_start + WITH YEAR_RANGE AS ( + SELECT ADD_MONTHS(TRUNC(TO_DATE(#{COUNT_FROM_DT}, 'YYYY'), 'YYYY'), (LEVEL - 1) * 12) AS YEAR_START + FROM DUAL + CONNECT BY LEVEL + FLOOR( + MONTHS_BETWEEN( + TRUNC(TO_DATE(#{COUNT_TO_DT}, 'YYYY'), 'YYYY'), + TRUNC(TO_DATE(#{COUNT_FROM_DT}, 'YYYY'), 'YYYY') + ) / 12 + ) + 1 + ) + SELECT TO_CHAR(YR.YEAR_START, 'YYYY') AS period + ,wai.name AS api_name + ,NVL(COUNT(WAL.ACCESS_DT), 0) AS access_count + FROM YEAR_RANGE YR + LEFT JOIN GEOINFO.WEB_API_LOG WAL ON TRUNC(WAL.ACCESS_DT, 'YYYY') = YR.YEAR_START + INNER JOIN GEOINFO.WEB_API_INBOUND WAI ON WAL.API_SEQ = WAI.IDX + GROUP BY YR.YEAR_START, wai.name, wai.idx + ORDER BY YR.YEAR_START DESC, wai.idx ASC, wai.name diff --git a/src/main/webapp/WEB-INF/views/admins/mgmtApi/api-request-statistics-index.jsp b/src/main/webapp/WEB-INF/views/admins/mgmtApi/api-request-statistics-index.jsp index 352c1e5..08562dd 100644 --- a/src/main/webapp/WEB-INF/views/admins/mgmtApi/api-request-statistics-index.jsp +++ b/src/main/webapp/WEB-INF/views/admins/mgmtApi/api-request-statistics-index.jsp @@ -7,10 +7,6 @@ - - - - @@ -509,10 +505,11 @@ } requestAnimationFrame(animation); } - + // --------- 일일 접속량 차트 관련 변수 -------------------- let trafficChart; // 전역 변수 + // --------- 일일 접속량 차트 관련 변수 -------------------- - + // --------- API 호출 통계 기간 입력 관련 변수 -------------------- var today = new Date(); // (기준)오늘 날짜 var thisYear = today.getFullYear(); // (기준)올해연도 var thisMonth = today.getMonth+1; // (기준)이번달 @@ -523,6 +520,16 @@ var fvMonthAgo = new Date(); fvMonthAgo.setMonth(toDate.getMonth() -5); + // --------- API 호출 통계 기간 입력 관련 변수 -------------------- + + // --------- API 호출 통계 CSV 내보내기 관련 변수 -------------------- + let processedStats = { + daily: new Map(), + monthly: new Map(), + yearly: new Map() + }; + // --------- API 호출 통계 CSV 내보내기 관련 변수 -------------------- + $(document).ready(function(){ // ---------------------------------------------------- // 1. 날짜 선택기 초기화 @@ -617,7 +624,7 @@ getMgmtApiList(); // API 호출 통계 > 일별 통계 - getApiStatDaily(); + getApiStat(); // API 호출로그 목록 조회 getMgmtApiLogList(); @@ -632,12 +639,16 @@ // API 호출 현황 건수 - 날짜 변경 시 차트 다시 로드 $(document).on('change', '#count_from_dt, #count_to_dt', function() { -// console.log(validDate()) if(!validDate()){ return; } - getApiStatDaily(); + getApiStat(); }); + + // API 호출 통계 건수 - CSV 내보내기 클릭 + $(document).on('click', '#export-csv-btn', function() { + exportToCSV(); + }) }); // API 호출 통제 목록 조회 @@ -728,8 +739,8 @@ return html; } - // API 호출 통계 > 일별 통계 - function getApiStatDaily(){ + // API 호출 통계 > 일별, 월별, 연도별 통계 + function getApiStat(){ var statType = $('.stats-container > .tab-nav').find('.active').data("tab"); var fromDt = $('#count_from_dt').val(); var toDt = $('#count_to_dt').val(); @@ -751,31 +762,34 @@ }, dataType :"json", success : function(res){ // res.code, res.msg, res.data - if (res.code == "SUCCESS") { - let countList = res.data; //Array List - let $listContainer; - if (statType === "monthly") { - $listContainer = $("#monthly-tbody"); - } else if (statType === "yearly") { - $listContainer = $("#yearly-tbody"); - } else { // statType === "daily" - $listContainer = $("#daily-tbody"); - } - - $listContainer.empty(); // 기존 내용 제거 - - if (!countList.length > 1) { - $listContainer.append('표시 할 통계데이타가 없습니다.'); - return; - } - // 데이터 반복 - $.each(res.data, function(i, item) { - // DOM에 추가 - $listContainer.append(drawApiCountList(item)); - }); - } else { - alert("API 호출 로그 목록 조회중 오류가 발생하였습니다. 다시 시도해주세요."); - } + if (res.code == "SUCCESS") { + let countList = res.data; + let $listContainer; + // 활성화 된 탭의 tbody를 컨테이너로 지정 + if (statType === "monthly") { + $listContainer = $("#monthly-tbody"); + } else if (statType === "yearly") { + $listContainer = $("#yearly-tbody"); + } else { + $listContainer = $("#daily-tbody"); + } + + // 활성화 된 탭의 csv 양식으로 지정 + if (statType === "monthly") { + $listContainer = $("#monthly-tbody"); + processedStats.monthly = countList; + } else if (statType === "yearly") { + $listContainer = $("#yearly-tbody"); + processedStats.yearly = countList; + } else { + $listContainer = $("#daily-tbody"); + processedStats.daily = countList; + } + // DOM에 추가 + drawApiCountList(res.data, $listContainer); + } else { + alert("API 호출 로그 목록 조회중 오류가 발생하였습니다. 다시 시도해주세요."); + } }, error : function(response){ alert("API 호출 로그 목록 조회중 내부 오류가 발생하였습니다. 다시 시도해주세요."); @@ -783,19 +797,43 @@ }); }; - function drawApiCountList(item) { - // HTML 문자열 생성 - const html = ` - - \${item.period} - - \${item.accessCount} - - `; - return html; + function drawApiCountList(countList, $target) { + $target.empty(); + if (!countList || countList.length === 0) { + $target.append('표시 할 통계데이타가 없습니다.'); + return; + } + + const grouped = {}; + let totalCount = 0; // 합계 초기화 + + countList.forEach(item => { + if (!grouped[item.period]) grouped[item.period] = []; + grouped[item.period].push(item); + totalCount += parseInt(item.accessCount); + }); + + let html = ''; + Object.keys(grouped).sort((a, b) => b.localeCompare(a)).forEach(period => { + const items = grouped[period]; + items.forEach((item, idx) => { + html += ` + \${idx === 0 ? `\${period}` : ''} + \${item.apiName} + \${item.accessCount} 건 + `; + }); + }); + + // 합계 row 추가 + html += ` + 합계 + \${totalCount} 건 + `; + + $target.append(html); } - // API 호출 로그 목록 조회 function getMgmtApiLogList(){ $.ajax({ @@ -838,7 +876,6 @@ `; return html; } - // API 호출 로그 목록 조회 function getMgmtApiChart(){ @@ -912,21 +949,65 @@ } }); } + + function exportToCSV() { + // 1. 현재 활성화된 탭 찾기 + const activeTab = document.querySelector('.tab-link.active'); + if (!activeTab) return; + + const tabId = activeTab.getAttribute('data-tab'); // 'daily', 'monthly', 'yearly' + const dataList = processedStats[tabId]; // 전역 변수에서 배열 데이터 가져오기 - /** - * 날짜형식 YYYYMMDDHHMI -> YYYY-MM-DD HH:MI - */ - function formatDateTime(raw) { - if (!raw || raw.length !== 12) return ""; - return ( - raw.slice(0, 4) + "-" + - raw.slice(4, 6) + "-" + - raw.slice(6, 8) + " " + - raw.slice(8, 10) + ":" + - raw.slice(10, 12) - ); + if (!Array.isArray(dataList) || dataList.length === 0) { + alert('내보낼 데이터가 없습니다.'); + return; + } + + let headers = []; + let filename = ''; + + // 2. 탭에 맞는 헤더와 파일명 설정 + if (tabId === 'daily') { + headers = ['날짜', 'API 명', '호출 건수']; + filename = 'api_stats_daily.csv'; + } else if (tabId === 'monthly') { + headers = ['월', 'API 명', '총 호출 건수']; + filename = 'api_stats_monthly.csv'; + } else { // 'yearly' + headers = ['연도', 'API 명', '총 호출 건수']; + filename = 'api_stats_yearly.csv'; + } + + // 3. 날짜 기준으로 내림차순 정렬 + const sortedData = [...dataList].sort((a, b) => b.period.localeCompare(a.period)); + + // 4. CSV 헤더 + 데이터 행 구성 + const rows = [headers]; + sortedData.forEach(item => { + const { period, apiName, accessCount } = item; + rows.push([period, apiName, accessCount]); + }); + + // 5. CSV 문자열로 변환 + const csvContent = rows.map(row => row.join(',')).join('\r\n'); + + // 6. UTF-8 BOM 추가 및 파일 다운로드 + const bom = '\uFEFF'; + const blob = new Blob([bom + csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = filename; + + document.body.appendChild(a); + a.click(); + + // 6. 리소스 정리 + document.body.removeChild(a); + URL.revokeObjectURL(url); } - /** * API 호출 통계 날짜 유효검사 */ @@ -940,65 +1021,6 @@ return true; } - // 예시 데이터 (100개 생성) -// const apiLogs = Array.from({ length: 100 }, (_, i) => ({ -// api: i % 2 === 0 ? "/API_CHA_085/request" : "/API_BH_020/dc332", -// method: ["GET", "POST", "PUT"][Math.floor(Math.random() * 3)], -// params: `subCode=test${i}&prvcCode=x${i}`, -// status: [ "200 OK", "404 NOT FOUND", "500 INTERNAL ERROR" ][Math.floor(Math.random() * 3)], -// time: Math.floor(Math.random() * 500), -// result: ["성공", "지연", "실패"][Math.floor(Math.random() * 3)] -// })); - -// const rowsPerPage = 10; -// let currentPage = 1; - -// function renderTable(page) { -// const tableBody = document.getElementById("apiLogBody"); -// tableBody.innerHTML = ""; - -// const start = (page - 1) * rowsPerPage; -// const end = start + rowsPerPage; -// const pageData = apiLogs.slice(start, end); - -// pageData.forEach(row => { -// const tr = document.createElement("tr"); -// tr.innerHTML += '' + row.api + '' + row.method + '' + row.params + '' + row.status + '' + row.time + '' + getResultIcon(row.result) + ' ' +row.result + ''; -// tableBody.appendChild(tr); -// }); -// } - -// function renderPagination() { -// const totalPages = Math.ceil(apiLogs.length / rowsPerPage); -// const pagination = document.getElementById("pagination"); -// pagination.innerHTML = ""; - -// for (let i = 1; i <= totalPages; i++) { -// const btn = document.createElement("button"); -// btn.textContent = i; -// if (i === currentPage) btn.classList.add("active"); -// btn.addEventListener("click", () => { -// currentPage = i; -// renderTable(currentPage); -// renderPagination(); -// }); -// pagination.appendChild(btn); -// } -// } - -// function getResultIcon(result) { -// if (result === "성공") -// return `✔`; -// if (result === "실패") -// return `⚠`; -// if (result === "지연") -// return `⏱`; -// return ""; -// } - - // 초기 렌더링 -// renderTable(currentPage); -// renderPagination();