972819b492
- ApexCharts 기반 실시간 모니터링 대시보드 - 어댑터/배치/서버/소켓/트랜잭션 모니터링 컴포넌트 - 처리량 계산 및 데이터 매핑 서비스
699 lines
20 KiB
Plaintext
699 lines
20 KiB
Plaintext
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
|
|
|
<div class="dashboard-summary">
|
|
<h2 class="section-title">실시간 트랜잭션 현황</h2>
|
|
<div class="card">
|
|
<div class="chart-controls">
|
|
<div class="btn-group" style="display: none">
|
|
<button class="btn btn-chart-type" data-type="area">영역</button>
|
|
<button class="btn btn-chart-type active" data-type="line">선형</button>
|
|
<button class="btn btn-chart-type" data-type="bar">막대</button>
|
|
</div>
|
|
<div class="btn-group ml-2">
|
|
<button class="btn btn-time-range" data-range="5m">5분</button>
|
|
<button class="btn btn-time-range active" data-range="10m">10분</button>
|
|
<button class="btn btn-time-range" data-range="30m">30분</button>
|
|
<button class="btn btn-time-range" data-range="1h">1시간</button>
|
|
</div>
|
|
</div>
|
|
<div class="chart-container">
|
|
<div id="transaction-chart" class="chart-content">
|
|
<i class="bi bi-bar-chart-line"></i>
|
|
<span>트랜잭션 데이터를 로딩 중입니다...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ApexCharts 라이브러리 로드 -->
|
|
<script src="<%=request.getContextPath()%>/addon/apexcharts/apexcharts.js"></script>
|
|
|
|
<!-- 트랜잭션 차트 관련 스크립트 -->
|
|
<script>
|
|
// 트랜잭션 시계열 데이터 관리 객체
|
|
const TransactionTimeSeriesData = {
|
|
// 서비스별 시계열 데이터를 저장할 맵
|
|
serviceData: {},
|
|
|
|
// 최대 데이터 포인트 수 (60분 간격, 10초마다 갱신 = 360개 포인트)
|
|
maxDataPoints: 360,
|
|
|
|
// 초기화
|
|
init: function() {
|
|
// 서비스 목록 초기화
|
|
this.initServiceTypes();
|
|
},
|
|
|
|
// 서비스 유형 초기화
|
|
initServiceTypes: function() {
|
|
// 서버 데이터 배열에서 서비스 타입 추출
|
|
if (window.serverDataArray) {
|
|
const uniqueTypes = new Set();
|
|
serverDataArray.forEach(server => {
|
|
uniqueTypes.add(server.type);
|
|
});
|
|
|
|
// 각 고유 서비스 타입에 대해 시계열 데이터 초기화
|
|
uniqueTypes.forEach(serviceType => {
|
|
if (!this.serviceData[serviceType]) {
|
|
this.serviceData[serviceType] = {
|
|
timestamps: [],
|
|
success: [],
|
|
fail: [],
|
|
timeout: [],
|
|
syserror: []
|
|
};
|
|
}
|
|
});
|
|
} else {
|
|
// 기본 서비스 타입 (기본값)
|
|
const defaultTypes = ['EAI', 'FEP', 'MCI', 'MCU', 'BAP'];
|
|
defaultTypes.forEach(serviceType => {
|
|
this.serviceData[serviceType] = {
|
|
timestamps: [],
|
|
success: [],
|
|
fail: [],
|
|
timeout: [],
|
|
syserror: []
|
|
};
|
|
});
|
|
}
|
|
|
|
// 합계 데이터를 위한 TOTAL 서비스 추가
|
|
this.serviceData['TOTAL'] = {
|
|
timestamps: [],
|
|
success: [],
|
|
fail: [],
|
|
timeout: [],
|
|
syserror: []
|
|
};
|
|
},
|
|
|
|
// 타임스탬프를 Date 객체로 변환
|
|
ensureDateObject: function(timestamp) {
|
|
if (timestamp instanceof Date) {
|
|
return timestamp;
|
|
} else if (typeof timestamp === 'number') {
|
|
return new Date(timestamp);
|
|
} else if (typeof timestamp === 'string') {
|
|
return new Date(timestamp);
|
|
}
|
|
return new Date(); // 기본값으로 현재 시간 반환
|
|
},
|
|
|
|
// 트랜잭션 데이터 업데이트
|
|
updateData: function(transactions) {
|
|
if (!transactions || !transactions.length) return;
|
|
|
|
const timestamp = new Date();
|
|
|
|
// 각 서비스별 데이터 업데이트
|
|
transactions.forEach(transaction => {
|
|
const serviceType = transaction.type;
|
|
|
|
// 해당 서비스의 시계열 데이터가 없으면 초기화
|
|
if (!this.serviceData[serviceType]) {
|
|
this.serviceData[serviceType] = {
|
|
timestamps: [],
|
|
success: [],
|
|
fail: [],
|
|
timeout: [],
|
|
syserror: []
|
|
};
|
|
}
|
|
|
|
const data = this.serviceData[serviceType];
|
|
|
|
// 타임스탬프 추가
|
|
data.timestamps.push(timestamp);
|
|
|
|
// 데이터 추가
|
|
data.success.push(transaction.success || 0);
|
|
data.fail.push(transaction.fail || 0);
|
|
data.timeout.push(transaction.timeout || 0);
|
|
data.syserror.push(transaction.syserror || 0);
|
|
|
|
// 최대 데이터 포인트 수 제한
|
|
if (data.timestamps.length > this.maxDataPoints) {
|
|
data.timestamps.shift();
|
|
data.success.shift();
|
|
data.fail.shift();
|
|
data.timeout.shift();
|
|
data.syserror.shift();
|
|
}
|
|
});
|
|
},
|
|
|
|
// 오래된 데이터 정리 (60분 이상 경과된 데이터)
|
|
cleanupData: function() {
|
|
const cutoffTime = new Date(new Date() - 60 * 60 * 1000); // 60분 전
|
|
|
|
for (const serviceType in this.serviceData) {
|
|
const data = this.serviceData[serviceType];
|
|
|
|
// 각 데이터 시리즈에서 60분 이전 데이터 제거
|
|
let i = 0;
|
|
while (i < data.timestamps.length && this.ensureDateObject(data.timestamps[i]) < cutoffTime) {
|
|
i++;
|
|
}
|
|
|
|
if (i > 0) {
|
|
data.timestamps = data.timestamps.slice(i);
|
|
data.success = data.success.slice(i);
|
|
data.fail = data.fail.slice(i);
|
|
data.timeout = data.timeout.slice(i);
|
|
data.syserror = data.syserror.slice(i);
|
|
}
|
|
}
|
|
},
|
|
|
|
// 특정 서비스의 시계열 데이터 가져오기
|
|
getServiceData: function(serviceType) {
|
|
return this.serviceData[serviceType] || {
|
|
timestamps: [],
|
|
success: [],
|
|
fail: [],
|
|
timeout: [],
|
|
syserror: []
|
|
};
|
|
},
|
|
};
|
|
|
|
// 트랜잭션 차트 관리 객체
|
|
const TransactionChart = {
|
|
chart: null,
|
|
chartType: 'line', // 기본값: 영역 차트
|
|
timeRange: '10m', // 기본값: 5분
|
|
|
|
// 초기화
|
|
init: function(containerId) {
|
|
// ApexCharts 로드 확인
|
|
if (typeof ApexCharts === 'undefined') {
|
|
console.error('ApexCharts 라이브러리가 로드되지 않았습니다.');
|
|
return;
|
|
}
|
|
|
|
const container = document.getElementById(containerId);
|
|
if (!container) {
|
|
console.error('차트 컨테이너를 찾을 수 없습니다: ' + containerId);
|
|
return;
|
|
}
|
|
|
|
// 기존 차트 제거
|
|
if (this.chart) {
|
|
this.chart.destroy();
|
|
}
|
|
|
|
// 새 차트 인스턴스 생성
|
|
const options = this.getChartOptions();
|
|
this.chart = new ApexCharts(container, options);
|
|
this.chart.render();
|
|
|
|
// 브라우저 창 크기 변경 시 차트 크기 조정
|
|
window.addEventListener('resize', () => {
|
|
if (this.chart) {
|
|
this.chart.updateOptions({
|
|
chart: {
|
|
width: '100%'
|
|
}
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
// 차트 옵션 가져오기
|
|
getChartOptions: function() {
|
|
return {
|
|
chart: {
|
|
type: this.chartType,
|
|
height: 180,
|
|
toolbar: {
|
|
show: false
|
|
},
|
|
animations: {
|
|
enabled: true,
|
|
easing: 'easeinout',
|
|
speed: 800,
|
|
animateGradually: {
|
|
enabled: true,
|
|
delay: 150
|
|
},
|
|
dynamicAnimation: {
|
|
enabled: true,
|
|
speed: 350
|
|
}
|
|
},
|
|
fontFamily: 'Noto Sans KR, sans-serif',
|
|
zoom: {
|
|
enabled: false
|
|
}
|
|
},
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
stroke: {
|
|
curve: 'smooth',
|
|
width: 2
|
|
},
|
|
series: [],
|
|
colors: ['#4CAF50', '#F44336', '#2196F3', '#FF5722', '#9C27B0', '#FF9800'],
|
|
fill: {
|
|
type: this.chartType === 'area' ? 'gradient' : 'solid',
|
|
gradient: {
|
|
shade: 'light',
|
|
type: 'vertical',
|
|
shadeIntensity: 0.3,
|
|
opacityFrom: 0.7,
|
|
opacityTo: 0.2,
|
|
stops: [0, 90, 100]
|
|
}
|
|
},
|
|
grid: {
|
|
borderColor: '#e0e0e0',
|
|
strokeDashArray: 3,
|
|
xaxis: {
|
|
lines: {
|
|
show: false
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
shared: true,
|
|
intersect: false,
|
|
y: {
|
|
formatter: function(value) {
|
|
return value.toLocaleString() + ' 건';
|
|
}
|
|
},
|
|
x: {
|
|
formatter: function(value) {
|
|
const date = new Date(value);
|
|
return date.toLocaleTimeString();
|
|
}
|
|
}
|
|
},
|
|
xaxis: {
|
|
type: 'datetime',
|
|
labels: {
|
|
datetimeUTC: false,
|
|
format: 'HH:mm'
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
},
|
|
tickAmount: 6
|
|
},
|
|
yaxis: {
|
|
title: {
|
|
text: '처리 건수',
|
|
style: {
|
|
fontWeight: 600
|
|
}
|
|
},
|
|
labels: {
|
|
formatter: function(value) {
|
|
return value.toLocaleString();
|
|
}
|
|
},
|
|
forceNiceScale: true,
|
|
min: undefined,
|
|
max: undefined
|
|
},
|
|
legend: {
|
|
position: 'top',
|
|
horizontalAlign: 'right',
|
|
floating: true,
|
|
offsetY: -25,
|
|
offsetX: -5,
|
|
showForSingleSeries: true,
|
|
onItemClick: {
|
|
toggleDataSeries: true
|
|
},
|
|
onItemHover: {
|
|
highlightDataSeries: true
|
|
},
|
|
markers: {
|
|
width: 12,
|
|
height: 12,
|
|
strokeWidth: 0,
|
|
radius: 12,
|
|
offsetX: -5
|
|
},
|
|
itemMargin: {
|
|
horizontal: 10,
|
|
vertical: 5
|
|
}
|
|
},
|
|
states: {
|
|
hover: {
|
|
filter: {
|
|
type: 'lighten',
|
|
value: 0.05
|
|
}
|
|
},
|
|
active: {
|
|
filter: {
|
|
type: 'darken',
|
|
value: 0.1
|
|
}
|
|
}
|
|
},
|
|
responsive: [
|
|
{
|
|
breakpoint: 768,
|
|
options: {
|
|
legend: {
|
|
position: 'bottom',
|
|
horizontalAlign: 'center',
|
|
floating: false,
|
|
offsetY: 0,
|
|
offsetX: 0
|
|
}
|
|
}
|
|
}
|
|
]
|
|
};
|
|
},
|
|
|
|
// 서비스 타입에 따른 색상 맵
|
|
colorMap: {
|
|
'EAI': { success: '#4CAF50', error: '#F44336' },
|
|
'FEP': { success: '#2196F3', error: '#FF5722' },
|
|
'MCI': { success: '#9C27B0', error: '#FF9800' },
|
|
'MCU': { success: '#3F51B5', error: '#FFC107' },
|
|
'BAP': { success: '#00BCD4', error: '#795548' },
|
|
'TOTAL': { success: '#607D8B', error: '#9E9E9E' }
|
|
},
|
|
|
|
// 서비스 및 데이터 타입에 따른 색상 반환
|
|
getServiceColor: function(serviceType, dataType) {
|
|
if (this.colorMap[serviceType] && this.colorMap[serviceType][dataType]) {
|
|
return this.colorMap[serviceType][dataType];
|
|
}
|
|
|
|
// 기본 색상
|
|
return dataType === 'success' ? '#66BB6A' : '#EF5350';
|
|
},
|
|
|
|
// 차트 타입 설정
|
|
setChartType: function(type) {
|
|
this.chartType = type;
|
|
this.updateChart();
|
|
},
|
|
|
|
// 시간 범위 설정
|
|
setTimeRange: function(range) {
|
|
this.timeRange = range;
|
|
this.updateChart();
|
|
},
|
|
|
|
// 차트 업데이트
|
|
updateChart: function() {
|
|
if (!this.chart) return;
|
|
|
|
const series = [];
|
|
|
|
// 각 서비스에 대한 시리즈 생성 (TOTAL 제외)
|
|
for (const serviceType in TransactionTimeSeriesData.serviceData) {
|
|
if (serviceType === 'TOTAL') continue;
|
|
|
|
const serviceData = TransactionTimeSeriesData.serviceData[serviceType];
|
|
if (!serviceData.timestamps.length) continue;
|
|
|
|
// 성공 시리즈
|
|
series.push({
|
|
name: serviceType + ' 성공',
|
|
type: this.chartType,
|
|
data: serviceData.timestamps.map((time, index) => ({
|
|
x: TransactionTimeSeriesData.ensureDateObject(time).getTime(),
|
|
y: serviceData.success[index]
|
|
})),
|
|
color: this.getServiceColor(serviceType, 'success')
|
|
});
|
|
|
|
// 에러 시리즈 (실패 + 타임아웃 + 시스템 에러)
|
|
series.push({
|
|
name: serviceType + ' 에러',
|
|
type: this.chartType,
|
|
data: serviceData.timestamps.map((time, index) => ({
|
|
x: TransactionTimeSeriesData.ensureDateObject(time).getTime(),
|
|
y: serviceData.fail[index] + serviceData.timeout[index] + serviceData.syserror[index]
|
|
})),
|
|
color: this.getServiceColor(serviceType, 'error')
|
|
});
|
|
}
|
|
|
|
// 시간 범위에 따른 X축 범위 설정
|
|
const rangeMinutes = {
|
|
'5m': 5,
|
|
'10m': 10,
|
|
'30m': 30,
|
|
'1h': 60
|
|
}[this.timeRange] || 10;
|
|
|
|
const startTime = new Date(new Date() - rangeMinutes * 60 * 1000);
|
|
const endTime = new Date();
|
|
|
|
// 차트 옵션 업데이트 - 전체 다시 그리기 방지
|
|
this.chart.updateOptions({
|
|
chart: {
|
|
type: this.chartType
|
|
},
|
|
fill: {
|
|
type: this.chartType === 'area' ? 'gradient' : 'solid'
|
|
},
|
|
xaxis: {
|
|
min: startTime.getTime(),
|
|
max: endTime.getTime() + 1000
|
|
}
|
|
}, false, false); // 차트 다시 그리지 않고 업데이트
|
|
|
|
// 시리즈 데이터만 업데이트
|
|
this.chart.updateSeries(series, false);
|
|
},
|
|
|
|
// 모의 데이터로 차트 업데이트 (테스트용)
|
|
updateWithMockData: function() {
|
|
const mockData = TransactionTimeSeriesData.generateMockData();
|
|
TransactionTimeSeriesData.updateData(mockData);
|
|
this.updateChart();
|
|
}
|
|
};
|
|
|
|
// 문서 로드 완료 시 초기화
|
|
$(document).ready(function() {
|
|
// 시계열 데이터 관리 객체 초기화
|
|
TransactionTimeSeriesData.init();
|
|
|
|
// 차트 초기화
|
|
TransactionChart.init('transaction-chart');
|
|
|
|
// 차트 컨트롤 이벤트 설정
|
|
setupChartControls();
|
|
|
|
// 초기 데이터 로드
|
|
loadInitialChartData();
|
|
|
|
// 10초마다 오래된 데이터 정리
|
|
setInterval(() => TransactionTimeSeriesData.cleanupData(), 10000);
|
|
|
|
// DashboardEventManager를 사용하여 새로고침 이벤트 구독
|
|
if (window.DashboardEventManager) {
|
|
window.DashboardEventManager.on('refresh', function() {
|
|
// 트랜잭션 데이터 추가 로드
|
|
loadSummaryTransactionData();
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
// 차트 컨트롤 이벤트 설정
|
|
function setupChartControls() {
|
|
// 차트 타입 변경
|
|
$('.btn-chart-type').click(function() {
|
|
// 기존 활성화 버튼 비활성화
|
|
$('.btn-chart-type').removeClass('active');
|
|
|
|
// 클릭한 버튼 활성화
|
|
$(this).addClass('active');
|
|
|
|
// 차트 타입 변경
|
|
const chartType = $(this).data('type');
|
|
TransactionChart.setChartType(chartType);
|
|
});
|
|
|
|
// 시간 범위 변경
|
|
$('.btn-time-range').click(function() {
|
|
// 기존 활성화 버튼 비활성화
|
|
$('.btn-time-range').removeClass('active');
|
|
|
|
// 클릭한 버튼 활성화
|
|
$(this).addClass('active');
|
|
|
|
// 시간 범위 변경
|
|
const timeRange = $(this).data('range');
|
|
TransactionChart.setTimeRange(timeRange);
|
|
});
|
|
}
|
|
|
|
// 초기 차트 데이터 로드
|
|
function loadInitialChartData() {
|
|
// 시계열 데이터 API 호출
|
|
ModernDashboardApi.getTransactionTimeSeries(null, 10, function(data) {
|
|
if (data && data.timeSeriesData) {
|
|
// 각 서비스별 시계열 데이터 처리
|
|
for (var serviceType in data.timeSeriesData) {
|
|
var serviceData = data.timeSeriesData[serviceType];
|
|
if (!serviceData.timestamps || serviceData.timestamps.length === 0) continue;
|
|
|
|
// 기존 데이터 초기화
|
|
TransactionTimeSeriesData.serviceData[serviceType] = {
|
|
timestamps: [],
|
|
success: [],
|
|
fail: [],
|
|
timeout: [],
|
|
syserror: []
|
|
};
|
|
|
|
// API 응답 데이터 복사 및 타임스탬프 변환
|
|
TransactionTimeSeriesData.serviceData[serviceType].timestamps = serviceData.timestamps.map(time =>
|
|
TransactionTimeSeriesData.ensureDateObject(time)
|
|
);
|
|
TransactionTimeSeriesData.serviceData[serviceType].success = serviceData.success.slice();
|
|
TransactionTimeSeriesData.serviceData[serviceType].fail = serviceData.fail.slice();
|
|
TransactionTimeSeriesData.serviceData[serviceType].timeout = serviceData.timeout.slice();
|
|
TransactionTimeSeriesData.serviceData[serviceType].syserror = serviceData.syserror.slice();
|
|
}
|
|
|
|
// 차트 업데이트
|
|
TransactionChart.updateChart();
|
|
} else {
|
|
// API 응답이 없는 경우 모의 데이터로 테스트
|
|
TransactionChart.updateWithMockData();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 트랜잭션 데이터 로드 및 차트 업데이트 (함수명 변경)
|
|
function loadSummaryTransactionData() {
|
|
|
|
// 현재 선택된 시간 범위 가져오기
|
|
var activeRange = $('.btn-time-range.active').data('range') || '10m';
|
|
var minutes = {
|
|
'5m': 5,
|
|
'10m': 10,
|
|
'30m': 30,
|
|
'1h': 60
|
|
}[activeRange] || 10;
|
|
|
|
ModernDashboardApi.getTransactionTimeSeries(null, minutes, function(data) {
|
|
if (data && data.timeSeriesData) {
|
|
// 각 서비스별 시계열 데이터 처리
|
|
for (var serviceType in data.timeSeriesData) {
|
|
var serviceData = data.timeSeriesData[serviceType];
|
|
if (!serviceData.timestamps || serviceData.timestamps.length === 0) continue;
|
|
|
|
// 기존 데이터 초기화
|
|
TransactionTimeSeriesData.serviceData[serviceType] = {
|
|
timestamps: [],
|
|
success: [],
|
|
fail: [],
|
|
timeout: [],
|
|
syserror: []
|
|
};
|
|
|
|
// API 응답 데이터 복사 및 타임스탬프 변환
|
|
TransactionTimeSeriesData.serviceData[serviceType].timestamps = serviceData.timestamps.map(time =>
|
|
TransactionTimeSeriesData.ensureDateObject(time)
|
|
);
|
|
TransactionTimeSeriesData.serviceData[serviceType].success = serviceData.success.slice();
|
|
TransactionTimeSeriesData.serviceData[serviceType].fail = serviceData.fail.slice();
|
|
TransactionTimeSeriesData.serviceData[serviceType].timeout = serviceData.timeout.slice();
|
|
TransactionTimeSeriesData.serviceData[serviceType].syserror = serviceData.syserror.slice();
|
|
}
|
|
|
|
// 차트 업데이트 - 부드러운 업데이트
|
|
TransactionChart.updateChart();
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<!-- 추가 CSS 스타일 -->
|
|
<style>
|
|
.chart-controls {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.chart-controls .btn-group {
|
|
display: flex;
|
|
}
|
|
|
|
.chart-controls .btn {
|
|
padding: 4px 8px;
|
|
font-size: 0.75rem;
|
|
border: 1px solid #e0e0e0;
|
|
background-color: #fff;
|
|
color: #666;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.chart-controls .btn:first-child {
|
|
border-top-left-radius: 4px;
|
|
border-bottom-left-radius: 4px;
|
|
}
|
|
|
|
.chart-controls .btn:last-child {
|
|
border-top-right-radius: 4px;
|
|
border-bottom-right-radius: 4px;
|
|
}
|
|
|
|
.chart-controls .btn.active {
|
|
background-color: #2196F3;
|
|
color: white;
|
|
border-color: #2196F3;
|
|
}
|
|
|
|
.chart-controls .ml-2 {
|
|
margin-left: 0.5rem;
|
|
}
|
|
|
|
.chart-content {
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 200px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
color: var(--color-gray-400);
|
|
}
|
|
|
|
.card {
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
background-color: #fff;
|
|
padding: 16px;
|
|
margin-bottom: 0px;
|
|
}
|
|
|
|
/* 범례 스타일 개선 */
|
|
.apexcharts-legend-series {
|
|
margin: 3px 8px !important;
|
|
cursor: pointer !important;
|
|
}
|
|
|
|
.apexcharts-legend-text {
|
|
font-size: 0.85rem !important;
|
|
color: #333 !important;
|
|
font-weight: 500 !important;
|
|
}
|
|
|
|
.apexcharts-legend-marker {
|
|
margin-right: 5px !important;
|
|
}
|
|
|
|
.apexcharts-legend-series.apexcharts-inactive-legend {
|
|
opacity: 0.5 !important;
|
|
}
|
|
</style> |