Merge remote-tracking branch 'origin/jenkins_with_weblogic' into jenkins_with_weblogic

This commit is contained in:
Rinjae
2026-01-27 11:17:15 +09:00
41 changed files with 6000 additions and 155 deletions
+5
View File
@@ -152,4 +152,9 @@
<bean class="com.eactive.ext.kjb.web.ApiTransactionLogController"></bean> <bean class="com.eactive.ext.kjb.web.ApiTransactionLogController"></bean>
<bean class="com.eactive.ext.kjb.statistics.ApiStatsMinuteController"></bean> <bean class="com.eactive.ext.kjb.statistics.ApiStatsMinuteController"></bean>
<bean class="com.eactive.ext.kjb.statistics.ApiStatsHourController"></bean> <bean class="com.eactive.ext.kjb.statistics.ApiStatsHourController"></bean>
<bean class="com.eactive.ext.kjb.statistics.ApiStatsDayController" />
<bean class="com.eactive.ext.kjb.statistics.ApiStatsMonthController" />
<bean class="com.eactive.ext.kjb.statistics.ApiStatsYearController" />
<bean class="com.eactive.ext.kjb.statistics.ApiStatsAggregationController" />
</beans> </beans>
@@ -513,7 +513,36 @@ function gridRendering(){
colModel:[ colModel:[
{ name : 'PRPTYGROUPNAME' , align:'left' , hidden: true }, { name : 'PRPTYGROUPNAME' , align:'left' , hidden: true },
{ name : 'PRPTYNAME' , width: 70 , align:'left' }, { name : 'PRPTYNAME' , width: 70 , align:'left' },
{ name : 'PRPTY2VAL' , width: 70 , align:'left' , title:false, editable: true, formatter:mltplFormatter, unformatter:mltplUnFormatter}, {
name : 'PRPTY2VAL',
width: 70,
align:'left',
title:false,
editable: true,
formatter:mltplFormatter,
unformatter:mltplUnFormatter,
editoptions: {
dataEvents: [
{
type: 'keydown',
fn: function(e) {
if (e.which === 13) {
var val = $(this).val();
$(this).val($.trim(val));
}
}
},
{
type: 'blur',
fn: function(e) {
var val = $(this).val();
$(this).val($.trim(val));
}
}
]
}
// [여기까지 추가]
},
{ name : 'PRPTYDESC' , width: 300 , align:'left'}, { name : 'PRPTYDESC' , width: 300 , align:'left'},
{ name : 'DMNKND' , hidden:true}, { name : 'DMNKND' , hidden:true},
{ name : 'DOMAINTYPE' , hidden:true, formatter:nullFormatter} { name : 'DOMAINTYPE' , hidden:true, formatter:nullFormatter}
@@ -942,6 +971,7 @@ $(document).ready(function() {
buttonControl(isDetail); buttonControl(isDetail);
titleControl(isDetail); titleControl(isDetail);
}); });
// clientManPopup.jsp에서 호출될 함수 (전역 스코프에 정의) // clientManPopup.jsp에서 호출될 함수 (전역 스코프에 정의)
@@ -0,0 +1,353 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
request.setCharacterEncoding("UTF-8");
%>
<html>
<head>
<title>API 통계 집계 실행</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<style>
.aggregation-section {
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 20px;
margin-bottom: 20px;
}
.aggregation-section h3 {
margin: 0 0 15px 0;
padding-bottom: 10px;
border-bottom: 2px solid #4CAF50;
color: #333;
font-size: 16px;
}
.aggregation-form {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.aggregation-form label {
font-weight: bold;
min-width: 80px;
}
.aggregation-form input[type="text"] {
width: 150px;
padding: 5px 10px;
}
.aggregation-form .cssbtn {
min-width: 120px;
}
.result-area {
margin-top: 15px;
padding: 10px;
border-radius: 4px;
display: none;
}
.result-area.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.result-area.error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.result-area pre {
margin: 5px 0 0 0;
white-space: pre-wrap;
font-size: 12px;
}
.info-box {
background: #e7f3ff;
border-left: 4px solid #2196F3;
padding: 12px;
margin-bottom: 20px;
}
.info-box ul {
margin: 5px 0 0 20px;
padding: 0;
}
.info-box li {
margin: 3px 0;
}
</style>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript">
function executeHourlyToDaily() {
var targetDate = $("#targetDate").val().replace(/-/g, "");
var resultDiv = $("#dailyResult");
// 날짜 입력 검증
if (!targetDate || targetDate.trim() === '') {
alert('대상 날짜를 입력하세요. (예: 20250120)');
$("#targetDate").focus();
return;
}
resultDiv.hide();
$("#btn_execute_daily").prop("disabled", true).text("실행중...");
$.ajax({
url: '<c:url value="/onl/kjb/statistics/apiStatsAggregationMan.json"/>',
type: 'POST',
data: {
cmd: 'AGGREGATION_DAY',
targetDate: targetDate,
serviceType: '${param.serviceType}'
},
success: function(response) {
resultDiv.removeClass("error").addClass("success");
var message = response.message + "\n";
message += "대상 날짜: " + response.targetDate + "\n";
message += "처리 건수: " + response.processedCount;
resultDiv.find("pre").text(message);
resultDiv.show();
},
error: function(xhr) {
resultDiv.removeClass("success").addClass("error");
var message = "집계 실행 실패\n";
try {
var error = JSON.parse(xhr.responseText);
message += error.message;
} catch(e) {
message += xhr.responseText || "알 수 없는 오류가 발생했습니다.";
}
resultDiv.find("pre").text(message);
resultDiv.show();
},
complete: function() {
$("#btn_execute_daily").prop("disabled", false).text("집계 실행");
}
});
}
function executeDailyToMonthly() {
var targetMonth = $("#targetMonth").val().replace(/-/g, "");
var resultDiv = $("#monthlyResult");
// 월 입력 검증
if (!targetMonth || targetMonth.trim() === '') {
alert('대상 월을 입력하세요. (예: 202501)');
$("#targetMonth").focus();
return;
}
resultDiv.hide();
$("#btn_execute_monthly").prop("disabled", true).text("실행중...");
$.ajax({
url: '<c:url value="/onl/kjb/statistics/apiStatsAggregationMan.json"/>',
type: 'POST',
data: {
cmd: 'AGGREGATION_MONTH',
targetMonth: targetMonth,
serviceType: '${param.serviceType}'
},
success: function(response) {
resultDiv.removeClass("error").addClass("success");
var message = response.message + "\n";
message += "대상 월: " + response.targetMonth + "\n";
message += "처리 건수: " + response.processedCount;
resultDiv.find("pre").text(message);
resultDiv.show();
},
error: function(xhr) {
resultDiv.removeClass("success").addClass("error");
var message = "집계 실행 실패\n";
try {
var error = JSON.parse(xhr.responseText);
message += error.message;
} catch(e) {
message += xhr.responseText || "알 수 없는 오류가 발생했습니다.";
}
resultDiv.find("pre").text(message);
resultDiv.show();
},
complete: function() {
$("#btn_execute_monthly").prop("disabled", false).text("집계 실행");
}
});
}
function executeMonthlyToYearly() {
var targetYear = $("#targetYear").val();
var resultDiv = $("#yearlyResult");
// 연도 입력 검증
if (!targetYear || targetYear.trim() === '') {
alert('대상 연도를 입력하세요. (예: 2024)');
$("#targetYear").focus();
return;
}
resultDiv.hide();
$("#btn_execute_yearly").prop("disabled", true).text("실행중...");
$.ajax({
url: '<c:url value="/onl/kjb/statistics/apiStatsAggregationMan.json"/>',
type: 'POST',
data: {
cmd: 'AGGREGATION_YEAR',
targetYear: targetYear,
serviceType: '${param.serviceType}'
},
success: function(response) {
resultDiv.removeClass("error").addClass("success");
var message = response.message + "\n";
message += "대상 연도: " + response.targetYear + "\n";
message += "처리 건수: " + response.processedCount;
resultDiv.find("pre").text(message);
resultDiv.show();
},
error: function(xhr) {
resultDiv.removeClass("success").addClass("error");
var message = "집계 실행 실패\n";
try {
var error = JSON.parse(xhr.responseText);
message += error.message;
} catch(e) {
message += xhr.responseText || "알 수 없는 오류가 발생했습니다.";
}
resultDiv.find("pre").text(message);
resultDiv.show();
},
complete: function() {
$("#btn_execute_yearly").prop("disabled", false).text("집계 실행");
}
});
}
$(document).ready(function() {
// 날짜 입력 마스크
$("#targetDate").inputmask("99999999", { 'autoUnmask': true, 'placeholder': '' });
$("#targetMonth").inputmask("999999", { 'autoUnmask': true, 'placeholder': '' });
$("#targetYear").inputmask("9999", { 'autoUnmask': true, 'placeholder': '' });
// 초기값 설정 (전일, 전월, 전년)
var today = new Date();
// 전일
var yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
var year = yesterday.getFullYear();
var month = String(yesterday.getMonth() + 1).padStart(2, '0');
var day = String(yesterday.getDate()).padStart(2, '0');
$("#targetDate").val(year + month + day);
// 전월
var lastMonth = new Date(today);
lastMonth.setMonth(lastMonth.getMonth() - 1);
var lastMonthYear = lastMonth.getFullYear();
var lastMonthMonth = String(lastMonth.getMonth() + 1).padStart(2, '0');
$("#targetMonth").val(lastMonthYear + lastMonthMonth);
// 전년
var lastYear = today.getFullYear() - 1;
$("#targetYear").val(lastYear);
// 버튼 이벤트
$("#btn_execute_daily").click(function() {
if (confirm("시간별→일별 통계 집계를 실행하시겠습니까?")) {
executeHourlyToDaily();
}
});
$("#btn_execute_monthly").click(function() {
if (confirm("일별→월별 통계 집계를 실행하시겠습니까?")) {
executeDailyToMonthly();
}
});
$("#btn_execute_yearly").click(function() {
if (confirm("월별→연별 통계 집계를 실행하시겠습니까?")) {
executeMonthlyToYearly();
}
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle" id="content_middle">
<div class="title" id="title">API 통계 집계 수동 실행</div>
<div class="info-box">
<strong>⚠️ 주의사항</strong>
<ul>
<li>통계 집계는 스케줄러에 의해 자동으로 실행됩니다.</li>
<li>수동 실행은 누락된 기간이나 재집계가 필요한 경우에만 사용하세요.</li>
<li>집계 대상 기간의 원본 데이터가 존재해야 합니다.</li>
<li>대상 날짜/월/연도를 반드시 입력해야 합니다.</li>
</ul>
</div>
<!-- 시간별 → 일별 집계 -->
<div class="aggregation-section">
<h3>1. 시간별 → 일별 통계 집계</h3>
<div class="aggregation-form">
<label>대상 날짜:</label>
<input type="text" id="targetDate" placeholder="yyyyMMdd" maxlength="8">
<span style="color: #666; font-size: 12px;">(예: 20250120)</span>
<button type="button" class="cssbtn" id="btn_execute_daily" level="W">
<i class="material-icons">play_arrow</i> 집계 실행
</button>
</div>
<div id="dailyResult" class="result-area">
<strong>실행 결과:</strong>
<pre></pre>
</div>
</div>
<!-- 일별 → 월별 집계 -->
<div class="aggregation-section">
<h3>2. 일별 → 월별 통계 집계</h3>
<div class="aggregation-form">
<label>대상 월:</label>
<input type="text" id="targetMonth" placeholder="yyyyMM" maxlength="6">
<span style="color: #666; font-size: 12px;">(예: 202501)</span>
<button type="button" class="cssbtn" id="btn_execute_monthly" level="W">
<i class="material-icons">play_arrow</i> 집계 실행
</button>
</div>
<div id="monthlyResult" class="result-area">
<strong>실행 결과:</strong>
<pre></pre>
</div>
</div>
<!-- 월별 → 연별 집계 -->
<div class="aggregation-section">
<h3>3. 월별 → 연별 통계 집계</h3>
<div class="aggregation-form">
<label>대상 연도:</label>
<input type="text" id="targetYear" placeholder="yyyy" maxlength="4">
<span style="color: #666; font-size: 12px;">(예: 2024)</span>
<button type="button" class="cssbtn" id="btn_execute_yearly" level="W">
<i class="material-icons">play_arrow</i> 집계 실행
</button>
</div>
<div id="yearlyResult" class="result-area">
<strong>실행 결과:</strong>
<pre></pre>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,695 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
request.setCharacterEncoding("UTF-8");
%>
<html>
<head>
<title>일별 통계 조회</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<style>
.chart-container {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
gap: 10px;
}
.chart-container.full-width {
display: block;
}
.chart {
width: 49%;
height: 300px;
border: 1px solid #ddd;
background: #fff;
}
.chart.full-width {
width: 100%;
height: 250px;
}
</style>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
<script language="javascript">
var url = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart;
function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
return Number(cellvalue).toLocaleString();
}
function decimalFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0.000';
return Number(cellvalue).toFixed(3);
}
function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트
var totalDonutOption = {
title: {
text: '총 건수 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류']
},
series: [{
name: '총건수',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트
var callOption = {
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 },
series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] },
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
]
};
callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
}
function updateCharts(data) {
// 시계열 차트용 변수
var times = [];
var successData = [];
var timeoutData = [];
var systemErrData = [];
var bizErrData = [];
var avgRespData = [];
var p50RespData = [];
var p95RespData = [];
// 도넛 차트용 총계 변수
var totalSuccess = 0;
var totalTimeout = 0;
var totalSystemErr = 0;
var totalBizErr = 0;
var seq900Timeout = 0;
var seq900SystemErr = 0;
var seq900BizErr = 0;
data.forEach(function(item) {
times.push(item.statTime);
// 시계열 차트 데이터
successData.push(item.successCnt);
timeoutData.push(item.timeoutCnt);
systemErrData.push(item.systemErrCnt);
bizErrData.push(item.bizErrCnt);
avgRespData.push(item.avgRespTime);
p50RespData.push(item.p50RespTime);
p95RespData.push(item.p95RespTime);
// 도넛 차트용 합산
totalSuccess += (item.successCnt || 0);
totalTimeout += (item.timeoutCnt || 0);
totalSystemErr += (item.systemErrCnt || 0);
totalBizErr += (item.bizErrCnt || 0);
seq900Timeout += (item.seq900TimeoutCnt || 0);
seq900SystemErr += (item.seq900SystemErrCnt || 0);
seq900BizErr += (item.seq900BizErrCnt || 0);
});
// 총건수 도넛 차트 업데이트
var grandTotal = totalSuccess + totalTimeout + totalSystemErr + totalBizErr;
totalDonutChart.setOption({
series: [{
data: [
{ value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' },
{ value: totalBizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: grandTotal.toLocaleString()
}
}]
});
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트
callChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(6, 8) + '일'; }) },
series: [
{ data: successData },
{ data: timeoutData },
{ data: systemErrData },
{ data: bizErrData }
]
});
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(6, 8) + '일'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data;
respChart.rawData = data;
}
function fetchChartData() {
var searchStartDate = $("input[name=searchStartDateTime]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDateTime]").val().replace(/-/g, "");
if (searchStartDate && searchEndDate) {
var start = new Date(searchStartDate.substring(0, 4), parseInt(searchStartDate.substring(4, 6)) - 1, searchStartDate.substring(6, 8));
var end = new Date(searchEndDate.substring(0, 4), parseInt(searchEndDate.substring(4, 6)) - 1, searchEndDate.substring(6, 8));
var diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
if (diffDays > 31) {
alert('조회 기간은 최대 31일까지 가능합니다.');
return;
}
}
var postData = {
cmd: 'CHART',
searchApiName: $("input[name=searchApiName]").val(),
searchGwInstanceId: $("input[name=searchGwInstanceId]").val(),
searchBizDivCode: $("input[name=searchBizDivCode]").val(),
searchClientId: $("input[name=searchClientId]").val(),
searchInboundAdapter: $("input[name=searchInboundAdapter]").val(),
searchOutboundAdapter: $("input[name=searchOutboundAdapter]").val()
};
if (searchStartDate) {
postData.searchStartDateTime = searchStartDate;
}
if (searchEndDate) {
postData.searchEndDateTime = searchEndDate;
}
$.ajax({
url: url,
type: 'POST',
data: postData,
success: function(response) {
updateCharts(response);
},
error: function(xhr, status, error) {
console.error('Chart data fetch error:', error);
}
});
}
function search() {
var searchStartDate = $("input[name=searchStartDateTime]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDateTime]").val().replace(/-/g, "");
if (searchStartDate && searchEndDate) {
var start = new Date(searchStartDate.substring(0, 4), parseInt(searchStartDate.substring(4, 6)) - 1, searchStartDate.substring(6, 8));
var end = new Date(searchEndDate.substring(0, 4), parseInt(searchEndDate.substring(4, 6)) - 1, searchEndDate.substring(6, 8));
var diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
if (diffDays > 31) {
alert('조회 기간은 최대 31일까지 가능합니다.');
return;
}
}
var postData = getSearchForJqgrid("cmd", "LIST");
if (searchStartDate) {
postData.searchStartDateTime = searchStartDate;
}
if (searchEndDate) {
postData.searchEndDateTime = searchEndDate;
}
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
fetchChartData();
}
function exportToExcel() {
console.log('[Excel Export] Starting...');
var searchStartDate = $("input[name=searchStartDateTime]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDateTime]").val().replace(/-/g, "");
var postData = {
cmd: 'EXCEL_EXPORT',
searchApiName: $("input[name=searchApiName]").val(),
searchGwInstanceId: $("input[name=searchGwInstanceId]").val(),
searchBizDivCode: $("input[name=searchBizDivCode]").val(),
searchClientId: $("input[name=searchClientId]").val(),
searchInboundAdapter: $("input[name=searchInboundAdapter]").val(),
searchOutboundAdapter: $("input[name=searchOutboundAdapter]").val(),
serviceType: '${param.serviceType}'
};
if (searchStartDate) {
postData.searchStartDateTime = searchStartDate;
}
if (searchEndDate) {
postData.searchEndDateTime = searchEndDate;
}
console.log('[Excel Export] Request data:', postData);
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function() {
console.log('[Excel Export] Response received - Status:', xhr.status);
if (xhr.status === 200) {
var blob = xhr.response;
var contentType = xhr.getResponseHeader('Content-Type');
console.log('[Excel Export] Blob size:', blob.size, 'Content-Type:', contentType);
// JSON 에러 응답인지 확인
if (contentType && contentType.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function() {
try {
var errorObj = JSON.parse(reader.result);
alert(errorObj.message || 'Excel 파일 생성에 실패했습니다.');
} catch (e) {
alert('Excel 파일 생성에 실패했습니다.');
}
};
reader.readAsText(blob);
return;
}
// Blob 크기가 0이면 에러
if (blob.size === 0) {
alert('Excel 파일 생성에 실패했습니다. (빈 응답)');
return;
}
// 파일명 생성
var filename = 'API일별통계_' + searchStartDate + '.xlsx';
var disposition = xhr.getResponseHeader('Content-Disposition');
console.log('[Excel Export] Content-Disposition:', disposition);
if (disposition && disposition.indexOf('filename=') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
filename = decodeURIComponent(filename);
}
}
console.log('[Excel Export] Downloading as:', filename);
// 파일 다운로드
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
console.log('[Excel Export] Download triggered');
} else if (xhr.status === 204) {
alert('조회된 데이터가 없습니다.');
} else {
// 에러 응답 처리
var reader = new FileReader();
reader.onload = function() {
var message = 'Excel 다운로드 중 오류가 발생했습니다.';
try {
var errorObj = JSON.parse(reader.result);
if (errorObj.message) message = errorObj.message;
} catch (e) {
console.error('[Excel Export] Error parsing error response:', e);
}
alert(message);
};
reader.readAsText(xhr.response);
}
};
xhr.onerror = function() {
console.error('[Excel Export] Network error');
alert('네트워크 오류가 발생했습니다.');
};
// Form data 생성
var formData = [];
for (var key in postData) {
if (postData.hasOwnProperty(key) && postData[key] != null && postData[key] !== '') {
formData.push(encodeURIComponent(key) + '=' + encodeURIComponent(postData[key]));
}
}
xhr.send(formData.join('&'));
}
function list() {
var gridPostData = getSearchForJqgrid("cmd", "LIST");
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
postData: gridPostData,
colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
],
colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false },
{ name: 'apiName', align: 'left', width: '150', sortable: false },
{ name: 'gwInstanceId', align: 'center', width: '80', sortable: false },
{ name: 'bizDivCode', align: 'center', width: '80', sortable: false },
{ name: 'clientId', align: 'left', width: '100', sortable: false },
{ name: 'inboundAdapter', align: 'left', width: '120', sortable: false },
{ name: 'outboundAdapter', align: 'left', width: '120', sortable: false },
{ name: 'totalCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
loadComplete: function(d) {
var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, { sortable: false });
}
}
});
}
$(document).ready(function() {
// 날짜 입력 마스크
$("input[name=searchStartDateTime]").inputmask("9999-99-99", { 'autoUnmask': true });
$("input[name=searchEndDateTime]").inputmask("9999-99-99", { 'autoUnmask': true });
// 기본값 설정 (이번달 1일 ~ 오늘)
var today = getToday();
if (!$("input[name=searchStartDateTime]").val()) {
var firstDay = today.substring(0, 8) + "01";
$("input[name=searchStartDateTime]").val(firstDay.substring(0, 4) + '-' + firstDay.substring(4, 6) + '-' + firstDay.substring(6, 8));
}
if (!$("input[name=searchEndDateTime]").val()) {
$("input[name=searchEndDateTime]").val(today);
}
initCharts();
list();
search();
fetchChartData();
resizeJqGridWidth('grid', 'content_middle', '1200');
$("#btn_search").click(function() {
search();
});
$("#btn_excel_export").click(function() {
exportToExcel();
});
$("input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) {
$("#btn_search").click();
}
});
// 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize();
if (respChart) respChart.resize();
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_search" level="R">
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button>
<button type="button" class="cssbtn" id="btn_excel_export" level="R">
<i class="material-icons">file_download</i> Excel 다운로드
</button>
</div>
<div class="title" id="title">API 일별 통계</div>
<table class="search_condition" cellspacing="0">
<tbody>
<tr>
<th style="width:100px;">조회기간</th>
<td colspan="3">
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;">
~
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;">
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span>
</td>
</tr>
<tr>
<th style="width:100px;">API명</th>
<td>
<input type="text" name="searchApiName" value="${param.searchApiName}">
</td>
<th style="width:100px;">인스턴스</th>
<td>
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
</td>
</tr>
<tr>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">클라이언트ID</th>
<td>
<input type="text" name="searchClientId" value="${param.searchClientId}">
</td>
</tr>
<tr>
<th style="width:100px;">Inbound Adapter</th>
<td>
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
</td>
<th style="width:100px;">Outbound Adapter</th>
<td>
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
</td>
</tr>
</tbody>
</table>
<!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container">
<div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div>
<table id="grid"></table>
<div id="pager"></div>
</div>
</div>
</body>
</html>
@@ -10,7 +10,7 @@
%> %>
<html> <html>
<head> <head>
<title>API 시간별 통계</title> <title>시간별 통계 조회</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<style> <style>
@@ -18,6 +18,10 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 15px; margin-bottom: 15px;
gap: 10px;
}
.chart-container.full-width {
display: block;
} }
.chart { .chart {
width: 49%; width: 49%;
@@ -25,6 +29,10 @@
border: 1px solid #ddd; border: 1px solid #ddd;
background: #fff; background: #fff;
} }
.chart.full-width {
width: 100%;
height: 250px;
}
</style> </style>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script> <script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
@@ -33,7 +41,7 @@
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>';
var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>'; var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
var callChart, respChart; var totalDonutChart, seq900DonutChart, callChart, respChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -46,9 +54,131 @@
} }
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart')); respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트
var totalDonutOption = {
title: {
text: '총 건수 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류']
},
series: [{
name: '총건수',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트
var callOption = { var callOption = {
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } }, title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
tooltip: { tooltip: {
@@ -60,13 +190,16 @@
xAxis: { type: 'category', boundaryGap: false, data: [] }, xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 }, yAxis: { type: 'value', minInterval: 1 },
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] },
{ name: '업무오류', type: 'line', stack: 'Total', areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] } { name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
] ]
}; };
callChart.setOption(callOption);
// 응답시간 차트
var respOption = { var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } }, title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: { tooltip: {
@@ -78,13 +211,12 @@
xAxis: { type: 'category', boundaryGap: false, data: [] }, xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
series: [ series: [
{ name: 'P95', type: 'line', itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] }, { name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', itemStyle: { color: '#5470C6' }, data: [] }, { name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', itemStyle: { color: '#91CC75' }, data: [] } { name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
] ]
}; };
callChart.setOption(callOption);
respChart.setOption(respOption); respChart.setOption(respOption);
// 드릴다운 이벤트 (시간 클릭 시 분단위 화면으로 이동) // 드릴다운 이벤트 (시간 클릭 시 분단위 화면으로 이동)
@@ -101,18 +233,34 @@
// rawData에서 실제 statTime 가져오기 (yyyyMMddHH00 형식) // rawData에서 실제 statTime 가져오기 (yyyyMMddHH00 형식)
var statTime = chart.rawData[dataIndex].statTime; var statTime = chart.rawData[dataIndex].statTime;
var date = statTime.substring(0, 4) + '-' + statTime.substring(4, 6) + '-' + statTime.substring(6, 8); var year = statTime.substring(0, 4);
var hour = statTime.substring(8, 10); var month = statTime.substring(4, 6);
var startTime = hour + ':00'; var day = statTime.substring(6, 8);
var endTime = hour + ':59'; var hour = parseInt(statTime.substring(8, 10));
var params = '?searchStartDate=' + date + '&searchStartTime=' + startTime var startDate = year + '-' + month + '-' + day;
+ '&searchEndDate=' + date + '&searchEndTime=' + endTime; var startTime = (hour < 10 ? '0' + hour : hour) + ':00';
// 1시간 후 계산 (23시인 경우 다음날 00시로 자동 계산)
var d = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), hour, 0, 0);
d.setHours(d.getHours() + 1);
var endDate = d.getFullYear() + '-' +
(d.getMonth() + 1 < 10 ? '0' : '') + (d.getMonth() + 1) + '-' +
(d.getDate() < 10 ? '0' : '') + d.getDate();
var endTime = (d.getHours() < 10 ? '0' : '') + d.getHours() + ':00';
var params = '?searchStartDate=' + startDate + '&searchStartTime=' + startTime
+ '&searchEndDate=' + endDate + '&searchEndTime=' + endTime;
params += '&menuId='+'${param.menuId}';
params += '&cmd='+'LIST';
params += '&serviceType='+'APIGW';
location.href = url_minute_view + params; location.href = url_minute_view + params;
} }
function updateCharts(data) { function updateCharts(data) {
// 시계열 차트용 변수
var times = []; var times = [];
var successData = []; var successData = [];
var timeoutData = []; var timeoutData = [];
@@ -122,10 +270,19 @@
var p50RespData = []; var p50RespData = [];
var p95RespData = []; var p95RespData = [];
// 도넛 차트용 총계 변수
var totalSuccess = 0;
var totalTimeout = 0;
var totalSystemErr = 0;
var totalBizErr = 0;
var seq900Timeout = 0;
var seq900SystemErr = 0;
var seq900BizErr = 0;
data.forEach(function(item) { data.forEach(function(item) {
// statTime: yyyyMMddHH00 -> HH시 형식으로 표시
var hour = item.statTime.substring(8, 10);
times.push(item.statTime); times.push(item.statTime);
// 시계열 차트 데이터
successData.push(item.successCnt); successData.push(item.successCnt);
timeoutData.push(item.timeoutCnt); timeoutData.push(item.timeoutCnt);
systemErrData.push(item.systemErrCnt); systemErrData.push(item.systemErrCnt);
@@ -133,8 +290,53 @@
avgRespData.push(item.avgRespTime); avgRespData.push(item.avgRespTime);
p50RespData.push(item.p50RespTime); p50RespData.push(item.p50RespTime);
p95RespData.push(item.p95RespTime); p95RespData.push(item.p95RespTime);
// 도넛 차트용 합산
totalSuccess += (item.successCnt || 0);
totalTimeout += (item.timeoutCnt || 0);
totalSystemErr += (item.systemErrCnt || 0);
totalBizErr += (item.bizErrCnt || 0);
seq900Timeout += (item.seq900TimeoutCnt || 0);
seq900SystemErr += (item.seq900SystemErrCnt || 0);
seq900BizErr += (item.seq900BizErrCnt || 0);
}); });
// 총건수 도넛 차트 업데이트
var grandTotal = totalSuccess + totalTimeout + totalSystemErr + totalBizErr;
totalDonutChart.setOption({
series: [{
data: [
{ value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' },
{ value: totalBizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: grandTotal.toLocaleString()
}
}]
});
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(8, 10) + '시'; }) }, xAxis: { data: times.map(function(t) { return t.substring(8, 10) + '시'; }) },
series: [ series: [
@@ -145,6 +347,7 @@
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({ respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(8, 10) + '시'; }) }, xAxis: { data: times.map(function(t) { return t.substring(8, 10) + '시'; }) },
series: [ series: [
@@ -204,6 +407,124 @@
fetchChartData(); fetchChartData();
} }
function exportToExcel() {
console.log('[Excel Export] Starting...');
var postData = {
cmd: 'EXCEL_EXPORT',
searchApiName: $("input[name=searchApiName]").val(),
searchGwInstanceId: $("input[name=searchGwInstanceId]").val(),
searchBizDivCode: $("input[name=searchBizDivCode]").val(),
searchClientId: $("input[name=searchClientId]").val(),
searchInboundAdapter: $("input[name=searchInboundAdapter]").val(),
searchOutboundAdapter: $("input[name=searchOutboundAdapter]").val(),
serviceType: '${param.serviceType}'
};
var searchDate = $("input[name=searchDate]").val().replace(/-/g, "");
if (searchDate) {
postData.searchStartDateTime = searchDate + "00";
postData.searchEndDateTime = searchDate + "23";
}
console.log('[Excel Export] Request data:', postData);
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function() {
console.log('[Excel Export] Response received - Status:', xhr.status);
if (xhr.status === 200) {
var blob = xhr.response;
var contentType = xhr.getResponseHeader('Content-Type');
console.log('[Excel Export] Blob size:', blob.size, 'Content-Type:', contentType);
// JSON 에러 응답인지 확인
if (contentType && contentType.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function() {
try {
var errorObj = JSON.parse(reader.result);
alert(errorObj.message || 'Excel 파일 생성에 실패했습니다.');
} catch (e) {
alert('Excel 파일 생성에 실패했습니다.');
}
};
reader.readAsText(blob);
return;
}
// Blob 크기가 0이면 에러
if (blob.size === 0) {
alert('Excel 파일 생성에 실패했습니다. (빈 응답)');
return;
}
// 파일명 추출
var filename = 'api_stats.xlsx';
var disposition = xhr.getResponseHeader('Content-Disposition');
console.log('[Excel Export] Content-Disposition:', disposition);
if (disposition && disposition.indexOf('filename=') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
filename = decodeURIComponent(filename);
}
}
console.log('[Excel Export] Downloading as:', filename);
// 파일 다운로드
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
console.log('[Excel Export] Download triggered');
} else if (xhr.status === 204) {
alert('조회된 데이터가 없습니다.');
} else {
// 에러 응답 처리
var reader = new FileReader();
reader.onload = function() {
var message = 'Excel 다운로드 중 오류가 발생했습니다.';
try {
var errorObj = JSON.parse(reader.result);
if (errorObj.message) message = errorObj.message;
} catch (e) {
console.error('[Excel Export] Error parsing error response:', e);
}
alert(message);
};
reader.readAsText(xhr.response);
}
};
xhr.onerror = function() {
console.error('[Excel Export] Network error');
alert('네트워크 오류가 발생했습니다.');
};
// Form data 생성
var formData = [];
for (var key in postData) {
if (postData.hasOwnProperty(key) && postData[key] != null && postData[key] !== '') {
formData.push(encodeURIComponent(key) + '=' + encodeURIComponent(postData[key]));
}
}
xhr.send(formData.join('&'));
}
function list() { function list() {
var gridPostData = getSearchForJqgrid("cmd", "LIST"); var gridPostData = getSearchForJqgrid("cmd", "LIST");
$('#grid').jqGrid({ $('#grid').jqGrid({
@@ -214,6 +535,7 @@
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
@@ -229,6 +551,9 @@
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
@@ -273,6 +598,10 @@
search(); search();
}); });
$("#btn_excel_export").click(function() {
exportToExcel();
});
$("input[name^=search]").keydown(function(key) { $("input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) { if (key.keyCode == 13) {
$("#btn_search").click(); $("#btn_search").click();
@@ -281,6 +610,8 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize(); if (respChart) respChart.resize();
}); });
@@ -301,6 +632,9 @@
<button type="button" class="cssbtn" id="btn_search" level="R"> <button type="button" class="cssbtn" id="btn_search" level="R">
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %> <i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button> </button>
<button type="button" class="cssbtn" id="btn_excel_export" level="R">
<i class="material-icons">file_download</i> Excel 다운로드
</button>
</div> </div>
<div class="title" id="title">API 시간별 통계</div> <div class="title" id="title">API 시간별 통계</div>
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0">
@@ -344,6 +678,12 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container">
<div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div> <div id="respChart" class="chart"></div>
@@ -10,7 +10,7 @@
%> %>
<html> <html>
<head> <head>
<title>API 분단위 통계</title> <title>분별 통계 조회</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<style> <style>
@@ -18,6 +18,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 15px; margin-bottom: 15px;
gap: 10px;
} }
.chart { .chart {
width: 49%; width: 49%;
@@ -32,7 +33,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
var callChart, respChart; var totalDonutChart, seq900DonutChart, callChart, respChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -45,9 +46,131 @@
} }
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart')); respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트
var totalDonutOption = {
title: {
text: '총 건수 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류']
},
series: [{
name: '총건수',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트
var callOption = { var callOption = {
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } }, title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
tooltip: { tooltip: {
@@ -63,10 +186,10 @@
{ start: 0, end: 100 } { start: 0, end: 100 }
], ],
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false },
{ name: 'Timeout', type: 'line', stack: 'Total', areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [], showSymbol: false }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [], showSymbol: false },
{ name: '시스템오류', type: 'line', stack: 'Total', areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [], showSymbol: false }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [], showSymbol: false },
{ name: '업무오류', type: 'line', stack: 'Total', areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [], showSymbol: false } { name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [], showSymbol: false }
] ]
}; };
@@ -85,9 +208,9 @@
{ start: 0, end: 100 } { start: 0, end: 100 }
], ],
series: [ series: [
{ name: 'P95', type: 'line', itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [], showSymbol: false }, { name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [], showSymbol: false },
{ name: 'P50', type: 'line', itemStyle: { color: '#5470C6' }, data: [], showSymbol: false }, { name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false },
{ name: '평균', type: 'line', itemStyle: { color: '#91CC75' }, data: [], showSymbol: false } { name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [], showSymbol: false }
] ]
}; };
@@ -96,6 +219,7 @@
} }
function updateCharts(data) { function updateCharts(data) {
// 시계열 차트용 변수
var times = []; var times = [];
var successData = []; var successData = [];
var timeoutData = []; var timeoutData = [];
@@ -105,10 +229,21 @@
var p50RespData = []; var p50RespData = [];
var p95RespData = []; var p95RespData = [];
// 도넛 차트용 총계 변수
var totalSuccess = 0;
var totalTimeout = 0;
var totalSystemErr = 0;
var totalBizErr = 0;
var seq900Timeout = 0;
var seq900SystemErr = 0;
var seq900BizErr = 0;
data.forEach(function(item) { data.forEach(function(item) {
// statTime: yyyyMMddHHmm -> HH:mm 형식으로 표시 // statTime: yyyyMMddHHmm -> HH:mm 형식으로 표시
var time = item.statTime.substring(8, 10) + ':' + item.statTime.substring(10, 12); var time = item.statTime.substring(8, 10) + ':' + item.statTime.substring(10, 12);
times.push(time); times.push(time);
// 시계열 차트 데이터
successData.push(item.successCnt); successData.push(item.successCnt);
timeoutData.push(item.timeoutCnt); timeoutData.push(item.timeoutCnt);
systemErrData.push(item.systemErrCnt); systemErrData.push(item.systemErrCnt);
@@ -116,8 +251,53 @@
avgRespData.push(item.avgRespTime); avgRespData.push(item.avgRespTime);
p50RespData.push(item.p50RespTime); p50RespData.push(item.p50RespTime);
p95RespData.push(item.p95RespTime); p95RespData.push(item.p95RespTime);
// 도넛 차트용 합산
totalSuccess += (item.successCnt || 0);
totalTimeout += (item.timeoutCnt || 0);
totalSystemErr += (item.systemErrCnt || 0);
totalBizErr += (item.bizErrCnt || 0);
seq900Timeout += (item.seq900TimeoutCnt || 0);
seq900SystemErr += (item.seq900SystemErrCnt || 0);
seq900BizErr += (item.seq900BizErrCnt || 0);
}); });
// 총건수 도넛 차트 업데이트
var grandTotal = totalSuccess + totalTimeout + totalSystemErr + totalBizErr;
totalDonutChart.setOption({
series: [{
data: [
{ value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' },
{ value: totalBizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: grandTotal.toLocaleString()
}
}]
});
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
xAxis: { data: times }, xAxis: { data: times },
series: [ series: [
@@ -128,6 +308,7 @@
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({ respChart.setOption({
xAxis: { data: times }, xAxis: { data: times },
series: [ series: [
@@ -234,6 +415,127 @@
fetchChartData(range); fetchChartData(range);
} }
function exportToExcel() {
console.log('[Excel Export] Starting...');
var postData = {
cmd: 'EXCEL_EXPORT',
searchApiName: $("input[name=searchApiName]").val(),
searchGwInstanceId: $("input[name=searchGwInstanceId]").val(),
searchBizDivCode: $("input[name=searchBizDivCode]").val(),
searchClientId: $("input[name=searchClientId]").val(),
searchInboundAdapter: $("input[name=searchInboundAdapter]").val(),
searchOutboundAdapter: $("input[name=searchOutboundAdapter]").val(),
serviceType: '${param.serviceType}'
};
var startDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
var startTime = $("input[name=searchStartTime]").val().replace(/:/g, "");
var endDate = $("input[name=searchEndDate]").val().replace(/-/g, "");
var endTime = $("input[name=searchEndTime]").val().replace(/:/g, "");
if (startDate && startTime) {
postData.searchStartDateTime = startDate + startTime;
postData.searchEndDateTime = endDate + endTime;
}
console.log('[Excel Export] Request data:', postData);
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function() {
console.log('[Excel Export] Response received - Status:', xhr.status);
if (xhr.status === 200) {
var blob = xhr.response;
var contentType = xhr.getResponseHeader('Content-Type');
console.log('[Excel Export] Blob size:', blob.size, 'Content-Type:', contentType);
// JSON 에러 응답인지 확인
if (contentType && contentType.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function() {
try {
var errorObj = JSON.parse(reader.result);
alert(errorObj.message || 'Excel 파일 생성에 실패했습니다.');
} catch (e) {
alert('Excel 파일 생성에 실패했습니다.');
}
};
reader.readAsText(blob);
return;
}
// Blob 크기가 0이면 에러
if (blob.size === 0) {
alert('Excel 파일 생성에 실패했습니다. (빈 응답)');
return;
}
// 파일명 추출
var filename = 'api_stats.xlsx';
var disposition = xhr.getResponseHeader('Content-Disposition');
console.log('[Excel Export] Content-Disposition:', disposition);
if (disposition && disposition.indexOf('filename=') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
filename = decodeURIComponent(filename);
}
}
console.log('[Excel Export] Downloading as:', filename);
// 파일 다운로드
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
console.log('[Excel Export] Download triggered');
} else if (xhr.status === 204) {
alert('조회된 데이터가 없습니다.');
} else {
// 에러 응답 처리
var reader = new FileReader();
reader.onload = function() {
var message = 'Excel 다운로드 중 오류가 발생했습니다.';
try {
var errorObj = JSON.parse(reader.result);
if (errorObj.message) message = errorObj.message;
} catch (e) {
console.error('[Excel Export] Error parsing error response:', e);
}
alert(message);
};
reader.readAsText(xhr.response);
}
};
xhr.onerror = function() {
console.error('[Excel Export] Network error');
alert('네트워크 오류가 발생했습니다.');
};
// Form data 생성
var formData = [];
for (var key in postData) {
if (postData.hasOwnProperty(key) && postData[key] != null && postData[key] !== '') {
formData.push(encodeURIComponent(key) + '=' + encodeURIComponent(postData[key]));
}
}
xhr.send(formData.join('&'));
}
function list() { function list() {
var gridPostData = getSearchForJqgrid("cmd", "LIST"); var gridPostData = getSearchForJqgrid("cmd", "LIST");
$('#grid').jqGrid({ $('#grid').jqGrid({
@@ -244,6 +546,7 @@
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
@@ -259,6 +562,9 @@
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
@@ -329,6 +635,10 @@
search(); search();
}); });
$("#btn_excel_export").click(function() {
exportToExcel();
});
$("input[name^=search]").keydown(function(key) { $("input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) { if (key.keyCode == 13) {
$("#btn_search").click(); $("#btn_search").click();
@@ -342,6 +652,8 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize(); if (respChart) respChart.resize();
}); });
@@ -362,6 +674,9 @@
<button type="button" class="cssbtn" id="btn_search" level="R"> <button type="button" class="cssbtn" id="btn_search" level="R">
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %> <i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button> </button>
<button type="button" class="cssbtn" id="btn_excel_export" level="R">
<i class="material-icons">file_download</i> Excel 다운로드
</button>
</div> </div>
<div class="title" id="title">API 분단위 통계</div> <div class="title" id="title">API 분단위 통계</div>
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0">
@@ -409,6 +724,12 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container">
<div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div> <div id="respChart" class="chart"></div>
@@ -0,0 +1,702 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
request.setCharacterEncoding("UTF-8");
%>
<html>
<head>
<title>월별 통계 조회</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<style>
.chart-container {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
gap: 10px;
}
.chart-container.full-width {
display: block;
}
.chart {
width: 49%;
height: 300px;
border: 1px solid #ddd;
background: #fff;
}
.chart.full-width {
width: 100%;
height: 250px;
}
</style>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
<script language="javascript">
var url = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart;
function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
return Number(cellvalue).toLocaleString();
}
function decimalFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0.000';
return Number(cellvalue).toFixed(3);
}
function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트
var totalDonutOption = {
title: {
text: '총 건수 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류']
},
series: [{
name: '총건수',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트
var callOption = {
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 },
series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] },
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
]
};
callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
}
function updateCharts(data) {
// 시계열 차트용 변수
var times = [];
var successData = [];
var timeoutData = [];
var systemErrData = [];
var bizErrData = [];
var avgRespData = [];
var p50RespData = [];
var p95RespData = [];
// 도넛 차트용 총계 변수
var totalSuccess = 0;
var totalTimeout = 0;
var totalSystemErr = 0;
var totalBizErr = 0;
var seq900Timeout = 0;
var seq900SystemErr = 0;
var seq900BizErr = 0;
data.forEach(function(item) {
times.push(item.statTime);
// 시계열 차트 데이터
successData.push(item.successCnt);
timeoutData.push(item.timeoutCnt);
systemErrData.push(item.systemErrCnt);
bizErrData.push(item.bizErrCnt);
avgRespData.push(item.avgRespTime);
p50RespData.push(item.p50RespTime);
p95RespData.push(item.p95RespTime);
// 도넛 차트용 합산
totalSuccess += (item.successCnt || 0);
totalTimeout += (item.timeoutCnt || 0);
totalSystemErr += (item.systemErrCnt || 0);
totalBizErr += (item.bizErrCnt || 0);
seq900Timeout += (item.seq900TimeoutCnt || 0);
seq900SystemErr += (item.seq900SystemErrCnt || 0);
seq900BizErr += (item.seq900BizErrCnt || 0);
});
// 총건수 도넛 차트 업데이트
var grandTotal = totalSuccess + totalTimeout + totalSystemErr + totalBizErr;
totalDonutChart.setOption({
series: [{
data: [
{ value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' },
{ value: totalBizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: grandTotal.toLocaleString()
}
}]
});
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트
callChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(4, 6) + '월'; }) },
series: [
{ data: successData },
{ data: timeoutData },
{ data: systemErrData },
{ data: bizErrData }
]
});
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(4, 6) + '월'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data;
respChart.rawData = data;
}
function fetchChartData() {
var searchStartDate = $("input[name=searchStartDateTime]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDateTime]").val().replace(/-/g, "");
if (searchStartDate && searchEndDate) {
var startYear = parseInt(searchStartDate.substring(0, 4));
var startMonth = parseInt(searchStartDate.substring(4, 6));
var endYear = parseInt(searchEndDate.substring(0, 4));
var endMonth = parseInt(searchEndDate.substring(4, 6));
var diffMonths = (endYear - startYear) * 12 + (endMonth - startMonth);
if (diffMonths > 24) {
alert('조회 기간은 최대 24개월까지 가능합니다.');
return;
}
}
var postData = {
cmd: 'CHART',
searchApiName: $("input[name=searchApiName]").val(),
searchGwInstanceId: $("input[name=searchGwInstanceId]").val(),
searchBizDivCode: $("input[name=searchBizDivCode]").val(),
searchClientId: $("input[name=searchClientId]").val(),
searchInboundAdapter: $("input[name=searchInboundAdapter]").val(),
searchOutboundAdapter: $("input[name=searchOutboundAdapter]").val()
};
if (searchStartDate) {
postData.searchStartDateTime = searchStartDate;
}
if (searchEndDate) {
postData.searchEndDateTime = searchEndDate;
}
$.ajax({
url: url,
type: 'POST',
data: postData,
success: function(response) {
updateCharts(response);
},
error: function(xhr, status, error) {
console.error('Chart data fetch error:', error);
}
});
}
function search() {
var searchStartDate = $("input[name=searchStartDateTime]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDateTime]").val().replace(/-/g, "");
if (searchStartDate && searchEndDate) {
var startYear = parseInt(searchStartDate.substring(0, 4));
var startMonth = parseInt(searchStartDate.substring(4, 6));
var endYear = parseInt(searchEndDate.substring(0, 4));
var endMonth = parseInt(searchEndDate.substring(4, 6));
var diffMonths = (endYear - startYear) * 12 + (endMonth - startMonth);
if (diffMonths > 24) {
alert('조회 기간은 최대 24개월까지 가능합니다.');
return;
}
}
var postData = getSearchForJqgrid("cmd", "LIST");
if (searchStartDate) {
postData.searchStartDateTime = searchStartDate;
}
if (searchEndDate) {
postData.searchEndDateTime = searchEndDate;
}
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
fetchChartData();
}
function exportToExcel() {
console.log('[Excel Export] Starting...');
var searchStartDate = $("input[name=searchStartDateTime]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDateTime]").val().replace(/-/g, "");
var postData = {
cmd: 'EXCEL_EXPORT',
searchApiName: $("input[name=searchApiName]").val(),
searchGwInstanceId: $("input[name=searchGwInstanceId]").val(),
searchBizDivCode: $("input[name=searchBizDivCode]").val(),
searchClientId: $("input[name=searchClientId]").val(),
searchInboundAdapter: $("input[name=searchInboundAdapter]").val(),
searchOutboundAdapter: $("input[name=searchOutboundAdapter]").val(),
serviceType: '${param.serviceType}'
};
if (searchStartDate) {
postData.searchStartDateTime = searchStartDate;
}
if (searchEndDate) {
postData.searchEndDateTime = searchEndDate;
}
console.log('[Excel Export] Request data:', postData);
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function() {
console.log('[Excel Export] Response received - Status:', xhr.status);
if (xhr.status === 200) {
var blob = xhr.response;
var contentType = xhr.getResponseHeader('Content-Type');
console.log('[Excel Export] Blob size:', blob.size, 'Content-Type:', contentType);
// JSON 에러 응답인지 확인
if (contentType && contentType.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function() {
try {
var errorObj = JSON.parse(reader.result);
alert(errorObj.message || 'Excel 파일 생성에 실패했습니다.');
} catch (e) {
alert('Excel 파일 생성에 실패했습니다.');
}
};
reader.readAsText(blob);
return;
}
// Blob 크기가 0이면 에러
if (blob.size === 0) {
alert('Excel 파일 생성에 실패했습니다. (빈 응답)');
return;
}
// 파일명 생성
var filename = 'API월별통계_' + searchStartDate + '.xlsx';
var disposition = xhr.getResponseHeader('Content-Disposition');
console.log('[Excel Export] Content-Disposition:', disposition);
if (disposition && disposition.indexOf('filename=') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
filename = decodeURIComponent(filename);
}
}
console.log('[Excel Export] Downloading as:', filename);
// 파일 다운로드
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
console.log('[Excel Export] Download triggered');
} else if (xhr.status === 204) {
alert('조회된 데이터가 없습니다.');
} else {
// 에러 응답 처리
var reader = new FileReader();
reader.onload = function() {
var message = 'Excel 다운로드 중 오류가 발생했습니다.';
try {
var errorObj = JSON.parse(reader.result);
if (errorObj.message) message = errorObj.message;
} catch (e) {
console.error('[Excel Export] Error parsing error response:', e);
}
alert(message);
};
reader.readAsText(xhr.response);
}
};
xhr.onerror = function() {
console.error('[Excel Export] Network error');
alert('네트워크 오류가 발생했습니다.');
};
// Form data 생성
var formData = [];
for (var key in postData) {
if (postData.hasOwnProperty(key) && postData[key] != null && postData[key] !== '') {
formData.push(encodeURIComponent(key) + '=' + encodeURIComponent(postData[key]));
}
}
xhr.send(formData.join('&'));
}
function list() {
var gridPostData = getSearchForJqgrid("cmd", "LIST");
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
postData: gridPostData,
colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
],
colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false },
{ name: 'apiName', align: 'left', width: '150', sortable: false },
{ name: 'gwInstanceId', align: 'center', width: '80', sortable: false },
{ name: 'bizDivCode', align: 'center', width: '80', sortable: false },
{ name: 'clientId', align: 'left', width: '100', sortable: false },
{ name: 'inboundAdapter', align: 'left', width: '120', sortable: false },
{ name: 'outboundAdapter', align: 'left', width: '120', sortable: false },
{ name: 'totalCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
loadComplete: function(d) {
var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, { sortable: false });
}
}
});
}
$(document).ready(function() {
// 월 입력 마스크
$("input[name=searchStartDateTime]").inputmask("9999-99", { 'autoUnmask': true });
$("input[name=searchEndDateTime]").inputmask("9999-99", { 'autoUnmask': true });
// 기본값 설정 (최근 12개월)
var today = new Date();
var endMonth = today.getFullYear() + '-' + (today.getMonth() + 1 < 10 ? '0' : '') + (today.getMonth() + 1);
var startDate = new Date(today.getFullYear(), today.getMonth() - 11, 1);
var startMonth = startDate.getFullYear() + '-' + (startDate.getMonth() + 1 < 10 ? '0' : '') + (startDate.getMonth() + 1);
if (!$("input[name=searchStartDateTime]").val()) {
$("input[name=searchStartDateTime]").val(startMonth);
}
if (!$("input[name=searchEndDateTime]").val()) {
$("input[name=searchEndDateTime]").val(endMonth);
}
initCharts();
list();
search();
fetchChartData();
resizeJqGridWidth('grid', 'content_middle', '1200');
$("#btn_search").click(function() {
search();
});
$("#btn_excel_export").click(function() {
exportToExcel();
});
$("input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) {
$("#btn_search").click();
}
});
// 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize();
if (respChart) respChart.resize();
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_search" level="R">
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button>
<button type="button" class="cssbtn" id="btn_excel_export" level="R">
<i class="material-icons">file_download</i> Excel 다운로드
</button>
</div>
<div class="title" id="title">API 월별 통계</div>
<table class="search_condition" cellspacing="0">
<tbody>
<tr>
<th style="width:100px;">조회기간</th>
<td colspan="3">
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;" placeholder="YYYY-MM">
~
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;" placeholder="YYYY-MM">
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 24개월)</span>
</td>
</tr>
<tr>
<th style="width:100px;">API명</th>
<td>
<input type="text" name="searchApiName" value="${param.searchApiName}">
</td>
<th style="width:100px;">인스턴스</th>
<td>
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
</td>
</tr>
<tr>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">클라이언트ID</th>
<td>
<input type="text" name="searchClientId" value="${param.searchClientId}">
</td>
</tr>
<tr>
<th style="width:100px;">Inbound Adapter</th>
<td>
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
</td>
<th style="width:100px;">Outbound Adapter</th>
<td>
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
</td>
</tr>
</tbody>
</table>
<!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container">
<div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div>
<table id="grid"></table>
<div id="pager"></div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,670 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
request.setCharacterEncoding("UTF-8");
%>
<html>
<head>
<title>연간 통계 조회</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<style>
.chart-container {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
gap: 10px;
}
.chart-container.full-width {
display: block;
}
.chart {
width: 49%;
height: 300px;
border: 1px solid #ddd;
background: #fff;
}
.chart.full-width {
width: 100%;
height: 250px;
}
</style>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
<script language="javascript">
var url = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart;
function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
return Number(cellvalue).toLocaleString();
}
function decimalFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0.000';
return Number(cellvalue).toFixed(3);
}
function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트
var totalDonutOption = {
title: {
text: '총 건수 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류']
},
series: [{
name: '총건수',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트
var callOption = {
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 },
series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] },
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
]
};
callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
}
function updateCharts(data) {
// 시계열 차트용 변수
var times = [];
var successData = [];
var timeoutData = [];
var systemErrData = [];
var bizErrData = [];
var avgRespData = [];
var p50RespData = [];
var p95RespData = [];
// 도넛 차트용 총계 변수
var totalSuccess = 0;
var totalTimeout = 0;
var totalSystemErr = 0;
var totalBizErr = 0;
var seq900Timeout = 0;
var seq900SystemErr = 0;
var seq900BizErr = 0;
data.forEach(function(item) {
times.push(item.statTime);
// 시계열 차트 데이터
successData.push(item.successCnt);
timeoutData.push(item.timeoutCnt);
systemErrData.push(item.systemErrCnt);
bizErrData.push(item.bizErrCnt);
avgRespData.push(item.avgRespTime);
p50RespData.push(item.p50RespTime);
p95RespData.push(item.p95RespTime);
// 도넛 차트용 합산
totalSuccess += (item.successCnt || 0);
totalTimeout += (item.timeoutCnt || 0);
totalSystemErr += (item.systemErrCnt || 0);
totalBizErr += (item.bizErrCnt || 0);
seq900Timeout += (item.seq900TimeoutCnt || 0);
seq900SystemErr += (item.seq900SystemErrCnt || 0);
seq900BizErr += (item.seq900BizErrCnt || 0);
});
// 총건수 도넛 차트 업데이트
var grandTotal = totalSuccess + totalTimeout + totalSystemErr + totalBizErr;
totalDonutChart.setOption({
series: [{
data: [
{ value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' },
{ value: totalBizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: grandTotal.toLocaleString()
}
}]
});
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트
callChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(0, 4) + '년'; }) },
series: [
{ data: successData },
{ data: timeoutData },
{ data: systemErrData },
{ data: bizErrData }
]
});
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(0, 4) + '년'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data;
respChart.rawData = data;
}
function fetchChartData() {
var searchStartDate = $("input[name=searchStartDateTime]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDateTime]").val().replace(/-/g, "");
var postData = {
cmd: 'CHART',
searchApiName: $("input[name=searchApiName]").val(),
searchGwInstanceId: $("input[name=searchGwInstanceId]").val(),
searchBizDivCode: $("input[name=searchBizDivCode]").val(),
searchClientId: $("input[name=searchClientId]").val(),
searchInboundAdapter: $("input[name=searchInboundAdapter]").val(),
searchOutboundAdapter: $("input[name=searchOutboundAdapter]").val()
};
if (searchStartDate) {
postData.searchStartDateTime = searchStartDate;
}
if (searchEndDate) {
postData.searchEndDateTime = searchEndDate;
}
$.ajax({
url: url,
type: 'POST',
data: postData,
success: function(response) {
updateCharts(response);
},
error: function(xhr, status, error) {
console.error('Chart data fetch error:', error);
}
});
}
function search() {
var searchStartDate = $("input[name=searchStartDateTime]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDateTime]").val().replace(/-/g, "");
var postData = getSearchForJqgrid("cmd", "LIST");
if (searchStartDate) {
postData.searchStartDateTime = searchStartDate;
}
if (searchEndDate) {
postData.searchEndDateTime = searchEndDate;
}
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
fetchChartData();
}
function exportToExcel() {
console.log('[Excel Export] Starting...');
var searchStartDate = $("input[name=searchStartDateTime]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDateTime]").val().replace(/-/g, "");
var postData = {
cmd: 'EXCEL_EXPORT',
searchApiName: $("input[name=searchApiName]").val(),
searchGwInstanceId: $("input[name=searchGwInstanceId]").val(),
searchBizDivCode: $("input[name=searchBizDivCode]").val(),
searchClientId: $("input[name=searchClientId]").val(),
searchInboundAdapter: $("input[name=searchInboundAdapter]").val(),
searchOutboundAdapter: $("input[name=searchOutboundAdapter]").val(),
serviceType: '${param.serviceType}'
};
if (searchStartDate) {
postData.searchStartDateTime = searchStartDate;
}
if (searchEndDate) {
postData.searchEndDateTime = searchEndDate;
}
console.log('[Excel Export] Request data:', postData);
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function() {
console.log('[Excel Export] Response received - Status:', xhr.status);
if (xhr.status === 200) {
var blob = xhr.response;
var contentType = xhr.getResponseHeader('Content-Type');
console.log('[Excel Export] Blob size:', blob.size, 'Content-Type:', contentType);
// JSON 에러 응답인지 확인
if (contentType && contentType.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function() {
try {
var errorObj = JSON.parse(reader.result);
alert(errorObj.message || 'Excel 파일 생성에 실패했습니다.');
} catch (e) {
alert('Excel 파일 생성에 실패했습니다.');
}
};
reader.readAsText(blob);
return;
}
// Blob 크기가 0이면 에러
if (blob.size === 0) {
alert('Excel 파일 생성에 실패했습니다. (빈 응답)');
return;
}
// 파일명 생성
var filename = 'API연간통계_' + searchStartDate + '.xlsx';
var disposition = xhr.getResponseHeader('Content-Disposition');
console.log('[Excel Export] Content-Disposition:', disposition);
if (disposition && disposition.indexOf('filename=') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
filename = decodeURIComponent(filename);
}
}
console.log('[Excel Export] Downloading as:', filename);
// 파일 다운로드
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
console.log('[Excel Export] Download triggered');
} else if (xhr.status === 204) {
alert('조회된 데이터가 없습니다.');
} else {
// 에러 응답 처리
var reader = new FileReader();
reader.onload = function() {
var message = 'Excel 다운로드 중 오류가 발생했습니다.';
try {
var errorObj = JSON.parse(reader.result);
if (errorObj.message) message = errorObj.message;
} catch (e) {
console.error('[Excel Export] Error parsing error response:', e);
}
alert(message);
};
reader.readAsText(xhr.response);
}
};
xhr.onerror = function() {
console.error('[Excel Export] Network error');
alert('네트워크 오류가 발생했습니다.');
};
// Form data 생성
var formData = [];
for (var key in postData) {
if (postData.hasOwnProperty(key) && postData[key] != null && postData[key] !== '') {
formData.push(encodeURIComponent(key) + '=' + encodeURIComponent(postData[key]));
}
}
xhr.send(formData.join('&'));
}
function list() {
var gridPostData = getSearchForJqgrid("cmd", "LIST");
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
postData: gridPostData,
colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
],
colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false },
{ name: 'apiName', align: 'left', width: '150', sortable: false },
{ name: 'gwInstanceId', align: 'center', width: '80', sortable: false },
{ name: 'bizDivCode', align: 'center', width: '80', sortable: false },
{ name: 'clientId', align: 'left', width: '100', sortable: false },
{ name: 'inboundAdapter', align: 'left', width: '120', sortable: false },
{ name: 'outboundAdapter', align: 'left', width: '120', sortable: false },
{ name: 'totalCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
loadComplete: function(d) {
var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, { sortable: false });
}
}
});
}
$(document).ready(function() {
// 연도 입력 마스크
$("input[name=searchStartDateTime]").inputmask("9999", { 'autoUnmask': true });
$("input[name=searchEndDateTime]").inputmask("9999", { 'autoUnmask': true });
// 초기값 설정: 5년 전 ~ 현재 연도
if (!$("input[name=searchStartDateTime]").val()) {
var currentYear = new Date().getFullYear();
var startYear = currentYear - 5;
$("input[name=searchStartDateTime]").val(startYear);
$("input[name=searchEndDateTime]").val(currentYear);
}
initCharts();
list();
search();
fetchChartData();
resizeJqGridWidth('grid', 'content_middle', '1200');
$("#btn_search").click(function() {
search();
});
$("#btn_excel_export").click(function() {
exportToExcel();
});
$("input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) {
$("#btn_search").click();
}
});
// 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize();
if (respChart) respChart.resize();
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_search" level="R">
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button>
<button type="button" class="cssbtn" id="btn_excel_export" level="R">
<i class="material-icons">file_download</i> Excel 다운로드
</button>
</div>
<div class="title" id="title">API 연간 통계</div>
<table class="search_condition" cellspacing="0">
<tbody>
<tr>
<th style="width:100px;">조회기간</th>
<td colspan="3">
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;" placeholder="YYYY">
~
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;" placeholder="YYYY">
</td>
</tr>
<tr>
<th style="width:100px;">API명</th>
<td>
<input type="text" name="searchApiName" value="${param.searchApiName}">
</td>
<th style="width:100px;">인스턴스</th>
<td>
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
</td>
</tr>
<tr>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">클라이언트ID</th>
<td>
<input type="text" name="searchClientId" value="${param.searchClientId}">
</td>
</tr>
<tr>
<th style="width:100px;">Inbound Adapter</th>
<td>
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
</td>
<th style="width:100px;">Outbound Adapter</th>
<td>
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
</td>
</tr>
</tbody>
</table>
<!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container">
<div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div>
<table id="grid"></table>
<div id="pager"></div>
</div>
</div>
</body>
</html>
@@ -303,6 +303,12 @@
return cellvalue; return cellvalue;
} }
function trimFormatter(cellvalue, options, rowObject) {
return cellvalue ? String(cellvalue).trim() : '';
}
$(document).ready(function () { $(document).ready(function () {
console.log(roleString); console.log(roleString);
@@ -373,7 +379,7 @@
{name: 'TRACKASISKEY1CTNT', hidden: true}, {name: 'TRACKASISKEY1CTNT', hidden: true},
{name: 'TRACKASISKEY2CTNT', hidden: true}, {name: 'TRACKASISKEY2CTNT', hidden: true},
{name: 'EAISVCSERNO_TMP', align: 'left', width: '220'}, {name: 'EAISVCSERNO_TMP', align: 'left', width: '220', formatter: trimFormatter},
{name: 'KEYMGTMSGCTNT', align: 'left', width: '240'}, {name: 'KEYMGTMSGCTNT', align: 'left', width: '240'},
{name: 'LOGPRCSSSERNO_TMP', align: 'center', width: '40'}, {name: 'LOGPRCSSSERNO_TMP', align: 'center', width: '40'},
{name: 'TELGMRECVTRANCD', align: 'left', width: '135', hidden: true}, {name: 'TELGMRECVTRANCD', align: 'left', width: '135', hidden: true},
@@ -216,7 +216,7 @@
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]); $(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
//업체코드.업체명 //업체코드.업체명
if(name=="companyName"){ if(name=="eaiSvcSerno"){
if(data["COMPANYCODE"] !=null && data["COMPANYCODE"].trim() != ''){ if(data["COMPANYCODE"] !=null && data["COMPANYCODE"].trim() != ''){
$("input[name=companyName]").val("["+data["COMPANYCODE"]+"] "+data[name.toUpperCase()]); $("input[name=companyName]").val("["+data["COMPANYCODE"]+"] "+data[name.toUpperCase()]);
} }
@@ -233,6 +233,13 @@
} }
}); });
// 거래고유번호 trim
var $svcSernoInput = $("input[name=eaiSvcSerno]");
if($svcSernoInput.val()) {
$svcSernoInput.val( $svcSernoInput.val().trim() );
}
$("input[name=gstatSysAdptrBzwkGroupName],input[name=psvSysAdptrBzwkGroupName]").each(function(){ $("input[name=gstatSysAdptrBzwkGroupName],input[name=psvSysAdptrBzwkGroupName]").each(function(){
var name = $(this).attr("name"); var name = $(this).attr("name");
if ("${rmsMenuAuth}" =="W"){ //2020.07.08 admin 만 버튼보이게 수정 if ("${rmsMenuAuth}" =="W"){ //2020.07.08 admin 만 버튼보이게 수정
@@ -0,0 +1,93 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* API Gateway 일별 처리 통계
*/
@Getter
@Setter
@Entity
@Table(name = "API_STATS_DAY")
@IdClass(ApiStatsDayId.class)
public class ApiStatsDay implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "STAT_TIME", nullable = false)
private LocalDate statTime;
@Id
@Column(name = "API_NAME", nullable = false, length = 100)
private String apiName;
@Id
@Column(name = "GW_INSTANCE_ID", nullable = false, length = 50)
private String gwInstanceId;
@Id
@Column(name = "BIZ_DIV_CODE", nullable = false, length = 50)
private String bizDivCode = "NONE";
@Id
@Column(name = "CLIENT_ID", nullable = false, length = 100)
private String clientId = "NONE";
@Id
@Column(name = "INBOUND_ADAPTER", nullable = false, length = 100)
private String inboundAdapter = "NONE";
@Id
@Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100)
private String outboundAdapter = "NONE";
@Column(name = "TOTAL_CNT", nullable = false)
private Long totalCnt = 0L;
@Column(name = "SUCCESS_CNT", nullable = false)
private Long successCnt = 0L;
@Column(name = "TIMEOUT_CNT", nullable = false)
private Long timeoutCnt = 0L;
@Column(name = "SYSTEM_ERR_CNT", nullable = false)
private Long systemErrCnt = 0L;
@Column(name = "BIZ_ERR_CNT", nullable = false)
private Long bizErrCnt = 0L;
@Column(name = "SEQ900_TIMEOUT_CNT", nullable = false)
private Long seq900TimeoutCnt = 0L;
@Column(name = "SEQ900_SYSTEM_ERR_CNT", nullable = false)
private Long seq900SystemErrCnt = 0L;
@Column(name = "SEQ900_BIZ_ERR_CNT", nullable = false)
private Long seq900BizErrCnt = 0L;
@Column(name = "AVG_RESP_TIME", precision = 10, scale = 3)
private BigDecimal avgRespTime = BigDecimal.ZERO;
@Column(name = "MIN_RESP_TIME", precision = 10, scale = 3)
private BigDecimal minRespTime = BigDecimal.ZERO;
@Column(name = "MAX_RESP_TIME", precision = 10, scale = 3)
private BigDecimal maxRespTime = BigDecimal.ZERO;
@Column(name = "P50_RESP_TIME", precision = 10, scale = 3)
private BigDecimal p50RespTime = BigDecimal.ZERO;
@Column(name = "P95_RESP_TIME", precision = 10, scale = 3)
private BigDecimal p95RespTime = BigDecimal.ZERO;
}
@@ -0,0 +1,26 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import java.io.Serializable;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* API 통계 일별 복합키
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiStatsDayId implements Serializable {
private static final long serialVersionUID = 1L;
private LocalDate statTime;
private String apiName;
private String gwInstanceId;
private String bizDivCode;
private String clientId;
private String inboundAdapter;
private String outboundAdapter;
}
@@ -0,0 +1,10 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import com.eactive.eai.data.jpa.BaseRepository;
/**
* API 통계 일별 Repository
*/
interface ApiStatsDayRepository extends BaseRepository<ApiStatsDay, ApiStatsDayId> {
}
@@ -0,0 +1,152 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.data.jpa.AbstractDataService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Projections;
@Service
@Transactional
public class ApiStatsDayService
extends AbstractDataService<ApiStatsDay, ApiStatsDayId, ApiStatsDayRepository> {
private static final int MAX_DAYS = 31;
public Page<ApiStatsDay> selectList(ApiStatsSearch search, Pageable pageable) {
QApiStatsDay q = QApiStatsDay.apiStatsDay;
BooleanBuilder builder = buildSearchConditions(q, search);
return repository.findAll(builder, pageable);
}
public List<ApiStatsDay> selectListForExcel(ApiStatsSearch search) {
QApiStatsDay q = QApiStatsDay.apiStatsDay;
BooleanBuilder builder = buildSearchConditions(q, search);
return getJPAQueryFactory()
.selectFrom(q)
.where(builder)
.orderBy(q.statTime.desc())
.fetch();
}
public List<ApiStatsChartUI> selectChartData(ApiStatsSearch search) {
QApiStatsDay q = QApiStatsDay.apiStatsDay;
BooleanBuilder builder = new BooleanBuilder();
calculateDateRange(search);
if (search.getParsedStartDate() != null) {
builder.and(q.statTime.goe(search.getParsedStartDate()));
builder.and(q.statTime.loe(search.getParsedEndDate()));
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName()));
}
if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) {
builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId()));
}
if (StringUtils.isNotBlank(search.getSearchBizDivCode())) {
builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode()));
}
if (StringUtils.isNotBlank(search.getSearchClientId())) {
builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId()));
}
return getJPAQueryFactory()
.select(Projections.constructor(ApiStatsChartUI.class,
q.statTime,
q.totalCnt.sum(),
q.successCnt.sum(),
q.timeoutCnt.sum(),
q.systemErrCnt.sum(),
q.bizErrCnt.sum(),
q.seq900TimeoutCnt.sum(),
q.seq900SystemErrCnt.sum(),
q.seq900BizErrCnt.sum(),
q.avgRespTime.avg(),
q.p50RespTime.avg(),
q.p95RespTime.avg()))
.from(q)
.where(builder)
.groupBy(q.statTime)
.orderBy(q.statTime.asc())
.fetch();
}
private BooleanBuilder buildSearchConditions(QApiStatsDay q, ApiStatsSearch search) {
BooleanBuilder builder = new BooleanBuilder();
calculateDateRange(search);
if (search.getParsedStartDate() != null) {
builder.and(q.statTime.goe(search.getParsedStartDate()));
builder.and(q.statTime.loe(search.getParsedEndDate()));
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName()));
}
if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) {
builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId()));
}
if (StringUtils.isNotBlank(search.getSearchBizDivCode())) {
builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode()));
}
if (StringUtils.isNotBlank(search.getSearchClientId())) {
builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId()));
}
if (StringUtils.isNotBlank(search.getSearchInboundAdapter())) {
builder.and(q.inboundAdapter.containsIgnoreCase(search.getSearchInboundAdapter()));
}
if (StringUtils.isNotBlank(search.getSearchOutboundAdapter())) {
builder.and(q.outboundAdapter.containsIgnoreCase(search.getSearchOutboundAdapter()));
}
return builder;
}
/**
* 조회 기간 계산 (최대 31일 제한)
*/
private void calculateDateRange(ApiStatsSearch search) {
if (StringUtils.isBlank(search.getSearchStartDateTime())) {
return;
}
LocalDate startDate = LocalDate.parse(search.getSearchStartDateTime(),
DateTimeFormatter.ofPattern("yyyyMMdd"));
LocalDate endDate;
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
endDate = LocalDate.parse(search.getSearchEndDateTime(),
DateTimeFormatter.ofPattern("yyyyMMdd"));
// 31일 초과 시 시작날짜 기준 31일 후로 강제 설정
if (java.time.Period.between(startDate, endDate).getDays() > MAX_DAYS) {
endDate = startDate.plusDays(MAX_DAYS);
}
} else {
endDate = startDate.plusDays(MAX_DAYS);
}
search.setParsedStartDate(startDate);
search.setParsedEndDate(endDate);
}
}
@@ -60,6 +60,47 @@ public class ApiStatsHourService
return repository.findAll(builder, pageable); return repository.findAll(builder, pageable);
} }
public List<ApiStatsHour> selectListForExcel(ApiStatsSearch search) {
QApiStatsHour q = QApiStatsHour.apiStatsHour;
BooleanBuilder builder = new BooleanBuilder();
calculateDateRange(search);
if (search.getParsedStartDateTime() != null) {
builder.and(q.statTime.goe(search.getParsedStartDateTime()));
builder.and(q.statTime.loe(search.getParsedEndDateTime()));
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName()));
}
if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) {
builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId()));
}
if (StringUtils.isNotBlank(search.getSearchBizDivCode())) {
builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode()));
}
if (StringUtils.isNotBlank(search.getSearchClientId())) {
builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId()));
}
if (StringUtils.isNotBlank(search.getSearchInboundAdapter())) {
builder.and(q.inboundAdapter.containsIgnoreCase(search.getSearchInboundAdapter()));
}
if (StringUtils.isNotBlank(search.getSearchOutboundAdapter())) {
builder.and(q.outboundAdapter.containsIgnoreCase(search.getSearchOutboundAdapter()));
}
return getJPAQueryFactory()
.selectFrom(q)
.where(builder)
.orderBy(q.statTime.desc())
.fetch();
}
public List<ApiStatsChartUI> selectChartData(ApiStatsSearch search) { public List<ApiStatsChartUI> selectChartData(ApiStatsSearch search) {
QApiStatsHour q = QApiStatsHour.apiStatsHour; QApiStatsHour q = QApiStatsHour.apiStatsHour;
BooleanBuilder builder = new BooleanBuilder(); BooleanBuilder builder = new BooleanBuilder();
@@ -94,6 +135,9 @@ public class ApiStatsHourService
q.timeoutCnt.sum(), q.timeoutCnt.sum(),
q.systemErrCnt.sum(), q.systemErrCnt.sum(),
q.bizErrCnt.sum(), q.bizErrCnt.sum(),
q.seq900TimeoutCnt.sum(),
q.seq900SystemErrCnt.sum(),
q.seq900BizErrCnt.sum(),
q.avgRespTime.avg(), q.avgRespTime.avg(),
q.p50RespTime.avg(), q.p50RespTime.avg(),
q.p95RespTime.avg())) q.p95RespTime.avg()))
@@ -62,6 +62,47 @@ public class ApiStatsMinuteService
return repository.findAll(builder, pageable); return repository.findAll(builder, pageable);
} }
public List<ApiStatsMinute> selectListForExcel(ApiStatsSearch search) {
QApiStatsMinute q = QApiStatsMinute.apiStatsMinute;
BooleanBuilder builder = new BooleanBuilder();
calculateDateRange(search);
if (search.getParsedStartDateTime() != null) {
builder.and(q.statTime.goe(search.getParsedStartDateTime()));
builder.and(q.statTime.loe(search.getParsedEndDateTime()));
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName()));
}
if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) {
builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId()));
}
if (StringUtils.isNotBlank(search.getSearchBizDivCode())) {
builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode()));
}
if (StringUtils.isNotBlank(search.getSearchClientId())) {
builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId()));
}
if (StringUtils.isNotBlank(search.getSearchInboundAdapter())) {
builder.and(q.inboundAdapter.containsIgnoreCase(search.getSearchInboundAdapter()));
}
if (StringUtils.isNotBlank(search.getSearchOutboundAdapter())) {
builder.and(q.outboundAdapter.containsIgnoreCase(search.getSearchOutboundAdapter()));
}
return getJPAQueryFactory()
.selectFrom(q)
.where(builder)
.orderBy(q.statTime.desc())
.fetch();
}
public List<ApiStatsChartUI> selectChartData(ApiStatsSearch search) { public List<ApiStatsChartUI> selectChartData(ApiStatsSearch search) {
QApiStatsMinute q = QApiStatsMinute.apiStatsMinute; QApiStatsMinute q = QApiStatsMinute.apiStatsMinute;
BooleanBuilder builder = new BooleanBuilder(); BooleanBuilder builder = new BooleanBuilder();
@@ -96,6 +137,9 @@ public class ApiStatsMinuteService
q.timeoutCnt.sum(), q.timeoutCnt.sum(),
q.systemErrCnt.sum(), q.systemErrCnt.sum(),
q.bizErrCnt.sum(), q.bizErrCnt.sum(),
q.seq900TimeoutCnt.sum(),
q.seq900SystemErrCnt.sum(),
q.seq900BizErrCnt.sum(),
q.avgRespTime.avg(), q.avgRespTime.avg(),
q.p50RespTime.avg(), q.p50RespTime.avg(),
q.p95RespTime.avg())) q.p95RespTime.avg()))
@@ -0,0 +1,92 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import java.io.Serializable;
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* API Gateway 월별 처리 통계
*/
@Getter
@Setter
@Entity
@Table(name = "API_STATS_MONTH")
@IdClass(ApiStatsMonthId.class)
public class ApiStatsMonth implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "STAT_TIME", nullable = false, length = 6)
private String statTime; // YYYYMM format
@Id
@Column(name = "API_NAME", nullable = false, length = 100)
private String apiName;
@Id
@Column(name = "GW_INSTANCE_ID", nullable = false, length = 50)
private String gwInstanceId;
@Id
@Column(name = "BIZ_DIV_CODE", nullable = false, length = 50)
private String bizDivCode = "NONE";
@Id
@Column(name = "CLIENT_ID", nullable = false, length = 100)
private String clientId = "NONE";
@Id
@Column(name = "INBOUND_ADAPTER", nullable = false, length = 100)
private String inboundAdapter = "NONE";
@Id
@Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100)
private String outboundAdapter = "NONE";
@Column(name = "TOTAL_CNT", nullable = false)
private Long totalCnt = 0L;
@Column(name = "SUCCESS_CNT", nullable = false)
private Long successCnt = 0L;
@Column(name = "TIMEOUT_CNT", nullable = false)
private Long timeoutCnt = 0L;
@Column(name = "SYSTEM_ERR_CNT", nullable = false)
private Long systemErrCnt = 0L;
@Column(name = "BIZ_ERR_CNT", nullable = false)
private Long bizErrCnt = 0L;
@Column(name = "SEQ900_TIMEOUT_CNT", nullable = false)
private Long seq900TimeoutCnt = 0L;
@Column(name = "SEQ900_SYSTEM_ERR_CNT", nullable = false)
private Long seq900SystemErrCnt = 0L;
@Column(name = "SEQ900_BIZ_ERR_CNT", nullable = false)
private Long seq900BizErrCnt = 0L;
@Column(name = "AVG_RESP_TIME", precision = 10, scale = 3)
private BigDecimal avgRespTime = BigDecimal.ZERO;
@Column(name = "MIN_RESP_TIME", precision = 10, scale = 3)
private BigDecimal minRespTime = BigDecimal.ZERO;
@Column(name = "MAX_RESP_TIME", precision = 10, scale = 3)
private BigDecimal maxRespTime = BigDecimal.ZERO;
@Column(name = "P50_RESP_TIME", precision = 10, scale = 3)
private BigDecimal p50RespTime = BigDecimal.ZERO;
@Column(name = "P95_RESP_TIME", precision = 10, scale = 3)
private BigDecimal p95RespTime = BigDecimal.ZERO;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* API 통계 월별 복합키
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiStatsMonthId implements Serializable {
private static final long serialVersionUID = 1L;
private String statTime; // YYYYMM format
private String apiName;
private String gwInstanceId;
private String bizDivCode;
private String clientId;
private String inboundAdapter;
private String outboundAdapter;
}
@@ -0,0 +1,10 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import com.eactive.eai.data.jpa.BaseRepository;
/**
* API 통계 월별 Repository
*/
interface ApiStatsMonthRepository extends BaseRepository<ApiStatsMonth, ApiStatsMonthId> {
}
@@ -0,0 +1,153 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.data.jpa.AbstractDataService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Projections;
@Service
@Transactional
public class ApiStatsMonthService
extends AbstractDataService<ApiStatsMonth, ApiStatsMonthId, ApiStatsMonthRepository> {
private static final int MAX_MONTHS = 24;
public Page<ApiStatsMonth> selectList(ApiStatsSearch search, Pageable pageable) {
QApiStatsMonth q = QApiStatsMonth.apiStatsMonth;
BooleanBuilder builder = buildSearchConditions(q, search);
return repository.findAll(builder, pageable);
}
public List<ApiStatsMonth> selectListForExcel(ApiStatsSearch search) {
QApiStatsMonth q = QApiStatsMonth.apiStatsMonth;
BooleanBuilder builder = buildSearchConditions(q, search);
return getJPAQueryFactory()
.selectFrom(q)
.where(builder)
.orderBy(q.statTime.desc())
.fetch();
}
public List<ApiStatsChartUI> selectChartData(ApiStatsSearch search) {
QApiStatsMonth q = QApiStatsMonth.apiStatsMonth;
BooleanBuilder builder = new BooleanBuilder();
calculateDateRange(search);
if (search.getParsedStartMonth() != null) {
builder.and(q.statTime.goe(search.getParsedStartMonth()));
builder.and(q.statTime.loe(search.getParsedEndMonth()));
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName()));
}
if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) {
builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId()));
}
if (StringUtils.isNotBlank(search.getSearchBizDivCode())) {
builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode()));
}
if (StringUtils.isNotBlank(search.getSearchClientId())) {
builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId()));
}
return getJPAQueryFactory()
.select(Projections.constructor(ApiStatsChartUI.class,
q.statTime,
q.totalCnt.sum(),
q.successCnt.sum(),
q.timeoutCnt.sum(),
q.systemErrCnt.sum(),
q.bizErrCnt.sum(),
q.seq900TimeoutCnt.sum(),
q.seq900SystemErrCnt.sum(),
q.seq900BizErrCnt.sum(),
q.avgRespTime.avg(),
q.p50RespTime.avg(),
q.p95RespTime.avg()))
.from(q)
.where(builder)
.groupBy(q.statTime)
.orderBy(q.statTime.asc())
.fetch();
}
private BooleanBuilder buildSearchConditions(QApiStatsMonth q, ApiStatsSearch search) {
BooleanBuilder builder = new BooleanBuilder();
calculateDateRange(search);
if (search.getParsedStartMonth() != null) {
builder.and(q.statTime.goe(search.getParsedStartMonth()));
builder.and(q.statTime.loe(search.getParsedEndMonth()));
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName()));
}
if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) {
builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId()));
}
if (StringUtils.isNotBlank(search.getSearchBizDivCode())) {
builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode()));
}
if (StringUtils.isNotBlank(search.getSearchClientId())) {
builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId()));
}
if (StringUtils.isNotBlank(search.getSearchInboundAdapter())) {
builder.and(q.inboundAdapter.containsIgnoreCase(search.getSearchInboundAdapter()));
}
if (StringUtils.isNotBlank(search.getSearchOutboundAdapter())) {
builder.and(q.outboundAdapter.containsIgnoreCase(search.getSearchOutboundAdapter()));
}
return builder;
}
/**
* 조회 기간 계산 (최대 24개월 제한)
*/
private void calculateDateRange(ApiStatsSearch search) {
if (StringUtils.isBlank(search.getSearchStartDateTime())) {
return;
}
YearMonth startMonth = YearMonth.parse(search.getSearchStartDateTime(),
DateTimeFormatter.ofPattern("yyyyMM"));
YearMonth endMonth;
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
endMonth = YearMonth.parse(search.getSearchEndDateTime(),
DateTimeFormatter.ofPattern("yyyyMM"));
// 12개월 초과 시 시작월 기준 12개월 후로 강제 설정
long monthsBetween = java.time.temporal.ChronoUnit.MONTHS.between(startMonth, endMonth);
if (monthsBetween > MAX_MONTHS) {
endMonth = startMonth.plusMonths(MAX_MONTHS);
}
} else {
endMonth = startMonth.plusMonths(MAX_MONTHS);
}
search.setParsedStartMonth(startMonth.format(DateTimeFormatter.ofPattern("yyyyMM")));
search.setParsedEndMonth(endMonth.format(DateTimeFormatter.ofPattern("yyyyMM")));
}
}
@@ -0,0 +1,92 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import java.io.Serializable;
import java.math.BigDecimal;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
/**
* API Gateway 연간 처리 통계
*/
@Getter
@Setter
@Entity
@Table(name = "API_STATS_YEAR")
@IdClass(ApiStatsYearId.class)
public class ApiStatsYear implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "STAT_TIME", nullable = false, length = 4)
private String statTime; // YYYY format
@Id
@Column(name = "API_NAME", nullable = false, length = 100)
private String apiName;
@Id
@Column(name = "GW_INSTANCE_ID", nullable = false, length = 50)
private String gwInstanceId;
@Id
@Column(name = "BIZ_DIV_CODE", nullable = false, length = 50)
private String bizDivCode = "NONE";
@Id
@Column(name = "CLIENT_ID", nullable = false, length = 100)
private String clientId = "NONE";
@Id
@Column(name = "INBOUND_ADAPTER", nullable = false, length = 100)
private String inboundAdapter = "NONE";
@Id
@Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100)
private String outboundAdapter = "NONE";
@Column(name = "TOTAL_CNT", nullable = false)
private Long totalCnt = 0L;
@Column(name = "SUCCESS_CNT", nullable = false)
private Long successCnt = 0L;
@Column(name = "TIMEOUT_CNT", nullable = false)
private Long timeoutCnt = 0L;
@Column(name = "SYSTEM_ERR_CNT", nullable = false)
private Long systemErrCnt = 0L;
@Column(name = "BIZ_ERR_CNT", nullable = false)
private Long bizErrCnt = 0L;
@Column(name = "SEQ900_TIMEOUT_CNT", nullable = false)
private Long seq900TimeoutCnt = 0L;
@Column(name = "SEQ900_SYSTEM_ERR_CNT", nullable = false)
private Long seq900SystemErrCnt = 0L;
@Column(name = "SEQ900_BIZ_ERR_CNT", nullable = false)
private Long seq900BizErrCnt = 0L;
@Column(name = "AVG_RESP_TIME", precision = 10, scale = 3)
private BigDecimal avgRespTime = BigDecimal.ZERO;
@Column(name = "MIN_RESP_TIME", precision = 10, scale = 3)
private BigDecimal minRespTime = BigDecimal.ZERO;
@Column(name = "MAX_RESP_TIME", precision = 10, scale = 3)
private BigDecimal maxRespTime = BigDecimal.ZERO;
@Column(name = "P50_RESP_TIME", precision = 10, scale = 3)
private BigDecimal p50RespTime = BigDecimal.ZERO;
@Column(name = "P95_RESP_TIME", precision = 10, scale = 3)
private BigDecimal p95RespTime = BigDecimal.ZERO;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* API 통계 연간 복합키
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiStatsYearId implements Serializable {
private static final long serialVersionUID = 1L;
private String statTime; // YYYY format
private String apiName;
private String gwInstanceId;
private String bizDivCode;
private String clientId;
private String inboundAdapter;
private String outboundAdapter;
}
@@ -0,0 +1,10 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import com.eactive.eai.data.jpa.BaseRepository;
/**
* API 통계 연간 Repository
*/
interface ApiStatsYearRepository extends BaseRepository<ApiStatsYear, ApiStatsYearId> {
}
@@ -0,0 +1,143 @@
package com.eactive.eai.rms.data.entity.onl.kjb.statistics;
import java.time.Year;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.data.jpa.AbstractDataService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Projections;
@Service
@Transactional
public class ApiStatsYearService
extends AbstractDataService<ApiStatsYear, ApiStatsYearId, ApiStatsYearRepository> {
public Page<ApiStatsYear> selectList(ApiStatsSearch search, Pageable pageable) {
QApiStatsYear q = QApiStatsYear.apiStatsYear;
BooleanBuilder builder = buildSearchConditions(q, search);
return repository.findAll(builder, pageable);
}
public List<ApiStatsYear> selectListForExcel(ApiStatsSearch search) {
QApiStatsYear q = QApiStatsYear.apiStatsYear;
BooleanBuilder builder = buildSearchConditions(q, search);
return getJPAQueryFactory()
.selectFrom(q)
.where(builder)
.orderBy(q.statTime.desc())
.fetch();
}
public List<ApiStatsChartUI> selectChartData(ApiStatsSearch search) {
QApiStatsYear q = QApiStatsYear.apiStatsYear;
BooleanBuilder builder = new BooleanBuilder();
calculateDateRange(search);
if (search.getParsedStartYear() != null) {
builder.and(q.statTime.goe(search.getParsedStartYear()));
builder.and(q.statTime.loe(search.getParsedEndYear()));
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName()));
}
if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) {
builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId()));
}
if (StringUtils.isNotBlank(search.getSearchBizDivCode())) {
builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode()));
}
if (StringUtils.isNotBlank(search.getSearchClientId())) {
builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId()));
}
return getJPAQueryFactory()
.select(Projections.constructor(ApiStatsChartUI.class,
q.statTime,
q.totalCnt.sum(),
q.successCnt.sum(),
q.timeoutCnt.sum(),
q.systemErrCnt.sum(),
q.bizErrCnt.sum(),
q.seq900TimeoutCnt.sum(),
q.seq900SystemErrCnt.sum(),
q.seq900BizErrCnt.sum(),
q.avgRespTime.avg(),
q.p50RespTime.avg(),
q.p95RespTime.avg()))
.from(q)
.where(builder)
.groupBy(q.statTime)
.orderBy(q.statTime.asc())
.fetch();
}
private BooleanBuilder buildSearchConditions(QApiStatsYear q, ApiStatsSearch search) {
BooleanBuilder builder = new BooleanBuilder();
calculateDateRange(search);
if (search.getParsedStartYear() != null) {
builder.and(q.statTime.goe(search.getParsedStartYear()));
builder.and(q.statTime.loe(search.getParsedEndYear()));
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
builder.and(q.apiName.containsIgnoreCase(search.getSearchApiName()));
}
if (StringUtils.isNotBlank(search.getSearchGwInstanceId())) {
builder.and(q.gwInstanceId.containsIgnoreCase(search.getSearchGwInstanceId()));
}
if (StringUtils.isNotBlank(search.getSearchBizDivCode())) {
builder.and(q.bizDivCode.containsIgnoreCase(search.getSearchBizDivCode()));
}
if (StringUtils.isNotBlank(search.getSearchClientId())) {
builder.and(q.clientId.containsIgnoreCase(search.getSearchClientId()));
}
if (StringUtils.isNotBlank(search.getSearchInboundAdapter())) {
builder.and(q.inboundAdapter.containsIgnoreCase(search.getSearchInboundAdapter()));
}
if (StringUtils.isNotBlank(search.getSearchOutboundAdapter())) {
builder.and(q.outboundAdapter.containsIgnoreCase(search.getSearchOutboundAdapter()));
}
return builder;
}
/**
* 조회 기간 계산
*/
private void calculateDateRange(ApiStatsSearch search) {
if (StringUtils.isBlank(search.getSearchStartDateTime())) {
return;
}
Year startYear = Year.parse(search.getSearchStartDateTime());
Year endYear;
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
endYear = Year.parse(search.getSearchEndDateTime());
} else {
endYear = Year.now();
}
search.setParsedStartYear(String.valueOf(startYear.getValue()));
search.setParsedEndYear(String.valueOf(endYear.getValue()));
}
}
@@ -44,38 +44,47 @@ public class EAILogRepositoryImpl implements EAILogRepository {
QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity; QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity;
JPAQuery<?> query = new JPAQueryFactory(em).from(entityPathBase); JPAQuery<?> query = new JPAQueryFactory(em)
appendConditions(eaiLogSearch, qeaiLog, entityPathBase, query); .from(entityPathBase)
.leftJoin(qeaiMessageEntity)
.on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eaisvcname));
appendConditions(eaiLogSearch, qeaiLog, entityPathBase, query, qeaiMessageEntity);
long totalCount = query.select(qeaiLog.id.msgdpstyms.count()).fetchOne(); long totalCount = query.select(qeaiLog.id.msgdpstyms.count()).fetchOne();
if (totalCount == 0) { if (totalCount == 0) {
return new PageImpl<>(new ArrayList<>(), pageable, 0); return new PageImpl<>(new ArrayList<>(), pageable, 0);
} }
List<EAILogDto> list = query List<EAILogDto> list = query
// .select(Projections.constructor(EAILogDto.class, qeaiLog, qeaiMessageEntity.eaisvcdesc)) // .select(Projections.constructor(EAILogDto.class, qeaiLog, qeaiMessageEntity.eaisvcdesc))
.select(Projections.constructor(EAILogDto.class, .select(Projections.constructor(EAILogDto.class,
qeaiLog.id, qeaiLog.trackasiskey1ctnt, qeaiLog.trackasiskey2ctnt, qeaiLog.keymgtmsgctnt, qeaiLog.id, qeaiLog.trackasiskey1ctnt, qeaiLog.trackasiskey2ctnt, qeaiLog.keymgtmsgctnt,
qeaiLog.eaisvcname, qeaiLog.extbizcd, qeaiLog.refkey, qeaiLog.clientname, qeaiLog.eaisvcname, qeaiLog.extbizcd, qeaiLog.refkey, qeaiLog.clientname,
qeaiLog.orgname, qeaiLog.msgprcssyms, qeaiLog.companycode, qeaiLog.eaierrctnt, qeaiLog.orgname, qeaiLog.msgprcssyms, qeaiLog.companycode, qeaiLog.eaierrctnt,
qeaiMessageEntity.eaisvcdesc)) qeaiMessageEntity.eaisvcdesc))
.from(entityPathBase) // .from(...) --> 이미 위에서 선언했으므로 생략
.leftJoin(qeaiMessageEntity) // .leftJoin(...) --> 이미 위에서 선언했으므로 생략
.on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eaisvcname)) .orderBy(qeaiLog.id.msgdpstyms.desc(), qeaiLog.id.eaisvcserno.asc(), qeaiLog.id.logprcssserno.asc())
.orderBy(qeaiLog.id.msgdpstyms.desc(), qeaiLog.id.eaisvcserno.asc(), qeaiLog.id.logprcssserno.asc()) .limit(1000)
.limit(1000) .fetch();
.fetch();
return new PageImpl<>(list, pageable, totalCount); return new PageImpl<>(list, pageable, totalCount);
} }
public Map<String, Long> countDetailedTransactions(Class<? extends EAILog> clazz, EAILogSearch eaiLogSearch) { public Map<String, Long> countDetailedTransactions(Class<? extends EAILog> clazz, EAILogSearch eaiLogSearch) {
QEAILog qeaiLog = QEAILog.eAILog; QEAILog qeaiLog = QEAILog.eAILog;
EntityPathBase<?> entityPathBase = new EntityPathBase<>(clazz, qeaiLog.getMetadata().getName()); EntityPathBase<?> entityPathBase = new EntityPathBase<>(clazz, qeaiLog.getMetadata().getName());
QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity;
JPAQuery<?> query = new JPAQueryFactory(em).from(entityPathBase); JPAQuery<?> query = new JPAQueryFactory(em)
appendConditions(eaiLogSearch, qeaiLog, entityPathBase, query); .from(entityPathBase)
.leftJoin(qeaiMessageEntity)
.on(qeaiLog.eaisvcname.eq(qeaiMessageEntity.eaisvcname));
appendConditions(eaiLogSearch, qeaiLog, entityPathBase, query, qeaiMessageEntity);
List<Tuple> results = query.select( List<Tuple> results = query.select(
qeaiLog.id.logprcssserno, // 로그 처리 상태 qeaiLog.id.logprcssserno, // 로그 처리 상태
@@ -91,7 +100,7 @@ public class EAILogRepositoryImpl implements EAILogRepository {
return reult; return reult;
} }
private void appendConditions(EAILogSearch eaiLogSearch, QEAILog qeaiLog, EntityPathBase<?> entityPathBase, JPAQuery<?> query) { private void appendConditions(EAILogSearch eaiLogSearch, QEAILog qeaiLog, EntityPathBase<?> entityPathBase, JPAQuery<?> query, QEAIMessageEntity qeaiMessageEntity) {
if (StringUtils.equalsIgnoreCase(eaiLogSearch.getAuthChk(), "Y")) { if (StringUtils.equalsIgnoreCase(eaiLogSearch.getAuthChk(), "Y")) {
QUserBusiness qUserBusiness = QUserBusiness.userBusiness; QUserBusiness qUserBusiness = QUserBusiness.userBusiness;
@@ -127,6 +136,10 @@ public class EAILogRepositoryImpl implements EAILogRepository {
if (StringUtils.isNotBlank(eaiLogSearch.getSearchEaiSvcName())) { if (StringUtils.isNotBlank(eaiLogSearch.getSearchEaiSvcName())) {
query.where(qeaiLog.eaisvcname.containsIgnoreCase(eaiLogSearch.getSearchEaiSvcName())); query.where(qeaiLog.eaisvcname.containsIgnoreCase(eaiLogSearch.getSearchEaiSvcName()));
} }
if (StringUtils.isNotBlank(eaiLogSearch.getSearchEaiSvcDesc())) {
query.where(qeaiMessageEntity.eaisvcdesc.containsIgnoreCase(eaiLogSearch.getSearchEaiSvcDesc()));
}
if (StringUtils.isNotBlank(eaiLogSearch.getSearchKeyMgtMsgCtnt())) { if (StringUtils.isNotBlank(eaiLogSearch.getSearchKeyMgtMsgCtnt())) {
query.where(qeaiLog.keymgtmsgctnt.contains(eaiLogSearch.getSearchKeyMgtMsgCtnt())); query.where(qeaiLog.keymgtmsgctnt.contains(eaiLogSearch.getSearchKeyMgtMsgCtnt()));
@@ -0,0 +1,185 @@
package com.eactive.ext.kjb.statistics;
import java.time.LocalDate;
import java.time.Year;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.eactive.ext.kjb.statistics.job.DailyToMonthlyAggregationJob;
import com.eactive.ext.kjb.statistics.job.HourlyToDailyAggregationJob;
import com.eactive.ext.kjb.statistics.job.MonthlyToYearlyAggregationJob;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* API 통계 집계 수동 실행 Controller
*/
@Slf4j
@Controller
@RequiredArgsConstructor
public class ApiStatsAggregationController {
private final HourlyToDailyAggregationJob hourlyToDailyAggregationJob;
private final DailyToMonthlyAggregationJob dailyToMonthlyAggregationJob;
private final MonthlyToYearlyAggregationJob monthlyToYearlyAggregationJob;
@GetMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.view")
public String view() {
return "/onl/kjb/statistics/apiStatsAggregationMan";
}
/**
* 시간별→일별 통계 집계 수동 실행
* @param targetDate 집계 대상 날짜 (yyyyMMdd 형식, 예: 20250120)
* @return 처리 결과
*/
@PostMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.json", params = "cmd=AGGREGATION_DAY")
public ResponseEntity<Map<String, Object>> executeHourlyToDaily(
@RequestParam(required = false) String targetDate) {
Map<String, Object> result = new HashMap<>();
try {
// 날짜 입력 검증
if (targetDate == null || targetDate.trim().isEmpty()) {
log.error("targetDate 미입력");
result.put("success", false);
result.put("message", "대상 날짜를 입력하세요. (예: 20250120)");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
LocalDate date;
try {
date = LocalDate.parse(targetDate, DateTimeFormatter.ofPattern("yyyyMMdd"));
} catch (DateTimeParseException e) {
log.error("잘못된 날짜 형식: {}", targetDate);
result.put("success", false);
result.put("message", "잘못된 날짜 형식입니다. yyyyMMdd 형식으로 입력하세요. (예: 20250120)");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
log.info("시간별→일별 통계 집계 수동 실행 시작. targetDate: {}", date);
int count = hourlyToDailyAggregationJob.executeManual(date);
result.put("success", true);
result.put("message", "시간별→일별 통계 집계가 완료되었습니다.");
result.put("targetDate", date.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
result.put("processedCount", count);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("시간별→일별 통계 집계 실행 중 오류 발생", e);
result.put("success", false);
result.put("message", "집계 실행 중 오류가 발생했습니다: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 일별→월별 통계 집계 수동 실행
* @param targetMonth 집계 대상 월 (yyyyMM 형식, 예: 202501)
* @return 처리 결과
*/
@PostMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.json", params = "cmd=AGGREGATION_MONTH")
public ResponseEntity<Map<String, Object>> executeDailyToMonthly(
@RequestParam(required = false) String targetMonth) {
Map<String, Object> result = new HashMap<>();
try {
// 월 입력 검증
if (targetMonth == null || targetMonth.trim().isEmpty()) {
log.error("targetMonth 미입력");
result.put("success", false);
result.put("message", "대상 월을 입력하세요. (예: 202501)");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
YearMonth month;
try {
month = YearMonth.parse(targetMonth, DateTimeFormatter.ofPattern("yyyyMM"));
} catch (DateTimeParseException e) {
log.error("잘못된 월 형식: {}", targetMonth);
result.put("success", false);
result.put("message", "잘못된 월 형식입니다. yyyyMM 형식으로 입력하세요. (예: 202501)");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
log.info("일별→월별 통계 집계 수동 실행 시작. targetMonth: {}", month);
int count = dailyToMonthlyAggregationJob.executeManual(month);
result.put("success", true);
result.put("message", "일별→월별 통계 집계가 완료되었습니다.");
result.put("targetMonth", month.format(DateTimeFormatter.ofPattern("yyyyMM")));
result.put("processedCount", count);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("일별→월별 통계 집계 실행 중 오류 발생", e);
result.put("success", false);
result.put("message", "집계 실행 중 오류가 발생했습니다: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
/**
* 월별→연별 통계 집계 수동 실행
* @param targetYear 집계 대상 연도 (yyyy 형식, 예: 2024)
* @return 처리 결과
*/
@PostMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.json", params = "cmd=AGGREGATION_YEAR")
public ResponseEntity<Map<String, Object>> executeMonthlyToYearly(
@RequestParam(required = false) String targetYear) {
Map<String, Object> result = new HashMap<>();
try {
// 연도 입력 검증
if (targetYear == null || targetYear.trim().isEmpty()) {
log.error("targetYear 미입력");
result.put("success", false);
result.put("message", "대상 연도를 입력하세요. (예: 2024)");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
Year year;
try {
year = Year.parse(targetYear);
} catch (DateTimeParseException e) {
log.error("잘못된 연도 형식: {}", targetYear);
result.put("success", false);
result.put("message", "잘못된 연도 형식입니다. yyyy 형식으로 입력하세요. (예: 2024)");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
log.info("월별→연별 통계 집계 수동 실행 시작. targetYear: {}", year);
int count = monthlyToYearlyAggregationJob.executeManual(year);
result.put("success", true);
result.put("message", "월별→연별 통계 집계가 완료되었습니다.");
result.put("targetYear", year.toString());
result.put("processedCount", count);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("월별→연별 통계 집계 실행 중 오류 발생", e);
result.put("success", false);
result.put("message", "집계 실행 중 오류가 발생했습니다: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
}
@@ -0,0 +1,115 @@
package com.eactive.ext.kjb.statistics;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayService;
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
import com.eactive.ext.kjb.statistics.service.ApiStatsExcelExportService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
@RequiredArgsConstructor
public class ApiStatsDayController {
private final ApiStatsDayService service;
private final ApiStatsUIMapper mapper;
private final ApiStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/kjb/statistics/apiStatsDayMan.view")
public String view() {
return "/onl/kjb/statistics/apiStatsDayMan";
}
@PostMapping(value = "/onl/kjb/statistics/apiStatsDayMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<ApiStatsUI>> selectList(ApiStatsSearch search, Pageable pageable) {
Pageable sortedPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
Sort.by(Sort.Order.desc("statTime")));
Page<ApiStatsDay> page = service.selectList(search, sortedPageable);
Page<ApiStatsUI> uiPage = page.map(mapper::toVo);
return ResponseEntity.ok(new GridResponse<>(uiPage));
}
@PostMapping(value = "/onl/kjb/statistics/apiStatsDayMan.json", params = "cmd=CHART")
public ResponseEntity<List<ApiStatsChartUI>> selectChartData(ApiStatsSearch search) {
List<ApiStatsChartUI> chartData = service.selectChartData(search);
return ResponseEntity.ok(chartData);
}
@PostMapping(value = "/onl/kjb/statistics/apiStatsDayMan.json", params = "cmd=EXCEL_EXPORT")
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
log.info("Excel export started - search: {}", search);
try {
List<ApiStatsDay> dataList = service.selectListForExcel(search);
log.info("Data retrieved - count: {}", dataList.size());
if (dataList.isEmpty()) {
log.warn("No data found for export");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"조회된 데이터가 없습니다.\"}");
response.getWriter().flush();
return;
}
List<ApiStatsUI> uiList = dataList.stream()
.map(mapper::toVo)
.collect(Collectors.toList());
log.info("Data converted to UI list - count: {}", uiList.size());
String fileName = generateFileName(search);
log.info("Generating Excel file: {}", fileName);
excelExportService.exportApiStats(uiList, fileName, response);
log.info("Excel export completed successfully");
} catch (Exception e) {
log.error("Excel export failed - error: {}", e.getMessage(), e);
if (!response.isCommitted()) {
response.reset();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"Excel 파일 생성 중 오류가 발생했습니다: " + e.getMessage() + "\"}");
response.getWriter().flush();
} else {
log.error("Cannot send error response - response already committed");
}
}
}
private String generateFileName(ApiStatsSearch search) {
String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime())
? search.getSearchStartDateTime()
: LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
return "API일별통계_" + timeRange + ".xlsx";
}
}
@@ -1,7 +1,14 @@
package com.eactive.ext.kjb.statistics; package com.eactive.ext.kjb.statistics;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@@ -15,6 +22,7 @@ import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHour; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHour;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService;
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper; import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
import com.eactive.ext.kjb.statistics.service.ApiStatsExcelExportService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI; import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
@@ -29,6 +37,7 @@ public class ApiStatsHourController {
private final ApiStatsHourService service; private final ApiStatsHourService service;
private final ApiStatsUIMapper mapper; private final ApiStatsUIMapper mapper;
private final ApiStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/kjb/statistics/apiStatsHourMan.view") @GetMapping(value = "/onl/kjb/statistics/apiStatsHourMan.view")
public String view() { public String view() {
@@ -53,4 +62,54 @@ public class ApiStatsHourController {
List<ApiStatsChartUI> chartData = service.selectChartData(search); List<ApiStatsChartUI> chartData = service.selectChartData(search);
return ResponseEntity.ok(chartData); return ResponseEntity.ok(chartData);
} }
@PostMapping(value = "/onl/kjb/statistics/apiStatsHourMan.json", params = "cmd=EXCEL_EXPORT")
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
log.info("Excel export started - search: {}", search);
try {
List<ApiStatsHour> dataList = service.selectListForExcel(search);
log.info("Data retrieved - count: {}", dataList.size());
if (dataList.isEmpty()) {
log.warn("No data found for export");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"조회된 데이터가 없습니다.\"}");
response.getWriter().flush();
return;
}
List<ApiStatsUI> uiList = dataList.stream()
.map(mapper::toVo)
.collect(Collectors.toList());
log.info("Data converted to UI list - count: {}", uiList.size());
String fileName = generateFileName(search);
log.info("Generating Excel file: {}", fileName);
excelExportService.exportApiStats(uiList, fileName, response);
log.info("Excel export completed successfully");
} catch (Exception e) {
log.error("Excel export failed - error: {}", e.getMessage(), e);
if (!response.isCommitted()) {
response.reset();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"Excel 파일 생성 중 오류가 발생했습니다: " + e.getMessage() + "\"}");
response.getWriter().flush();
} else {
log.error("Cannot send error response - response already committed");
}
}
}
private String generateFileName(ApiStatsSearch search) {
String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime())
? search.getSearchStartDateTime()
: LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHH"));
return "API시간별통계_" + timeRange + ".xlsx";
}
} }
@@ -1,7 +1,14 @@
package com.eactive.ext.kjb.statistics; package com.eactive.ext.kjb.statistics;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@@ -15,6 +22,7 @@ import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinute; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinute;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinuteService; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinuteService;
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper; import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
import com.eactive.ext.kjb.statistics.service.ApiStatsExcelExportService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI; import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
@@ -29,6 +37,7 @@ public class ApiStatsMinuteController {
private final ApiStatsMinuteService service; private final ApiStatsMinuteService service;
private final ApiStatsUIMapper mapper; private final ApiStatsUIMapper mapper;
private final ApiStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.view") @GetMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.view")
public String view() { public String view() {
@@ -53,4 +62,54 @@ public class ApiStatsMinuteController {
List<ApiStatsChartUI> chartData = service.selectChartData(search); List<ApiStatsChartUI> chartData = service.selectChartData(search);
return ResponseEntity.ok(chartData); return ResponseEntity.ok(chartData);
} }
@PostMapping(value = "/onl/kjb/statistics/apiStatsMinuteMan.json", params = "cmd=EXCEL_EXPORT")
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
log.info("Excel export started - search: {}", search);
try {
List<ApiStatsMinute> dataList = service.selectListForExcel(search);
log.info("Data retrieved - count: {}", dataList.size());
if (dataList.isEmpty()) {
log.warn("No data found for export");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"조회된 데이터가 없습니다.\"}");
response.getWriter().flush();
return;
}
List<ApiStatsUI> uiList = dataList.stream()
.map(mapper::toVo)
.collect(Collectors.toList());
log.info("Data converted to UI list - count: {}", uiList.size());
String fileName = generateFileName(search);
log.info("Generating Excel file: {}", fileName);
excelExportService.exportApiStats(uiList, fileName, response);
log.info("Excel export completed successfully");
} catch (Exception e) {
log.error("Excel export failed - error: {}", e.getMessage(), e);
if (!response.isCommitted()) {
response.reset();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"Excel 파일 생성 중 오류가 발생했습니다: " + e.getMessage() + "\"}");
response.getWriter().flush();
} else {
log.error("Cannot send error response - response already committed");
}
}
}
private String generateFileName(ApiStatsSearch search) {
String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime())
? search.getSearchStartDateTime()
: LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
return "API분단위통계_" + timeRange + ".xlsx";
}
} }
@@ -0,0 +1,115 @@
package com.eactive.ext.kjb.statistics;
import java.io.IOException;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonth;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonthService;
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
import com.eactive.ext.kjb.statistics.service.ApiStatsExcelExportService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
@RequiredArgsConstructor
public class ApiStatsMonthController {
private final ApiStatsMonthService service;
private final ApiStatsUIMapper mapper;
private final ApiStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.view")
public String view() {
return "/onl/kjb/statistics/apiStatsMonthMan";
}
@PostMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<ApiStatsUI>> selectList(ApiStatsSearch search, Pageable pageable) {
Pageable sortedPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
Sort.by(Sort.Order.desc("statTime")));
Page<ApiStatsMonth> page = service.selectList(search, sortedPageable);
Page<ApiStatsUI> uiPage = page.map(mapper::toVo);
return ResponseEntity.ok(new GridResponse<>(uiPage));
}
@PostMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.json", params = "cmd=CHART")
public ResponseEntity<List<ApiStatsChartUI>> selectChartData(ApiStatsSearch search) {
List<ApiStatsChartUI> chartData = service.selectChartData(search);
return ResponseEntity.ok(chartData);
}
@PostMapping(value = "/onl/kjb/statistics/apiStatsMonthMan.json", params = "cmd=EXCEL_EXPORT")
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
log.info("Excel export started - search: {}", search);
try {
List<ApiStatsMonth> dataList = service.selectListForExcel(search);
log.info("Data retrieved - count: {}", dataList.size());
if (dataList.isEmpty()) {
log.warn("No data found for export");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"조회된 데이터가 없습니다.\"}");
response.getWriter().flush();
return;
}
List<ApiStatsUI> uiList = dataList.stream()
.map(mapper::toVo)
.collect(Collectors.toList());
log.info("Data converted to UI list - count: {}", uiList.size());
String fileName = generateFileName(search);
log.info("Generating Excel file: {}", fileName);
excelExportService.exportApiStats(uiList, fileName, response);
log.info("Excel export completed successfully");
} catch (Exception e) {
log.error("Excel export failed - error: {}", e.getMessage(), e);
if (!response.isCommitted()) {
response.reset();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"Excel 파일 생성 중 오류가 발생했습니다: " + e.getMessage() + "\"}");
response.getWriter().flush();
} else {
log.error("Cannot send error response - response already committed");
}
}
}
private String generateFileName(ApiStatsSearch search) {
String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime())
? search.getSearchStartDateTime()
: YearMonth.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
return "API월별통계_" + timeRange + ".xlsx";
}
}
@@ -0,0 +1,114 @@
package com.eactive.ext.kjb.statistics;
import java.io.IOException;
import java.time.Year;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYear;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYearService;
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
import com.eactive.ext.kjb.statistics.service.ApiStatsExcelExportService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
@RequiredArgsConstructor
public class ApiStatsYearController {
private final ApiStatsYearService service;
private final ApiStatsUIMapper mapper;
private final ApiStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/kjb/statistics/apiStatsYearMan.view")
public String view() {
return "/onl/kjb/statistics/apiStatsYearMan";
}
@PostMapping(value = "/onl/kjb/statistics/apiStatsYearMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<ApiStatsUI>> selectList(ApiStatsSearch search, Pageable pageable) {
Pageable sortedPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
Sort.by(Sort.Order.desc("statTime")));
Page<ApiStatsYear> page = service.selectList(search, sortedPageable);
Page<ApiStatsUI> uiPage = page.map(mapper::toVo);
return ResponseEntity.ok(new GridResponse<>(uiPage));
}
@PostMapping(value = "/onl/kjb/statistics/apiStatsYearMan.json", params = "cmd=CHART")
public ResponseEntity<List<ApiStatsChartUI>> selectChartData(ApiStatsSearch search) {
List<ApiStatsChartUI> chartData = service.selectChartData(search);
return ResponseEntity.ok(chartData);
}
@PostMapping(value = "/onl/kjb/statistics/apiStatsYearMan.json", params = "cmd=EXCEL_EXPORT")
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
log.info("Excel export started - search: {}", search);
try {
List<ApiStatsYear> dataList = service.selectListForExcel(search);
log.info("Data retrieved - count: {}", dataList.size());
if (dataList.isEmpty()) {
log.warn("No data found for export");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"조회된 데이터가 없습니다.\"}");
response.getWriter().flush();
return;
}
List<ApiStatsUI> uiList = dataList.stream()
.map(mapper::toVo)
.collect(Collectors.toList());
log.info("Data converted to UI list - count: {}", uiList.size());
String fileName = generateFileName(search);
log.info("Generating Excel file: {}", fileName);
excelExportService.exportApiStats(uiList, fileName, response);
log.info("Excel export completed successfully");
} catch (Exception e) {
log.error("Excel export failed - error: {}", e.getMessage(), e);
if (!response.isCommitted()) {
response.reset();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().write("{\"message\":\"Excel 파일 생성 중 오류가 발생했습니다: " + e.getMessage() + "\"}");
response.getWriter().flush();
} else {
log.error("Cannot send error response - response already committed");
}
}
}
private String generateFileName(ApiStatsSearch search) {
String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime())
? search.getSearchStartDateTime()
: String.valueOf(Year.now().getValue());
return "API연간통계_" + timeRange + ".xlsx";
}
}
@@ -1,102 +0,0 @@
# API Gateway 통계 화면
## 개요
API Gateway의 분단위/시간별 처리 통계를 조회하는 관리 화면입니다.
## 파일 구조
```
com.eactive.ext.kjb.statistics/
├── ApiStatsMinuteController.java # 분단위 통계 Controller
├── ApiStatsMinuteService.java # 분단위 통계 Service
├── ApiStatsHourController.java # 시간별 통계 Controller
├── ApiStatsHourService.java # 시간별 통계 Service
├── entity/
│ ├── ApiStatsMinute.java # 분단위 통계 Entity
│ ├── ApiStatsMinuteId.java # 분단위 통계 복합키
│ ├── ApiStatsHour.java # 시간별 통계 Entity
│ └── ApiStatsHourId.java # 시간별 통계 복합키
├── repository/
│ ├── ApiStatsMinuteRepository.java
│ └── ApiStatsHourRepository.java
├── ui/
│ ├── ApiStatsUI.java # 통계 UI DTO
│ └── ApiStatsSearch.java # 검색조건 DTO
├── mapping/
│ └── ApiStatsUIMapper.java # Entity ↔ UI 매핑
└── README.md
JSP 화면:
WebContent/jsp/kjb/statistics/
├── apiStatsMinuteMan.jsp # 분단위 통계 화면
└── apiStatsHourMan.jsp # 시간별 통계 화면
```
## URL
| 화면 | URL |
|------|-----|
| 분단위 통계 | `/kjb/statistics/apiStatsMinuteMan.view` |
| 시간별 통계 | `/kjb/statistics/apiStatsHourMan.view` |
## 테이블
| 테이블명 | 설명 |
|----------|------|
| API_STATS_MINUTE | 분단위 통계 |
| API_STATS_HOUR | 시간별 통계 |
## 복합키 구성
- statTime (통계 시간)
- apiName (API명)
- gwInstanceId (Gateway 인스턴스 ID)
- bizDivCode (업무구분코드)
- clientId (클라이언트 ID)
- inboundAdapter (Inbound Adapter)
- outboundAdapter (Outbound Adapter)
## 메트릭
### 건수 메트릭
- totalCnt: 총 처리 건수
- successCnt: 성공 건수
- timeoutCnt: Timeout 오류 건수
- systemErrCnt: 시스템 오류 건수
- bizErrCnt: 업무 오류 건수
- seq900TimeoutCnt: 로그시퀀스900 Timeout 건수
- seq900SystemErrCnt: 로그시퀀스900 시스템 오류 건수
- seq900BizErrCnt: 로그시퀀스900 업무 오류 건수
### 응답시간 메트릭 (ms)
- avgRespTime: 평균 응답시간
- minRespTime: 최소 응답시간
- maxRespTime: 최대 응답시간
- p50RespTime: 50 백분위 응답시간
- p95RespTime: 95 백분위 응답시간
## Multi-tenancy
API Gateway 통계는 `APIGW` 스키마에 저장됩니다.
Controller에서 `DataSourceContextHolder`를 사용하여 스키마를 전환합니다.
```java
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
```
## 검색 조건
- 조회기간 (시작~종료)
- API명 (like 검색)
- Gateway 인스턴스 ID
- 업무구분코드 (like 검색)
- 클라이언트 ID (like 검색)
## 기술 스택
- JPA + QueryDSL
- MapStruct (Entity ↔ UI 매핑)
- Spring MVC (ResponseEntity)
- jqGrid (목록 화면)
@@ -0,0 +1,167 @@
# API Gateway 통계 화면
## 개요
API Gateway의 분단위/시간별/일별/월별/연도별 처리 통계를 조회하는 관리 화면입니다.
## 파일 구조
```
com.eactive.ext.kjb.statistics/
├── ApiStatsMinuteController.java # 분단위 통계 Controller
├── ApiStatsHourController.java # 시간별 통계 Controller
├── ApiStatsDayController.java # 일별 통계 Controller
├── ApiStatsMonthController.java # 월별 통계 Controller
├── ApiStatsYearController.java # 연도별 통계 Controller
├── ApiStatsAggregationController.java # 통계 집계 수동 실행 Controller
├── service/
│ └── ApiStatsExcelExportService.java # Excel 내보내기 Service
├── entity/
│ ├── ApiStatsMinute.java # 분단위 통계 Entity
│ ├── ApiStatsMinuteId.java # 분단위 통계 복합키
│ ├── ApiStatsHour.java # 시간별 통계 Entity
│ ├── ApiStatsHourId.java # 시간별 통계 복합키
│ ├── ApiStatsDay.java # 일별 통계 Entity
│ ├── ApiStatsDayId.java # 일별 통계 복합키
│ ├── ApiStatsMonth.java # 월별 통계 Entity
│ ├── ApiStatsMonthId.java # 월별 통계 복합키
│ ├── ApiStatsYear.java # 연도별 통계 Entity
│ └── ApiStatsYearId.java # 연도별 통계 복합키
├── repository/
│ ├── ApiStatsMinuteRepository.java
│ ├── ApiStatsHourRepository.java
│ ├── ApiStatsDayRepository.java
│ ├── ApiStatsMonthRepository.java
│ └── ApiStatsYearRepository.java
├── ui/
│ ├── ApiStatsUI.java # 통계 UI DTO
│ ├── ApiStatsSearch.java # 검색조건 DTO
│ └── ApiStatsChartUI.java # 차트 UI DTO
├── mapping/
│ └── ApiStatsUIMapper.java # Entity ↔ UI 매핑
└── README.md
JSP 화면:
WebContent/jsp/kjb/statistics/
├── apiStatsMinuteMan.jsp # 분단위 통계 화면
├── apiStatsHourMan.jsp # 시간별 통계 화면
├── apiStatsDayMan.jsp # 일별 통계 화면
├── apiStatsMonthMan.jsp # 월별 통계 화면
├── apiStatsYearMan.jsp # 연도별 통계 화면
└── apiStatsAggregationMan.jsp # 통계 집계 수동 실행 화면
```
## URL
| 화면 | URL |
|------|-----|
| 분단위 통계 | `/kjb/statistics/apiStatsMinuteMan.view` |
| 시간별 통계 | `/kjb/statistics/apiStatsHourMan.view` |
| 일별 통계 | `/kjb/statistics/apiStatsDayMan.view` |
| 월별 통계 | `/kjb/statistics/apiStatsMonthMan.view` |
| 연도별 통계 | `/kjb/statistics/apiStatsYearMan.view` |
| 통계 집계 실행 | `/kjb/statistics/apiStatsAggregationMan.view` |
## 테이블
| 테이블명 | 설명 |
|----------|------|
| API_STATS_MINUTE | 분단위 통계 |
| API_STATS_HOUR | 시간별 통계 |
| API_STATS_DAY | 일별 통계 |
| API_STATS_MONTH | 월별 통계 (STAT_TIME: YYYYMM 형식) |
| API_STATS_YEAR | 연도별 통계 (STAT_TIME: YYYY 형식) |
> 상세 테이블 스키마는 [statistics_table.md](statistics_table.md) 참조
## 복합키 구성
- statTime (통계 시간)
- apiName (API명)
- gwInstanceId (Gateway 인스턴스 ID)
- bizDivCode (업무구분코드)
- clientId (클라이언트 ID)
- inboundAdapter (Inbound Adapter)
- outboundAdapter (Outbound Adapter)
## 메트릭
### 건수 메트릭
- totalCnt: 총 처리 건수
- successCnt: 성공 건수
- timeoutCnt: Timeout 오류 건수
- systemErrCnt: 시스템 오류 건수
- bizErrCnt: 업무 오류 건수
- seq900TimeoutCnt: 로그시퀀스900 Timeout 건수
- seq900SystemErrCnt: 로그시퀀스900 시스템 오류 건수
- seq900BizErrCnt: 로그시퀀스900 업무 오류 건수
### 응답시간 메트릭 (ms)
- avgRespTime: 평균 응답시간
- minRespTime: 최소 응답시간
- maxRespTime: 최대 응답시간
- p50RespTime: 50 백분위 응답시간
- p95RespTime: 95 백분위 응답시간
## Multi-tenancy
API Gateway 통계는 `APIGW` 스키마에 저장됩니다.
Controller에서 `DataSourceContextHolder`를 사용하여 스키마를 전환합니다.
```java
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
```
## 검색 조건
- 조회기간 (시작~종료)
- API명 (like 검색)
- Gateway 인스턴스 ID
- 업무구분코드 (like 검색)
- 클라이언트 ID (like 검색)
## 주요 기능
- 통계 데이터 조회 (분/시간/일/월/년 단위)
- Excel 내보내기
- 차트 데이터 제공 (ApiStatsChartUI)
- 통계 집계 수동 실행 (시간→일, 일→월, 월→년)
## 통계 집계 수동 실행
통계 데이터는 스케줄러에 의해 자동으로 집계되지만, 누락된 기간이나 재집계가 필요한 경우 수동 실행할 수 있습니다.
### 집계 실행 화면 (apiStatsAggregationMan.jsp)
**접근 URL**: `/kjb/statistics/apiStatsAggregationMan.view`
한 화면에서 3가지 집계 작업을 모두 수동으로 실행할 수 있습니다:
1. **시간별 → 일별 집계**
- API 엔드포인트: `POST /onl/kjb/statistics/apiStatsDayMan.json?cmd=AGGREGATION`
- 파라미터: `targetDate` (yyyyMMdd 형식, 예: 20250120)
- 기본값: 전일 날짜
2. **일별 → 월별 집계**
- API 엔드포인트: `POST /onl/kjb/statistics/apiStatsMonthMan.json?cmd=AGGREGATION`
- 파라미터: `targetMonth` (yyyyMM 형식, 예: 202501)
- 기본값: 전월
3. **월별 → 연별 집계**
- API 엔드포인트: `POST /onl/kjb/statistics/apiStatsYearMan.json?cmd=AGGREGATION`
- 파라미터: `targetYear` (yyyy 형식, 예: 2024)
- 기본값: 전년
**주의사항**:
- 집계 대상 기간의 원본 데이터가 존재해야 정상 집계됩니다.
- 입력값을 비워두면 기본값(전일/전월/전년)으로 실행됩니다.
- 실행 결과는 처리 건수와 함께 화면에 표시됩니다.
## 기술 스택
- JPA + QueryDSL
- MapStruct (Entity ↔ UI 매핑)
- Spring MVC (ResponseEntity)
- jqGrid (목록 화면)
- Apache POI (Excel 내보내기)
@@ -0,0 +1,164 @@
package com.eactive.ext.kjb.statistics.job;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.List;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonth;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonthService;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayService;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsMonth;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
/**
* 일별 통계를 월별 통계로 집계하는 배치 작업
* API_STATS_DAY → API_STATS_MONTH
* 실행 주기: 매월 1일 00:20 (전월 데이터 집계)
*/
@Component
public class DailyToMonthlyAggregationJob implements Job {
private static final Logger log = LoggerFactory.getLogger(DailyToMonthlyAggregationJob.class);
private ApiStatsDayService apiStatsDayService;
private ApiStatsMonthService apiStatsMonthService;
private DailyToMonthlyAggregationJob self;
@Autowired
public void setApiStatsDayService(ApiStatsDayService apiStatsDayService) {
this.apiStatsDayService = apiStatsDayService;
}
@Autowired
public void setApiStatsMonthService(ApiStatsMonthService apiStatsMonthService) {
this.apiStatsMonthService = apiStatsMonthService;
}
@Autowired
public void setSelf(DailyToMonthlyAggregationJob self) {
this.self = self;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
DataSourceType dataType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
DataSourceContextHolder.setDataSourceType(dataType);
try {
YearMonth targetMonth = YearMonth.now().minusMonths(1);
self.executeManual(targetMonth);
} catch (Exception e) {
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
/**
* 수동 실행 메서드 (월 지정)
*/
@Transactional
public int executeManual(YearMonth targetMonth) {
long startTime = System.currentTimeMillis();
log.info("=== 일별→월별 통계 집계 작업 시작 ===");
LocalDate startDate = targetMonth.atDay(1);
LocalDate endDate = targetMonth.atEndOfMonth();
log.info("집계 대상 기간: {} ~ {}", startDate, endDate);
List<ApiStatsMonth> aggregatedData = aggregateDailyToMonthly(startDate, endDate, targetMonth);
// 1. 기존 데이터 삭제 (QueryDSL 사용)
JPAQueryFactory queryFactory = apiStatsMonthService.getJPAQueryFactory();
QApiStatsMonth qm = QApiStatsMonth.apiStatsMonth;
String targetMonthStr = targetMonth.format(DateTimeFormatter.ofPattern("yyyyMM"));
long deletedCount = queryFactory
.delete(qm)
.where(qm.statTime.eq(targetMonthStr))
.execute();
log.info("기존 데이터 삭제: {} 건", deletedCount);
// 2. 배치 INSERT
apiStatsMonthService.saveAll(aggregatedData);
int savedCount = aggregatedData.size();
long elapsedTime = System.currentTimeMillis() - startTime;
log.info("=== 일별→월별 통계 집계 작업 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime);
return savedCount;
}
/**
* 일별 데이터를 월별로 집계
*/
private List<ApiStatsMonth> aggregateDailyToMonthly(LocalDate startDate, LocalDate endDate, YearMonth targetMonth) {
QApiStatsDay qd = QApiStatsDay.apiStatsDay;
JPAQueryFactory queryFactory = apiStatsDayService.getJPAQueryFactory();
String targetMonthStr = targetMonth.format(DateTimeFormatter.ofPattern("yyyyMM"));
List<ApiStatsMonth> aggregatedData = queryFactory
.select(Projections.bean(ApiStatsMonth.class,
qd.apiName,
qd.gwInstanceId,
qd.bizDivCode,
qd.clientId,
qd.inboundAdapter,
qd.outboundAdapter,
qd.totalCnt.sum().as("totalCnt"),
qd.successCnt.sum().as("successCnt"),
qd.timeoutCnt.sum().as("timeoutCnt"),
qd.systemErrCnt.sum().as("systemErrCnt"),
qd.bizErrCnt.sum().as("bizErrCnt"),
qd.seq900TimeoutCnt.sum().as("seq900TimeoutCnt"),
qd.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
qd.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
qd.avgRespTime.avg().castToNum(BigDecimal.class).as("avgRespTime"),
qd.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
qd.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
qd.p50RespTime.avg().castToNum(BigDecimal.class).as("p50RespTime"),
qd.p95RespTime.avg().castToNum(BigDecimal.class).as("p95RespTime")))
.from(qd)
.where(qd.statTime.goe(startDate)
.and(qd.statTime.loe(endDate)))
.groupBy(qd.apiName, qd.gwInstanceId, qd.bizDivCode, qd.clientId, qd.inboundAdapter, qd.outboundAdapter)
.fetch();
for (ApiStatsMonth statsMonth : aggregatedData) {
statsMonth.setStatTime(targetMonthStr);
if (statsMonth.getAvgRespTime() != null) {
statsMonth.setAvgRespTime(statsMonth.getAvgRespTime().setScale(3, RoundingMode.HALF_UP));
}
if (statsMonth.getP50RespTime() != null) {
statsMonth.setP50RespTime(statsMonth.getP50RespTime().setScale(3, RoundingMode.HALF_UP));
}
if (statsMonth.getP95RespTime() != null) {
statsMonth.setP95RespTime(statsMonth.getP95RespTime().setScale(3, RoundingMode.HALF_UP));
}
if (statsMonth.getMinRespTime() == null) {
statsMonth.setMinRespTime(BigDecimal.ZERO);
}
if (statsMonth.getMaxRespTime() == null) {
statsMonth.setMaxRespTime(BigDecimal.ZERO);
}
}
return aggregatedData;
}
}
@@ -0,0 +1,160 @@
package com.eactive.ext.kjb.statistics.job;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayService;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsHour;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
/**
* 시간별 통계를 일별 통계로 집계하는 배치 작업
* API_STATS_HOUR → API_STATS_DAY
* 실행 주기: 매일 00:10 (전일 데이터 집계)
*/
@Component
public class HourlyToDailyAggregationJob implements Job {
private static final Logger log = LoggerFactory.getLogger(HourlyToDailyAggregationJob.class);
private ApiStatsHourService apiStatsHourService;
private ApiStatsDayService apiStatsDayService;
private HourlyToDailyAggregationJob self;
@Autowired
public void setApiStatsHourService(ApiStatsHourService apiStatsHourService) {
this.apiStatsHourService = apiStatsHourService;
}
@Autowired
public void setApiStatsDayService(ApiStatsDayService apiStatsDayService) {
this.apiStatsDayService = apiStatsDayService;
}
@Autowired
public void setSelf(HourlyToDailyAggregationJob self) {
this.self = self;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
DataSourceType dataType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
DataSourceContextHolder.setDataSourceType(dataType);
try {
LocalDate targetDate = LocalDate.now().minusDays(1);
self.executeManual(targetDate);
} catch (Exception e) {
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
/**
* 수동 실행 메서드 (날짜 지정)
*/
@Transactional
public int executeManual(LocalDate targetDate) {
long startTime = System.currentTimeMillis();
log.info("=== 시간별→일별 통계 집계 작업 시작 ===");
LocalDateTime startDateTime = targetDate.atStartOfDay();
LocalDateTime endDateTime = targetDate.plusDays(1).atStartOfDay().minusSeconds(1);
log.info("집계 대상 기간: {} ~ {}", startDateTime, endDateTime);
List<ApiStatsDay> aggregatedData = aggregateHourlyToDaily(startDateTime, endDateTime, targetDate);
// 1. 기존 데이터 삭제 (QueryDSL 사용)
JPAQueryFactory queryFactory = apiStatsDayService.getJPAQueryFactory();
QApiStatsDay qd = QApiStatsDay.apiStatsDay;
long deletedCount = queryFactory
.delete(qd)
.where(qd.statTime.eq(targetDate))
.execute();
log.info("기존 데이터 삭제: {} 건", deletedCount);
// 2. 배치 INSERT
apiStatsDayService.saveAll(aggregatedData);
int savedCount = aggregatedData.size();
long elapsedTime = System.currentTimeMillis() - startTime;
log.info("=== 시간별→일별 통계 집계 작업 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime);
return savedCount;
}
/**
* 시간별 데이터를 일별로 집계
*/
private List<ApiStatsDay> aggregateHourlyToDaily(LocalDateTime startDateTime, LocalDateTime endDateTime, LocalDate targetDate) {
QApiStatsHour qh = QApiStatsHour.apiStatsHour;
JPAQueryFactory queryFactory = apiStatsHourService.getJPAQueryFactory();
List<ApiStatsDay> aggregatedData = queryFactory
.select(Projections.bean(ApiStatsDay.class,
qh.apiName,
qh.gwInstanceId,
qh.bizDivCode,
qh.clientId,
qh.inboundAdapter,
qh.outboundAdapter,
qh.totalCnt.sum().as("totalCnt"),
qh.successCnt.sum().as("successCnt"),
qh.timeoutCnt.sum().as("timeoutCnt"),
qh.systemErrCnt.sum().as("systemErrCnt"),
qh.bizErrCnt.sum().as("bizErrCnt"),
qh.seq900TimeoutCnt.sum().as("seq900TimeoutCnt"),
qh.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
qh.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
qh.avgRespTime.avg().castToNum(BigDecimal.class).as("avgRespTime"),
qh.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
qh.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
qh.p50RespTime.avg().castToNum(BigDecimal.class).as("p50RespTime"),
qh.p95RespTime.avg().castToNum(BigDecimal.class).as("p95RespTime")))
.from(qh)
.where(qh.statTime.goe(startDateTime)
.and(qh.statTime.loe(endDateTime)))
.groupBy(qh.apiName, qh.gwInstanceId, qh.bizDivCode, qh.clientId, qh.inboundAdapter, qh.outboundAdapter)
.fetch();
for (ApiStatsDay statsDay : aggregatedData) {
statsDay.setStatTime(targetDate);
if (statsDay.getAvgRespTime() != null) {
statsDay.setAvgRespTime(statsDay.getAvgRespTime().setScale(3, RoundingMode.HALF_UP));
}
if (statsDay.getP50RespTime() != null) {
statsDay.setP50RespTime(statsDay.getP50RespTime().setScale(3, RoundingMode.HALF_UP));
}
if (statsDay.getP95RespTime() != null) {
statsDay.setP95RespTime(statsDay.getP95RespTime().setScale(3, RoundingMode.HALF_UP));
}
if (statsDay.getMinRespTime() == null) {
statsDay.setMinRespTime(BigDecimal.ZERO);
}
if (statsDay.getMaxRespTime() == null) {
statsDay.setMaxRespTime(BigDecimal.ZERO);
}
}
return aggregatedData;
}
}
@@ -0,0 +1,162 @@
package com.eactive.ext.kjb.statistics.job;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Year;
import java.util.List;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYear;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYearService;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonthService;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsMonth;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsYear;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
/**
* 월별 통계를 연별 통계로 집계하는 배치 작업
* API_STATS_MONTH → API_STATS_YEAR
* 실행 주기: 매년 1월 1일 00:30 (전년 데이터 집계)
*/
@Component
public class MonthlyToYearlyAggregationJob implements Job {
private static final Logger log = LoggerFactory.getLogger(MonthlyToYearlyAggregationJob.class);
private ApiStatsMonthService apiStatsMonthService;
private ApiStatsYearService apiStatsYearService;
private MonthlyToYearlyAggregationJob self;
@Autowired
public void setApiStatsMonthService(ApiStatsMonthService apiStatsMonthService) {
this.apiStatsMonthService = apiStatsMonthService;
}
@Autowired
public void setApiStatsYearService(ApiStatsYearService apiStatsYearService) {
this.apiStatsYearService = apiStatsYearService;
}
@Autowired
public void setSelf(MonthlyToYearlyAggregationJob self) {
this.self = self;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
DataSourceType dataType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
DataSourceContextHolder.setDataSourceType(dataType);
try {
Year targetYear = Year.now().minusYears(1);
self.executeManual(targetYear);
} catch (Exception e) {
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
/**
* 수동 실행 메서드 (연도 지정)
*/
@Transactional
public int executeManual(Year targetYear) {
long startTime = System.currentTimeMillis();
log.info("=== 월별→연별 통계 집계 작업 시작 ===");
String startMonth = targetYear.toString() + "01";
String endMonth = targetYear.toString() + "12";
log.info("집계 대상 기간: {} ~ {}", startMonth, endMonth);
List<ApiStatsYear> aggregatedData = aggregateMonthlyToYearly(startMonth, endMonth, targetYear);
// 1. 기존 데이터 삭제 (QueryDSL 사용)
JPAQueryFactory queryFactory = apiStatsYearService.getJPAQueryFactory();
QApiStatsYear qy = QApiStatsYear.apiStatsYear;
String targetYearStr = targetYear.toString();
long deletedCount = queryFactory
.delete(qy)
.where(qy.statTime.eq(targetYearStr))
.execute();
log.info("기존 데이터 삭제: {} 건", deletedCount);
// 2. 배치 INSERT
apiStatsYearService.saveAll(aggregatedData);
int savedCount = aggregatedData.size();
long elapsedTime = System.currentTimeMillis() - startTime;
log.info("=== 월별→연별 통계 집계 작업 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime);
return savedCount;
}
/**
* 월별 데이터를 연별로 집계
*/
private List<ApiStatsYear> aggregateMonthlyToYearly(String startMonth, String endMonth, Year targetYear) {
QApiStatsMonth qm = QApiStatsMonth.apiStatsMonth;
JPAQueryFactory queryFactory = apiStatsMonthService.getJPAQueryFactory();
String targetYearStr = targetYear.toString();
List<ApiStatsYear> aggregatedData = queryFactory
.select(Projections.bean(ApiStatsYear.class,
qm.apiName,
qm.gwInstanceId,
qm.bizDivCode,
qm.clientId,
qm.inboundAdapter,
qm.outboundAdapter,
qm.totalCnt.sum().as("totalCnt"),
qm.successCnt.sum().as("successCnt"),
qm.timeoutCnt.sum().as("timeoutCnt"),
qm.systemErrCnt.sum().as("systemErrCnt"),
qm.bizErrCnt.sum().as("bizErrCnt"),
qm.seq900TimeoutCnt.sum().as("seq900TimeoutCnt"),
qm.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
qm.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
qm.avgRespTime.avg().castToNum(BigDecimal.class).as("avgRespTime"),
qm.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
qm.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
qm.p50RespTime.avg().castToNum(BigDecimal.class).as("p50RespTime"),
qm.p95RespTime.avg().castToNum(BigDecimal.class).as("p95RespTime")))
.from(qm)
.where(qm.statTime.goe(startMonth)
.and(qm.statTime.loe(endMonth)))
.groupBy(qm.apiName, qm.gwInstanceId, qm.bizDivCode, qm.clientId, qm.inboundAdapter, qm.outboundAdapter)
.fetch();
for (ApiStatsYear statsYear : aggregatedData) {
statsYear.setStatTime(targetYearStr);
if (statsYear.getAvgRespTime() != null) {
statsYear.setAvgRespTime(statsYear.getAvgRespTime().setScale(3, RoundingMode.HALF_UP));
}
if (statsYear.getP50RespTime() != null) {
statsYear.setP50RespTime(statsYear.getP50RespTime().setScale(3, RoundingMode.HALF_UP));
}
if (statsYear.getP95RespTime() != null) {
statsYear.setP95RespTime(statsYear.getP95RespTime().setScale(3, RoundingMode.HALF_UP));
}
if (statsYear.getMinRespTime() == null) {
statsYear.setMinRespTime(BigDecimal.ZERO);
}
if (statsYear.getMaxRespTime() == null) {
statsYear.setMaxRespTime(BigDecimal.ZERO);
}
}
return aggregatedData;
}
}
@@ -1,5 +1,6 @@
package com.eactive.ext.kjb.statistics.mapping; package com.eactive.ext.kjb.statistics.mapping;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@@ -8,8 +9,11 @@ import org.mapstruct.Mapping;
import org.mapstruct.Named; import org.mapstruct.Named;
import com.eactive.eai.data.mapper.BaseMapperConfig; import com.eactive.eai.data.mapper.BaseMapperConfig;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHour; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHour;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinute; import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMinute;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsMonth;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsYear;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
@Mapper(config = BaseMapperConfig.class) @Mapper(config = BaseMapperConfig.class)
@@ -17,6 +21,7 @@ public interface ApiStatsUIMapper {
DateTimeFormatter MINUTE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); DateTimeFormatter MINUTE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
DateTimeFormatter HOUR_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00"); DateTimeFormatter HOUR_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00");
DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatMinuteTime") @Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatMinuteTime")
ApiStatsUI toVo(ApiStatsMinute entity); ApiStatsUI toVo(ApiStatsMinute entity);
@@ -24,6 +29,15 @@ public interface ApiStatsUIMapper {
@Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatHourTime") @Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatHourTime")
ApiStatsUI toVo(ApiStatsHour entity); ApiStatsUI toVo(ApiStatsHour entity);
@Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatDayTime")
ApiStatsUI toVo(ApiStatsDay entity);
@Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatMonthTime")
ApiStatsUI toVo(ApiStatsMonth entity);
@Mapping(source = "statTime", target = "statTime", qualifiedByName = "formatYearTime")
ApiStatsUI toVo(ApiStatsYear entity);
@Named("formatMinuteTime") @Named("formatMinuteTime")
default String formatMinuteTime(LocalDateTime dateTime) { default String formatMinuteTime(LocalDateTime dateTime) {
if (dateTime == null) { if (dateTime == null) {
@@ -39,4 +53,25 @@ public interface ApiStatsUIMapper {
} }
return dateTime.format(HOUR_FORMATTER); return dateTime.format(HOUR_FORMATTER);
} }
@Named("formatDayTime")
default String formatDayTime(LocalDate date) {
if (date == null) {
return null;
}
return date.format(DAY_FORMATTER);
}
@Named("formatMonthTime")
default String formatMonthTime(String yyyymm) {
if (yyyymm == null || yyyymm.length() != 6) {
return yyyymm;
}
return yyyymm.substring(0, 4) + "-" + yyyymm.substring(4, 6);
}
@Named("formatYearTime")
default String formatYearTime(String yyyy) {
return yyyy;
}
} }
@@ -0,0 +1,274 @@
package com.eactive.ext.kjb.statistics.service;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
import lombok.extern.slf4j.Slf4j;
/**
* API 통계 Excel Export 서비스
* Hour/Minute 통계 데이터를 Excel 파일로 생성
*/
@Slf4j
@Service
public class ApiStatsExcelExportService {
private static final String[] COLUMN_HEADERS = {
"통계시간", "API명", "인스턴스", "업무구분", "클라이언트ID",
"Inbound Adapter", "Outbound Adapter",
"총건수", "성공건수", "Timeout건수", "시스템오류건수", "업무오류건수",
"Seq900 Timeout", "Seq900 시스템오류", "Seq900 업무오류",
"평균응답시간", "최소응답시간", "최대응답시간", "P50응답시간", "P95응답시간"
};
/**
* API 통계 데이터를 Excel 파일로 생성하여 HTTP 응답으로 전송
*
* @param dataList API 통계 데이터 리스트
* @param fileName 다운로드 파일명
* @param response HTTP 응답 객체
* @throws IOException 파일 생성 실패 시
*/
public void exportApiStats(List<ApiStatsUI> dataList, String fileName, HttpServletResponse response)
throws IOException {
log.info("Starting Excel export - rows: {}, filename: {}", dataList.size(), fileName);
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("API통계");
log.debug("Excel sheet created");
// 스타일 생성
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle stringStyle = createStringStyle(workbook);
CellStyle numberStyle = createNumberStyle(workbook);
CellStyle decimalStyle = createDecimalStyle(workbook);
log.debug("Excel styles created");
// 헤더 행 생성
createHeaderRow(sheet, headerStyle);
log.debug("Header row created");
// 데이터 행 생성
int rowNum = 1;
for (ApiStatsUI data : dataList) {
createDataRow(sheet, rowNum++, data, stringStyle, numberStyle, decimalStyle);
}
log.info("Data rows created - count: {}", dataList.size());
// 컬럼 너비 자동 조정
autoSizeColumns(sheet);
log.debug("Column widths adjusted");
// HTTP 응답 설정
setHttpResponse(response, fileName, workbook);
log.info("Excel file sent to response");
} catch (Exception e) {
log.error("Error creating Excel file", e);
throw new IOException("Excel file creation failed: " + e.getMessage(), e);
}
}
/**
* 헤더 스타일 생성 (회색 배경, 볼드, 테두리)
*/
private CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
// 배경색
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 테두리
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
// 정렬
style.setAlignment(HorizontalAlignment.CENTER);
// 폰트 (볼드)
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
return style;
}
/**
* 문자열 데이터 스타일 생성 (기본 테두리)
*/
private CellStyle createStringStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
return style;
}
/**
* 숫자 데이터 스타일 생성 (우측정렬 + 쉼표 구분)
*/
private CellStyle createNumberStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.RIGHT);
style.setDataFormat(workbook.createDataFormat().getFormat("#,##0"));
return style;
}
/**
* 소수점 데이터 스타일 생성 (우측정렬 + 소수점 3자리)
*/
private CellStyle createDecimalStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.RIGHT);
style.setDataFormat(workbook.createDataFormat().getFormat("0.000"));
return style;
}
/**
* 헤더 행 생성
*/
private void createHeaderRow(Sheet sheet, CellStyle headerStyle) {
Row headerRow = sheet.createRow(0);
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(COLUMN_HEADERS[i]);
cell.setCellStyle(headerStyle);
}
}
/**
* 데이터 행 생성
*/
private void createDataRow(Sheet sheet, int rowNum, ApiStatsUI data,
CellStyle stringStyle, CellStyle numberStyle, CellStyle decimalStyle) {
Row row = sheet.createRow(rowNum);
int colNum = 0;
// 문자열 컬럼 (0-6)
createStringCell(row, colNum++, data.getStatTime(), stringStyle);
createStringCell(row, colNum++, data.getApiName(), stringStyle);
createStringCell(row, colNum++, data.getGwInstanceId(), stringStyle);
createStringCell(row, colNum++, data.getBizDivCode(), stringStyle);
createStringCell(row, colNum++, data.getClientId(), stringStyle);
createStringCell(row, colNum++, data.getInboundAdapter(), stringStyle);
createStringCell(row, colNum++, data.getOutboundAdapter(), stringStyle);
// 숫자 컬럼 (7-14)
createLongCell(row, colNum++, data.getTotalCnt(), numberStyle);
createLongCell(row, colNum++, data.getSuccessCnt(), numberStyle);
createLongCell(row, colNum++, data.getTimeoutCnt(), numberStyle);
createLongCell(row, colNum++, data.getSystemErrCnt(), numberStyle);
createLongCell(row, colNum++, data.getBizErrCnt(), numberStyle);
createLongCell(row, colNum++, data.getSeq900TimeoutCnt(), numberStyle);
createLongCell(row, colNum++, data.getSeq900SystemErrCnt(), numberStyle);
createLongCell(row, colNum++, data.getSeq900BizErrCnt(), numberStyle);
// 소수점 컬럼 (15-19)
createDecimalCell(row, colNum++, data.getAvgRespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getMinRespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getMaxRespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getP50RespTime(), decimalStyle);
createDecimalCell(row, colNum++, data.getP95RespTime(), decimalStyle);
}
/**
* 문자열 셀 생성
*/
private void createStringCell(Row row, int colNum, String value, CellStyle style) {
Cell cell = row.createCell(colNum);
cell.setCellValue(value != null ? value : "");
cell.setCellStyle(style);
}
/**
* Long 타입 숫자 셀 생성
*/
private void createLongCell(Row row, int colNum, Long value, CellStyle style) {
Cell cell = row.createCell(colNum);
if (value != null) {
cell.setCellValue(value.doubleValue());
} else {
cell.setCellValue(0);
}
cell.setCellStyle(style);
}
/**
* BigDecimal 타입 소수점 셀 생성
*/
private void createDecimalCell(Row row, int colNum, BigDecimal value, CellStyle style) {
Cell cell = row.createCell(colNum);
if (value != null) {
cell.setCellValue(value.doubleValue());
} else {
cell.setCellValue(0.0);
}
cell.setCellStyle(style);
}
/**
* 컬럼 너비 자동 조정
*/
private void autoSizeColumns(Sheet sheet) {
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
sheet.autoSizeColumn(i);
// 한글 문자 고려하여 약간 여유 공간 추가
sheet.setColumnWidth(i, sheet.getColumnWidth(i) + 512);
}
}
/**
* HTTP 응답 설정 및 파일 전송
*/
private void setHttpResponse(HttpServletResponse response, String fileName, Workbook workbook)
throws IOException {
// Content-Type 설정
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// Content-Disposition 설정 (파일명 UTF-8 인코딩)
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString())
.replaceAll("\\+", "%20");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
// Workbook을 응답 스트림으로 전송
workbook.write(response.getOutputStream());
response.getOutputStream().flush();
}
}
@@ -167,3 +167,225 @@ COMMENT ON COLUMN API_STATS_HOUR.P95_RESP_TIME IS '95 백분위 응답시간(ms)
--- ---
## 일별 통계 테이블 DDL
```sql
-- API Gateway 일별 통계 테이블
CREATE TABLE API_STATS_DAY (
STAT_TIME DATE NOT NULL, -- 통계 시간 (일 단위)
API_NAME VARCHAR2(100) NOT NULL, -- API명
GW_INSTANCE_ID VARCHAR2(50) NOT NULL, -- API Gateway 인스턴스 ID
BIZ_DIV_CODE VARCHAR2(50) DEFAULT 'NONE' NOT NULL, -- 업무구분코드
CLIENT_ID VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- 클라이언트 ID
INBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Inbound Adapter
OUTBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Outbound Adapter
-- 건수 메트릭
TOTAL_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SUCCESS_CNT NUMBER(12) DEFAULT 0 NOT NULL,
TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
-- 로그 시퀀스 900 구간 에러 카운트
SEQ900_TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SEQ900_SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SEQ900_BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
-- 응답시간 메트릭 (ms, 소수점 3자리)
AVG_RESP_TIME NUMBER(10,3) DEFAULT 0,
MIN_RESP_TIME NUMBER(10,3) DEFAULT 0,
MAX_RESP_TIME NUMBER(10,3) DEFAULT 0,
P50_RESP_TIME NUMBER(10,3) DEFAULT 0,
P95_RESP_TIME NUMBER(10,3) DEFAULT 0,
CONSTRAINT PK_API_STATS_DAY PRIMARY KEY (
STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID, INBOUND_ADAPTER, OUTBOUND_ADAPTER
)
);
-- 코멘트
COMMENT ON TABLE API_STATS_DAY IS 'API Gateway 일별 처리 통계';
COMMENT ON COLUMN API_STATS_DAY.STAT_TIME IS '통계시간(일단위)';
COMMENT ON COLUMN API_STATS_DAY.API_NAME IS 'API 명칭';
COMMENT ON COLUMN API_STATS_DAY.GW_INSTANCE_ID IS 'API Gateway 인스턴스 ID';
COMMENT ON COLUMN API_STATS_DAY.BIZ_DIV_CODE IS '업무구분코드 (없으면 NONE)';
COMMENT ON COLUMN API_STATS_DAY.CLIENT_ID IS '클라이언트 ID (없으면 NONE)';
COMMENT ON COLUMN API_STATS_DAY.INBOUND_ADAPTER IS 'Inbound Adapter (없으면 NONE)';
COMMENT ON COLUMN API_STATS_DAY.OUTBOUND_ADAPTER IS 'Outbound Adapter (없으면 NONE)';
COMMENT ON COLUMN API_STATS_DAY.TOTAL_CNT IS '총 처리 건수';
COMMENT ON COLUMN API_STATS_DAY.SUCCESS_CNT IS '성공 건수';
COMMENT ON COLUMN API_STATS_DAY.TIMEOUT_CNT IS 'Timeout 오류 건수';
COMMENT ON COLUMN API_STATS_DAY.SYSTEM_ERR_CNT IS '시스템 오류 건수';
COMMENT ON COLUMN API_STATS_DAY.BIZ_ERR_CNT IS '업무 오류 건수';
COMMENT ON COLUMN API_STATS_DAY.SEQ900_TIMEOUT_CNT IS '로그시퀀스900 Timeout 오류 건수';
COMMENT ON COLUMN API_STATS_DAY.SEQ900_SYSTEM_ERR_CNT IS '로그시퀀스900 시스템 오류 건수';
COMMENT ON COLUMN API_STATS_DAY.SEQ900_BIZ_ERR_CNT IS '로그시퀀스900 업무 오류 건수';
COMMENT ON COLUMN API_STATS_DAY.AVG_RESP_TIME IS '평균 응답시간(ms)';
COMMENT ON COLUMN API_STATS_DAY.MIN_RESP_TIME IS '최소 응답시간(ms)';
COMMENT ON COLUMN API_STATS_DAY.MAX_RESP_TIME IS '최대 응답시간(ms)';
COMMENT ON COLUMN API_STATS_DAY.P50_RESP_TIME IS '50 백분위 응답시간(ms)';
COMMENT ON COLUMN API_STATS_DAY.P95_RESP_TIME IS '95 백분위 응답시간(ms)';
```
---
## 월별 통계 테이블 DDL
```sql
-- API Gateway 월별 통계 테이블
CREATE TABLE API_STATS_MONTH (
STAT_TIME VARCHAR2(6) NOT NULL, -- 통계 시간 (YYYYMM 형식)
API_NAME VARCHAR2(100) NOT NULL, -- API명
GW_INSTANCE_ID VARCHAR2(50) NOT NULL, -- API Gateway 인스턴스 ID
BIZ_DIV_CODE VARCHAR2(50) DEFAULT 'NONE' NOT NULL, -- 업무구분코드
CLIENT_ID VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- 클라이언트 ID
INBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Inbound Adapter
OUTBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Outbound Adapter
-- 건수 메트릭
TOTAL_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SUCCESS_CNT NUMBER(12) DEFAULT 0 NOT NULL,
TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
-- 로그 시퀀스 900 구간 에러 카운트
SEQ900_TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SEQ900_SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SEQ900_BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
-- 응답시간 메트릭 (ms, 소수점 3자리)
AVG_RESP_TIME NUMBER(10,3) DEFAULT 0,
MIN_RESP_TIME NUMBER(10,3) DEFAULT 0,
MAX_RESP_TIME NUMBER(10,3) DEFAULT 0,
P50_RESP_TIME NUMBER(10,3) DEFAULT 0,
P95_RESP_TIME NUMBER(10,3) DEFAULT 0,
CONSTRAINT PK_API_STATS_MONTH PRIMARY KEY (
STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID, INBOUND_ADAPTER, OUTBOUND_ADAPTER
)
);
-- 코멘트
COMMENT ON TABLE API_STATS_MONTH IS 'API Gateway 월별 처리 통계';
COMMENT ON COLUMN API_STATS_MONTH.STAT_TIME IS '통계시간(월단위, YYYYMM 형식)';
COMMENT ON COLUMN API_STATS_MONTH.API_NAME IS 'API 명칭';
COMMENT ON COLUMN API_STATS_MONTH.GW_INSTANCE_ID IS 'API Gateway 인스턴스 ID';
COMMENT ON COLUMN API_STATS_MONTH.BIZ_DIV_CODE IS '업무구분코드 (없으면 NONE)';
COMMENT ON COLUMN API_STATS_MONTH.CLIENT_ID IS '클라이언트 ID (없으면 NONE)';
COMMENT ON COLUMN API_STATS_MONTH.INBOUND_ADAPTER IS 'Inbound Adapter (없으면 NONE)';
COMMENT ON COLUMN API_STATS_MONTH.OUTBOUND_ADAPTER IS 'Outbound Adapter (없으면 NONE)';
COMMENT ON COLUMN API_STATS_MONTH.TOTAL_CNT IS '총 처리 건수';
COMMENT ON COLUMN API_STATS_MONTH.SUCCESS_CNT IS '성공 건수';
COMMENT ON COLUMN API_STATS_MONTH.TIMEOUT_CNT IS 'Timeout 오류 건수';
COMMENT ON COLUMN API_STATS_MONTH.SYSTEM_ERR_CNT IS '시스템 오류 건수';
COMMENT ON COLUMN API_STATS_MONTH.BIZ_ERR_CNT IS '업무 오류 건수';
COMMENT ON COLUMN API_STATS_MONTH.SEQ900_TIMEOUT_CNT IS '로그시퀀스900 Timeout 오류 건수';
COMMENT ON COLUMN API_STATS_MONTH.SEQ900_SYSTEM_ERR_CNT IS '로그시퀀스900 시스템 오류 건수';
COMMENT ON COLUMN API_STATS_MONTH.SEQ900_BIZ_ERR_CNT IS '로그시퀀스900 업무 오류 건수';
COMMENT ON COLUMN API_STATS_MONTH.AVG_RESP_TIME IS '평균 응답시간(ms)';
COMMENT ON COLUMN API_STATS_MONTH.MIN_RESP_TIME IS '최소 응답시간(ms)';
COMMENT ON COLUMN API_STATS_MONTH.MAX_RESP_TIME IS '최대 응답시간(ms)';
COMMENT ON COLUMN API_STATS_MONTH.P50_RESP_TIME IS '50 백분위 응답시간(ms)';
COMMENT ON COLUMN API_STATS_MONTH.P95_RESP_TIME IS '95 백분위 응답시간(ms)';
```
---
## 연도별 통계 테이블 DDL
```sql
-- API Gateway 연도별 통계 테이블
CREATE TABLE API_STATS_YEAR (
STAT_TIME VARCHAR2(4) NOT NULL, -- 통계 시간 (YYYY 형식)
API_NAME VARCHAR2(100) NOT NULL, -- API명
GW_INSTANCE_ID VARCHAR2(50) NOT NULL, -- API Gateway 인스턴스 ID
BIZ_DIV_CODE VARCHAR2(50) DEFAULT 'NONE' NOT NULL, -- 업무구분코드
CLIENT_ID VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- 클라이언트 ID
INBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Inbound Adapter
OUTBOUND_ADAPTER VARCHAR2(100) DEFAULT 'NONE' NOT NULL, -- Outbound Adapter
-- 건수 메트릭
TOTAL_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SUCCESS_CNT NUMBER(12) DEFAULT 0 NOT NULL,
TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
-- 로그 시퀀스 900 구간 에러 카운트
SEQ900_TIMEOUT_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SEQ900_SYSTEM_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
SEQ900_BIZ_ERR_CNT NUMBER(12) DEFAULT 0 NOT NULL,
-- 응답시간 메트릭 (ms, 소수점 3자리)
AVG_RESP_TIME NUMBER(10,3) DEFAULT 0,
MIN_RESP_TIME NUMBER(10,3) DEFAULT 0,
MAX_RESP_TIME NUMBER(10,3) DEFAULT 0,
P50_RESP_TIME NUMBER(10,3) DEFAULT 0,
P95_RESP_TIME NUMBER(10,3) DEFAULT 0,
CONSTRAINT PK_API_STATS_YEAR PRIMARY KEY (
STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID, INBOUND_ADAPTER, OUTBOUND_ADAPTER
)
);
-- 코멘트
COMMENT ON TABLE API_STATS_YEAR IS 'API Gateway 연도별 처리 통계';
COMMENT ON COLUMN API_STATS_YEAR.STAT_TIME IS '통계시간(연도단위, YYYY 형식)';
COMMENT ON COLUMN API_STATS_YEAR.API_NAME IS 'API 명칭';
COMMENT ON COLUMN API_STATS_YEAR.GW_INSTANCE_ID IS 'API Gateway 인스턴스 ID';
COMMENT ON COLUMN API_STATS_YEAR.BIZ_DIV_CODE IS '업무구분코드 (없으면 NONE)';
COMMENT ON COLUMN API_STATS_YEAR.CLIENT_ID IS '클라이언트 ID (없으면 NONE)';
COMMENT ON COLUMN API_STATS_YEAR.INBOUND_ADAPTER IS 'Inbound Adapter (없으면 NONE)';
COMMENT ON COLUMN API_STATS_YEAR.OUTBOUND_ADAPTER IS 'Outbound Adapter (없으면 NONE)';
COMMENT ON COLUMN API_STATS_YEAR.TOTAL_CNT IS '총 처리 건수';
COMMENT ON COLUMN API_STATS_YEAR.SUCCESS_CNT IS '성공 건수';
COMMENT ON COLUMN API_STATS_YEAR.TIMEOUT_CNT IS 'Timeout 오류 건수';
COMMENT ON COLUMN API_STATS_YEAR.SYSTEM_ERR_CNT IS '시스템 오류 건수';
COMMENT ON COLUMN API_STATS_YEAR.BIZ_ERR_CNT IS '업무 오류 건수';
COMMENT ON COLUMN API_STATS_YEAR.SEQ900_TIMEOUT_CNT IS '로그시퀀스900 Timeout 오류 건수';
COMMENT ON COLUMN API_STATS_YEAR.SEQ900_SYSTEM_ERR_CNT IS '로그시퀀스900 시스템 오류 건수';
COMMENT ON COLUMN API_STATS_YEAR.SEQ900_BIZ_ERR_CNT IS '로그시퀀스900 업무 오류 건수';
COMMENT ON COLUMN API_STATS_YEAR.AVG_RESP_TIME IS '평균 응답시간(ms)';
COMMENT ON COLUMN API_STATS_YEAR.MIN_RESP_TIME IS '최소 응답시간(ms)';
COMMENT ON COLUMN API_STATS_YEAR.MAX_RESP_TIME IS '최대 응답시간(ms)';
COMMENT ON COLUMN API_STATS_YEAR.P50_RESP_TIME IS '50 백분위 응답시간(ms)';
COMMENT ON COLUMN API_STATS_YEAR.P95_RESP_TIME IS '95 백분위 응답시간(ms)';
```
---
## 샘플 데이터 (테스트용)
```sql
-- 월별 통계 샘플 데이터
INSERT INTO API_STATS_MONTH (
STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID,
INBOUND_ADAPTER, OUTBOUND_ADAPTER, TOTAL_CNT, SUCCESS_CNT,
TIMEOUT_CNT, SYSTEM_ERR_CNT, BIZ_ERR_CNT,
SEQ900_TIMEOUT_CNT, SEQ900_SYSTEM_ERR_CNT, SEQ900_BIZ_ERR_CNT,
AVG_RESP_TIME, MIN_RESP_TIME, MAX_RESP_TIME, P50_RESP_TIME, P95_RESP_TIME
) VALUES (
'202512', 'TEST_API', 'GW01', 'MOBILE', 'CLIENT01',
'HTTP', 'REST', 30000, 29400, 300, 150, 150, 60, 30, 60,
120.500, 8.000, 600.000, 95.000, 280.000
);
-- 연도별 통계 샘플 데이터
INSERT INTO API_STATS_YEAR (
STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID,
INBOUND_ADAPTER, OUTBOUND_ADAPTER, TOTAL_CNT, SUCCESS_CNT,
TIMEOUT_CNT, SYSTEM_ERR_CNT, BIZ_ERR_CNT,
SEQ900_TIMEOUT_CNT, SEQ900_SYSTEM_ERR_CNT, SEQ900_BIZ_ERR_CNT,
AVG_RESP_TIME, MIN_RESP_TIME, MAX_RESP_TIME, P50_RESP_TIME, P95_RESP_TIME
) VALUES (
'2025', 'TEST_API', 'GW01', 'MOBILE', 'CLIENT01',
'HTTP', 'REST', 365000, 357700, 3650, 1825, 1825, 730, 365, 730,
118.750, 5.000, 700.000, 90.000, 270.000
);
COMMIT;
```
---
@@ -1,6 +1,7 @@
package com.eactive.ext.kjb.statistics.ui; package com.eactive.ext.kjb.statistics.ui;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@@ -21,22 +22,60 @@ public class ApiStatsChartUI {
private Long timeoutCnt; private Long timeoutCnt;
private Long systemErrCnt; private Long systemErrCnt;
private Long bizErrCnt; private Long bizErrCnt;
private Long seq900TimeoutCnt;
private Long seq900SystemErrCnt;
private Long seq900BizErrCnt;
// 응답시간 메트릭 (AVG) // 응답시간 메트릭 (AVG)
private BigDecimal avgRespTime; private BigDecimal avgRespTime;
private BigDecimal p50RespTime; private BigDecimal p50RespTime;
private BigDecimal p95RespTime; private BigDecimal p95RespTime;
// QueryDSL Projections용 생성자 // QueryDSL Projections용 생성자 - Hour/Minute (LocalDateTime)
public ApiStatsChartUI(LocalDateTime statTime, Long totalCnt, Long successCnt, public ApiStatsChartUI(LocalDateTime statTime, Long totalCnt, Long successCnt,
Long timeoutCnt, Long systemErrCnt, Long bizErrCnt, Long timeoutCnt, Long systemErrCnt, Long bizErrCnt,
Long seq900TimeoutCnt, Long seq900SystemErrCnt, Long seq900BizErrCnt,
Double avgRespTime, Double p50RespTime, Double p95RespTime) { Double avgRespTime, Double p50RespTime, Double p95RespTime) {
this.statTime = statTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")); this.statTime = statTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
setMetrics(totalCnt, successCnt, timeoutCnt, systemErrCnt, bizErrCnt,
seq900TimeoutCnt, seq900SystemErrCnt, seq900BizErrCnt,
avgRespTime, p50RespTime, p95RespTime);
}
// QueryDSL Projections용 생성자 - Day (LocalDate)
public ApiStatsChartUI(LocalDate statTime, Long totalCnt, Long successCnt,
Long timeoutCnt, Long systemErrCnt, Long bizErrCnt,
Long seq900TimeoutCnt, Long seq900SystemErrCnt, Long seq900BizErrCnt,
Double avgRespTime, Double p50RespTime, Double p95RespTime) {
this.statTime = statTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
setMetrics(totalCnt, successCnt, timeoutCnt, systemErrCnt, bizErrCnt,
seq900TimeoutCnt, seq900SystemErrCnt, seq900BizErrCnt,
avgRespTime, p50RespTime, p95RespTime);
}
// QueryDSL Projections용 생성자 - Month/Year (String)
public ApiStatsChartUI(String statTime, Long totalCnt, Long successCnt,
Long timeoutCnt, Long systemErrCnt, Long bizErrCnt,
Long seq900TimeoutCnt, Long seq900SystemErrCnt, Long seq900BizErrCnt,
Double avgRespTime, Double p50RespTime, Double p95RespTime) {
this.statTime = statTime;
setMetrics(totalCnt, successCnt, timeoutCnt, systemErrCnt, bizErrCnt,
seq900TimeoutCnt, seq900SystemErrCnt, seq900BizErrCnt,
avgRespTime, p50RespTime, p95RespTime);
}
private void setMetrics(Long totalCnt, Long successCnt, Long timeoutCnt,
Long systemErrCnt, Long bizErrCnt, Long seq900TimeoutCnt,
Long seq900SystemErrCnt, Long seq900BizErrCnt,
Double avgRespTime, Double p50RespTime, Double p95RespTime) {
this.totalCnt = totalCnt != null ? totalCnt : 0L; this.totalCnt = totalCnt != null ? totalCnt : 0L;
this.successCnt = successCnt != null ? successCnt : 0L; this.successCnt = successCnt != null ? successCnt : 0L;
this.timeoutCnt = timeoutCnt != null ? timeoutCnt : 0L; this.timeoutCnt = timeoutCnt != null ? timeoutCnt : 0L;
this.systemErrCnt = systemErrCnt != null ? systemErrCnt : 0L; this.systemErrCnt = systemErrCnt != null ? systemErrCnt : 0L;
this.bizErrCnt = bizErrCnt != null ? bizErrCnt : 0L; this.bizErrCnt = bizErrCnt != null ? bizErrCnt : 0L;
this.seq900TimeoutCnt = seq900TimeoutCnt != null ? seq900TimeoutCnt : 0L;
this.seq900SystemErrCnt = seq900SystemErrCnt != null ? seq900SystemErrCnt : 0L;
this.seq900BizErrCnt = seq900BizErrCnt != null ? seq900BizErrCnt : 0L;
this.avgRespTime = avgRespTime != null ? BigDecimal.valueOf(avgRespTime) : BigDecimal.ZERO; this.avgRespTime = avgRespTime != null ? BigDecimal.valueOf(avgRespTime) : BigDecimal.ZERO;
this.p50RespTime = p50RespTime != null ? BigDecimal.valueOf(p50RespTime) : BigDecimal.ZERO; this.p50RespTime = p50RespTime != null ? BigDecimal.valueOf(p50RespTime) : BigDecimal.ZERO;
this.p95RespTime = p95RespTime != null ? BigDecimal.valueOf(p95RespTime) : BigDecimal.ZERO; this.p95RespTime = p95RespTime != null ? BigDecimal.valueOf(p95RespTime) : BigDecimal.ZERO;
@@ -1,5 +1,6 @@
package com.eactive.ext.kjb.statistics.ui; package com.eactive.ext.kjb.statistics.ui;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import lombok.Data; import lombok.Data;
@@ -22,4 +23,19 @@ public class ApiStatsSearch {
private LocalDateTime parsedStartDateTime; private LocalDateTime parsedStartDateTime;
/** 파싱된 종료시간 */ /** 파싱된 종료시간 */
private LocalDateTime parsedEndDateTime; private LocalDateTime parsedEndDateTime;
/** 파싱된 시작날짜 (Day 통계용) */
private LocalDate parsedStartDate;
/** 파싱된 종료날짜 (Day 통계용) */
private LocalDate parsedEndDate;
/** 파싱된 시작월 (Month 통계용, YYYYMM) */
private String parsedStartMonth;
/** 파싱된 종료월 (Month 통계용, YYYYMM) */
private String parsedEndMonth;
/** 파싱된 시작년 (Year 통계용, YYYY) */
private String parsedStartYear;
/** 파싱된 종료년 (Year 통계용, YYYY) */
private String parsedEndYear;
} }