Files
eapim-admin/WebContent/jsp/dashboard-modern/components/dashboard-tran-chart.jsp
T
youna 9545d69dc4 fix: 모던 대시보드 K8S 환경 확인 오류 수정
존재하지 않는 DB 테이블을 참조하여 발생하던 대시보드 오류를 해결.
K8S 환경 확인을 위한 DB 조회 로직을 제거, 일반 환경에서 항상 정상 동작하도록 수정.
2025-11-17 17:39:57 +09:00

714 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(); // 기본값으로 현재 시간 반환
},
// 모의 데이터 생성 (테스트용)
generateMockData: function() {
const mockData = [];
for (const serviceType in this.serviceData) {
mockData.push({
type: serviceType,
success: 0,
fail: 0,
timeout: 0,
syserror: 0
});
}
return mockData;
},
// 트랜잭션 데이터 업데이트
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>