Merge remote-tracking branch 'z-eactive/feature/modern-dashboard' into devs/dashboard

This commit is contained in:
Rinjae
2025-11-17 14:52:32 +09:00
72 changed files with 49662 additions and 547 deletions
+15 -1
View File
@@ -401,6 +401,14 @@
w.focus();
}
function goDashboard2() {
// You must open from parent window !
var width = window.screen.availWidth;
var height = window.screen.availHeight;
var w = parent.window.open("<%=request.getContextPath()%>/modern-dashboard.do", "RMSDASHBOARD<%=OPS%>", "width=" + width + ",height=" + height + ",left=0,top=0,scrollbars=no,resizable=yes,status=no");
w.focus();
}
function menuRender() {
<%if(topListSize > 0){%>
// 처음 로딩시 제일첫번째 화면 보여주기
@@ -472,6 +480,12 @@
$('.depth2 a').on("click", function (e) {
$(top.document).find(".topMenu").css("height", "80px");
});
$("#newDashboardShow").on("dblclick", function(){
<% if(((String)SessionManager.getRoleIdString(request)).indexOf("admin") > -1 || ((String)SessionManager.getRoleIdString(request)).indexOf("eai_monitor") > -1) { %>
goDashboard2();
<% } %>
});
});
function logout() {
@@ -535,7 +549,7 @@
</h1><!-- /monitoring/images/top_logo.png -->
<div class="topmenu_box">
<ul>
<li style="width:240px;">
<li style="width:240px;" id="newDashboardShow">
<a style="width:240px;"
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>)</span>
<span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span>
@@ -0,0 +1,415 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*" %>
<%@ page import="com.eactive.eai.rms.common.datasource.*" %>
<div class="dashboard-adapter-summary">
<div class="dashboard-header">
<div class="header-title-container">
<h2 class="section-title">회선 상태</h2>
<%--<div class="adapter-actions">
<button class="filter-btn view-socket-detail active">상세보기</button>
</div>--%>
</div>
</div>
<div class="card adapter-summary-card">
<div class="adapter-summary-container">
<div class="adapter-summary-header">
<div class="adapter-type-column">어댑터 유형</div>
<div class="adapter-status-column">
<div class="status-label error-label">에러</div>
<div class="status-label warning-label">경고</div>
<div class="status-label normal-label">정상</div>
<div class="status-label inactive-label">정지</div>
<div class="status-label total-label">합계</div>
</div>
</div>
<div class="adapter-summary-body">
<!-- 어댑터 요약 데이터가 동적으로 추가됩니다 -->
</div>
</div>
</div>
</div>
<!-- 어댑터 요약 행 템플릿 -->
<template id="adapter-summary-row-template">
<div class="adapter-summary-row">
<div class="adapter-type-column">
<div class="adapter-type-info">
<span class="adapter-type-name">Socket</span>
<span class="status-badge adapter-status-normal">정상</span>
</div>
</div>
<div class="adapter-status-column">
<div class="status-count error-count">0</div>
<div class="status-count warning-count">0</div>
<div class="status-count normal-count">0</div>
<div class="status-count inactive-count">0</div>
<div class="status-count total-count">0</div>
</div>
</div>
</template>
<!-- 소켓 상세 정보 모달 포함 -->
<jsp:include page="dashboard-socket-detail.jsp" />
<style>
/* 어댑터 섹션 */
.adapter-card {
overflow: hidden;
}
.adapter-table-container {
max-height: 300px;
overflow-y: auto;
}
/* 어댑터 탭 스타일 개선 */
.dashboard-adapters .card-tabs {
display: flex;
border-bottom: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
overflow-x: auto;
}
.dashboard-adapters .tab-btn {
padding: 0.75rem 1.25rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-gray-600);
background: none;
border: none;
cursor: pointer;
white-space: nowrap;
border-bottom: 2px solid transparent;
}
.dashboard-adapters .tab-btn:hover {
color: var(--color-primary);
}
.dashboard-adapters .tab-btn.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
background-color: white;
}
.dashboard-adapters .tab-content {
display: none;
padding: 1rem;
}
.dashboard-adapters .tab-content.active {
display: block;
}
/* 어댑터 섹션 스타일 조정 */
.adapters-container .card {
height: 100%;
min-height: 249px;
}
.dashboard-adapter-summary {
margin-bottom: 1rem;
}
.adapter-summary-card {
padding: 0;
overflow: hidden;
}
.adapter-summary-container {
padding: 0.5rem 1rem;
}
.adapter-summary-header {
display: flex;
font-weight: bold;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 0.25rem;
margin-bottom: 0.25rem;
}
.adapter-summary-body {
max-height: 300px;
overflow-y: auto;
}
.adapter-summary-row {
display: flex;
padding: 0.25rem 0;
border-bottom: 1px solid #f0f0f0;
}
.adapter-summary-row:last-child {
border-bottom: none;
}
.adapter-type-column {
flex: 1;
display: flex;
align-items: center;
}
.adapter-type-info {
display: flex;
align-items: center;
}
.adapter-type-name {
font-weight: bold;
margin-right: 0.5rem;
}
.adapter-status-column {
flex: 3;
display: flex;
justify-content: space-between;
align-items: center;
}
.status-label, .status-count {
flex: 1;
text-align: center;
padding: 0.5rem;
}
.status-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-size: 0.7rem;
font-weight: bold;
text-transform: uppercase;
}
.adapter-status-normal {
background-color: rgba(76, 175, 80, 0.15);
color: #4caf50;
}
.adapter-status-warning {
background-color: rgba(255, 152, 0, 0.15);
color: #ff9800;
}
.adapter-status-error {
background-color: rgba(229, 57, 53, 0.15);
color: #e53935;
}
.adapter-status-inactive {
background-color: rgba(158, 158, 158, 0.15);
color: #9e9e9e;
}
.error-label, .error-count {
color: #e53935;
}
.warning-label, .warning-count {
color: #ff9800;
}
.normal-label, .normal-count {
color: #4caf50;
}
.inactive-label, .inactive-count {
color: #9e9e9e;
}
.total-label, .total-count {
font-weight: bold;
}
.empty-data-message {
padding: 1rem;
text-align: center;
color: #757575;
font-style: italic;
}
/* 어댑터 헤더 스타일 */
.adapter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
/* 대시보드 헤더 스타일 */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
/* 제목과 버튼을 한 줄에 나란히 표시하기 위한 컨테이너 */
.header-title-container {
display: flex;
align-items: center;
}
.adapter-actions {
margin-left: 10px; /* 제목과 버튼 사이 간격 */
display: flex;
align-items: center;
}
.filter-btn:hover {
background-color: var(--color-gray-200);
color: var(--color-gray-800);
}
.filter-btn.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0069d9;
opacity: 1 !important;
}
</style>
<script>
// 어댑터 요약 행 생성 함수
function createAdapterSummaryRow(adapterSummary) {
var template = document.getElementById('adapter-summary-row-template');
var clone = document.importNode(template.content, true);
// 어댑터 유형 설정
clone.querySelector('.adapter-type-name').textContent = adapterSummary.adapterType || '—';
// 상태 설정
var statusBadge = clone.querySelector('.status-badge');
statusBadge.className = 'status-badge';
if (adapterSummary.overallStatus === 'normal') {
statusBadge.classList.add('adapter-status-normal');
statusBadge.textContent = '정상';
} else if (adapterSummary.overallStatus === 'warning') {
statusBadge.classList.add('adapter-status-warning');
statusBadge.textContent = '경고';
} else if (adapterSummary.overallStatus === 'error') {
statusBadge.classList.add('adapter-status-error');
statusBadge.textContent = '오류';
} else if (adapterSummary.overallStatus === 'inactive') {
statusBadge.classList.add('adapter-status-inactive');
statusBadge.textContent = '정지';
} else {
statusBadge.classList.add('adapter-status-inactive');
statusBadge.textContent = '알 수 없음';
}
// 상태별 카운트 설정
clone.querySelector('.error-count').textContent = formatNumber(adapterSummary.errorCount || 0);
clone.querySelector('.warning-count').textContent = formatNumber(adapterSummary.warningCount || 0);
clone.querySelector('.normal-count').textContent = formatNumber(adapterSummary.normalCount || 0);
clone.querySelector('.inactive-count').textContent = formatNumber(adapterSummary.inactiveCount || 0);
clone.querySelector('.total-count').textContent = formatNumber(adapterSummary.totalCount || 0);
return clone;
}
// 어댑터 요약 테이블 업데이트 함수
function updateAdapterSummaryTable(data) {
if (!data || !data.serverAdapters) return;
// 어댑터 요약 컨테이너 찾기
var summaryBody = document.querySelector('.adapter-summary-body');
if (!summaryBody) return;
// 컨테이너 초기화
summaryBody.innerHTML = '';
// 모든 서버의 어댑터 요약 데이터 통합
var allAdapterSummaries = [];
data.serverAdapters.forEach(function(serverAdapter) {
if (serverAdapter.adapterSummaries && serverAdapter.adapterSummaries.length > 0) {
allAdapterSummaries = allAdapterSummaries.concat(serverAdapter.adapterSummaries);
}
});
// 어댑터 유형별로 그룹화
var adapterTypeMap = {};
allAdapterSummaries.forEach(function(summary) {
if (!adapterTypeMap[summary.adapterType]) {
adapterTypeMap[summary.adapterType] = summary;
} else {
// 기존 데이터에 카운트 누적
var existing = adapterTypeMap[summary.adapterType];
existing.errorCount = (existing.errorCount || 0) + (summary.errorCount || 0);
existing.warningCount = (existing.warningCount || 0) + (summary.warningCount || 0);
existing.normalCount = (existing.normalCount || 0) + (summary.normalCount || 0);
existing.inactiveCount = (existing.inactiveCount || 0) + (summary.inactiveCount || 0);
existing.totalCount = (existing.totalCount || 0) + (summary.totalCount || 0);
// 전체 상태 업데이트
if (summary.overallStatus === 'error' || existing.overallStatus === 'error') {
existing.overallStatus = 'error';
} else if (summary.overallStatus === 'warning' || existing.overallStatus === 'warning') {
existing.overallStatus = 'warning';
}
}
});
// 필터링된 어댑터 유형만 표시 (Socket, Socket2, HTTP)
var filteredAdapterTypes = Object.keys(adapterTypeMap).filter(function(type) {
return type === 'Socket' || type === 'Socket2' || type === 'HTTP';
});
if (filteredAdapterTypes.length > 0) {
// 어댑터 유형별로 행 추가
filteredAdapterTypes.forEach(function(type) {
var summaryRow = createAdapterSummaryRow(adapterTypeMap[type]);
summaryBody.appendChild(summaryRow);
});
} else {
// 데이터가 없는 경우 안내 메시지 표시
var emptyRow = document.createElement('div');
emptyRow.className = 'empty-data-message';
emptyRow.textContent = '표시할 어댑터 데이터가 없습니다.';
summaryBody.appendChild(emptyRow);
}
}
// 어댑터 요약 데이터 로드 함수
function loadAdapterSummaryData() {
// 어댑터 요약 데이터 API 호출
ModernDashboardApi.getAdapterStatus(null, function(data) {
if (data) {
updateAdapterSummaryTable(data);
}
});
}
// 문서 로드 완료 시 초기화
$(document).ready(function() {
// 초기 데이터 로드
loadAdapterSummaryData();
// 소켓 어댑터 상세 보기 버튼 이벤트
$('.view-socket-detail').on('click', function() {
// 소켓 상세 정보 모달 열기
openSocketDetailModal();
});
// DashboardEventManager를 사용하여 새로고침 이벤트 구독
if (window.DashboardEventManager) {
window.DashboardEventManager.on('refresh', function() {
// 어댑터 요약 데이터 새로고침
loadAdapterSummaryData();
});
}
});
</script>
@@ -0,0 +1,405 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<div class="dashboard-batch">
<h2 class="section-title">배치 처리 상태</h2>
<div class="card">
<div class="batch-grid">
<div class="batch-card" id="bapBatchCard" style="display: none">
<div class="batch-header">
<h4>일괄 전송 처리 (BAP)</h4>
</div>
<div class="batch-content">
<div class="batch-metric">
<div class="batch-metric-header">
<span class="metric-label">송신</span>
<div class="metric-values">
<span class="metric-value" id="bap-send-success">0</span>
<span class="metric-title">성공</span>
</div>
<div class="metric-values">
<span class="metric-value error" id="bap-send-error">0</span>
<span class="metric-title">에러</span>
</div>
<div class="metric-values">
<span class="metric-value warning" id="bap-send-waiting">0</span>
<span class="metric-title">대기</span>
</div>
</div>
<div class="metric-progress">
<div class="progress-bar">
<div class="progress-fill success" id="bap-send-success-bar" style="width: 80%"></div>
<div class="progress-fill danger" id="bap-send-error-bar" style="width: 10%"></div>
<div class="progress-fill warning" id="bap-send-waiting-bar" style="width: 10%"></div>
</div>
<div class="progress-info">
<span class="progress-label">처리율:</span>
<span class="progress-value" id="bap-send-rate">0%</span>
</div>
</div>
</div>
<div class="batch-metric">
<div class="batch-metric-header">
<span class="metric-label">수신</span>
<div class="metric-values">
<span class="metric-value" id="bap-receive-success">0</span>
<span class="metric-title">성공</span>
</div>
<div class="metric-values">
<span class="metric-value error" id="bap-receive-error">0</span>
<span class="metric-title">에러</span>
</div>
<div class="metric-values">
<span class="metric-value warning" id="bap-receive-waiting">0</span>
<span class="metric-title">대기</span>
</div>
</div>
<div class="metric-progress">
<div class="progress-bar">
<div class="progress-fill success" id="bap-receive-success-bar" style="width: 70%"></div>
<div class="progress-fill danger" id="bap-receive-error-bar" style="width: 5%"></div>
<div class="progress-fill warning" id="bap-receive-waiting-bar" style="width: 25%"></div>
</div>
<div class="progress-info">
<span class="progress-label">처리율:</span>
<span class="progress-value" id="bap-receive-rate">0%</span>
</div>
</div>
</div>
</div>
</div>
<div class="batch-card" id="batBatchCard" style="display: none">
<div class="batch-header">
<h4>배치 처리 (BAT)</h4>
</div>
<div class="batch-content">
<div class="batch-metric">
<div class="batch-metric-header">
<span class="metric-label">송신</span>
<div class="metric-values">
<span class="metric-value" id="bat-send-success">0</span>
<span class="metric-title">성공</span>
</div>
<div class="metric-values">
<span class="metric-value error" id="bat-send-error">0</span>
<span class="metric-title">에러</span>
</div>
<div class="metric-values">
<span class="metric-value warning" id="bat-send-waiting">0</span>
<span class="metric-title">대기</span>
</div>
</div>
<div class="metric-progress">
<div class="progress-bar">
<div class="progress-fill success" id="bat-send-success-bar" style="width: 75%"></div>
<div class="progress-fill danger" id="bat-send-error-bar" style="width: 15%"></div>
<div class="progress-fill warning" id="bat-send-waiting-bar" style="width: 10%"></div>
</div>
<div class="progress-info">
<span class="progress-label">처리율:</span>
<span class="progress-value" id="bat-send-rate">0%</span>
</div>
</div>
</div>
<div class="batch-metric">
<div class="batch-metric-header">
<span class="metric-label">수신</span>
<div class="metric-values">
<span class="metric-value" id="bat-receive-success">0</span>
<span class="metric-title">성공</span>
</div>
<div class="metric-values">
<span class="metric-value error" id="bat-receive-error">0</span>
<span class="metric-title">에러</span>
</div>
<div class="metric-values">
<span class="metric-value warning" id="bat-receive-waiting">0</span>
<span class="metric-title">대기</span>
</div>
</div>
<div class="metric-progress">
<div class="progress-bar">
<div class="progress-fill success" id="bat-receive-success-bar" style="width: 65%"></div>
<div class="progress-fill danger" id="bat-receive-error-bar" style="width: 10%"></div>
<div class="progress-fill warning" id="bat-receive-waiting-bar" style="width: 25%"></div>
</div>
<div class="progress-info">
<span class="progress-label">처리율:</span>
<span class="progress-value" id="bat-receive-rate">0%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* 배치 처리 섹션 */
.batch-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.batch-card {
background-color: white;
border-radius: var(--rounded-md);
border: 1px solid var(--color-gray-200);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.batch-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.25rem;
background-color: var(--color-gray-50);
border-bottom: 1px solid var(--color-gray-200);
}
.batch-header h4 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--color-gray-800);
}
.batch-date {
font-size: 0.75rem;
color: var(--color-gray-500);
}
.batch-content {
padding: 0.75rem 1.25rem;
}
.batch-metric {
margin-bottom: 0.75rem;
}
.batch-metric:last-child {
margin-bottom: 0;
}
.batch-metric-header {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
.batch-metric .metric-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-gray-700);
width: 60px;
}
.batch-metric .metric-values {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 1.5rem;
}
.batch-metric .metric-value {
font-size: 1rem;
font-weight: 600;
color: var(--color-gray-800);
}
.batch-metric .metric-value.error {
color: var(--color-danger);
}
.batch-metric .metric-value.warning {
color: var(--color-warning);
}
.batch-metric .metric-title {
font-size: 0.75rem;
color: var(--color-gray-500);
margin-top: 0.25rem;
}
.metric-progress {
margin-top: 0.25rem;
}
.progress-bar {
height: 8px;
background-color: var(--color-gray-200);
border-radius: var(--rounded-full);
display: flex;
overflow: hidden;
}
.progress-fill {
height: 100%;
transition: width 0.3s ease;
}
.progress-fill.success {
background-color: var(--color-success);
}
.progress-fill.danger {
background-color: var(--color-danger);
}
.progress-fill.warning {
background-color: var(--color-warning);
}
.progress-info {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--color-gray-600);
margin-top: 0.5rem;
}
/* 배치 섹션 스타일 조정 */
.batch-container .card {
height: 100%;
}
</style>
<script>
// 숫자 형식화 함수 (천 단위 콤마 추가)
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// 배치 프로세스 업데이트 함수
function updateBatchProcess(data) {
if (!data || !data.batchData) return;
// BAP 배치 데이터 업데이트
var bapData = data.batchData.filter(function(batch) {
return batch.serviceType === 'BAP';
})[0];
if (bapData) {
// 송신 데이터 설정
$('#bap-send-success').text(formatNumber(bapData.sendSuccess));
$('#bap-send-error').text(formatNumber(bapData.sendError));
$('#bap-send-waiting').text(formatNumber(bapData.sendWaiting));
// 총 합계 계산
var sendTotal = bapData.sendSuccess + bapData.sendError + bapData.sendWaiting;
// 바 너비 계산
var sendSuccessPercent = sendTotal > 0 ? (bapData.sendSuccess / sendTotal * 100) : 0;
var sendErrorPercent = sendTotal > 0 ? (bapData.sendError / sendTotal * 100) : 0;
var sendWaitingPercent = sendTotal > 0 ? (bapData.sendWaiting / sendTotal * 100) : 0;
// 바 너비 설정
$('#bap-send-success-bar').css('width', sendSuccessPercent + '%');
$('#bap-send-error-bar').css('width', sendErrorPercent + '%');
$('#bap-send-waiting-bar').css('width', sendWaitingPercent + '%');
// 처리율 설정
var sendCompletedPercent = sendTotal > 0 ?
Math.round((bapData.sendSuccess + bapData.sendError) / sendTotal * 100) : 0;
$('#bap-send-rate').text(sendCompletedPercent + '%');
// 수신 데이터 설정
$('#bap-receive-success').text(formatNumber(bapData.receiveSuccess));
$('#bap-receive-error').text(formatNumber(bapData.receiveError));
$('#bap-receive-waiting').text(formatNumber(bapData.receiveWaiting));
// 총 합계 계산
var receiveTotal = bapData.receiveSuccess + bapData.receiveError + bapData.receiveWaiting;
// 바 너비 계산
var receiveSuccessPercent = receiveTotal > 0 ? (bapData.receiveSuccess / receiveTotal * 100) : 0;
var receiveErrorPercent = receiveTotal > 0 ? (bapData.receiveError / receiveTotal * 100) : 0;
var receiveWaitingPercent = receiveTotal > 0 ? (bapData.receiveWaiting / receiveTotal * 100) : 0;
// 바 너비 설정
$('#bap-receive-success-bar').css('width', receiveSuccessPercent + '%');
$('#bap-receive-error-bar').css('width', receiveErrorPercent + '%');
$('#bap-receive-waiting-bar').css('width', receiveWaitingPercent + '%');
// 처리율 설정
var receiveCompletedPercent = receiveTotal > 0 ?
Math.round((bapData.receiveSuccess + bapData.receiveError) / receiveTotal * 100) : 0;
$('#bap-receive-rate').text(receiveCompletedPercent + '%');
document.getElementById("bapBatchCard").style.display = "block";
}
// BAT 배치 데이터 업데이트
var batData = data.batchData.filter(function(batch) {
return batch.serviceType === 'BAT';
})[0];
if (batData) {
// 송신 데이터 설정
$('#bat-send-success').text(formatNumber(batData.sendSuccess));
$('#bat-send-error').text(formatNumber(batData.sendError));
$('#bat-send-waiting').text(formatNumber(batData.sendWaiting));
// 총 합계 계산
var sendTotal = batData.sendSuccess + batData.sendError + batData.sendWaiting;
// 바 너비 계산
var sendSuccessPercent = sendTotal > 0 ? (batData.sendSuccess / sendTotal * 100) : 0;
var sendErrorPercent = sendTotal > 0 ? (batData.sendError / sendTotal * 100) : 0;
var sendWaitingPercent = sendTotal > 0 ? (batData.sendWaiting / sendTotal * 100) : 0;
// 바 너비 설정
$('#bat-send-success-bar').css('width', sendSuccessPercent + '%');
$('#bat-send-error-bar').css('width', sendErrorPercent + '%');
$('#bat-send-waiting-bar').css('width', sendWaitingPercent + '%');
// 처리율 설정
var sendCompletedPercent = sendTotal > 0 ?
Math.round((batData.sendSuccess + batData.sendError) / sendTotal * 100) : 0;
$('#bat-send-rate').text(sendCompletedPercent + '%');
// 수신 데이터 설정
$('#bat-receive-success').text(formatNumber(batData.receiveSuccess));
$('#bat-receive-error').text(formatNumber(batData.receiveError));
$('#bat-receive-waiting').text(formatNumber(batData.receiveWaiting));
// 총 합계 계산
var receiveTotal = batData.receiveSuccess + batData.receiveError + batData.receiveWaiting;
// 바 너비 계산
var receiveSuccessPercent = receiveTotal > 0 ? (batData.receiveSuccess / receiveTotal * 100) : 0;
var receiveErrorPercent = receiveTotal > 0 ? (batData.receiveError / receiveTotal * 100) : 0;
var receiveWaitingPercent = receiveTotal > 0 ? (batData.receiveWaiting / receiveTotal * 100) : 0;
// 바 너비 설정
$('#bat-receive-success-bar').css('width', receiveSuccessPercent + '%');
$('#bat-receive-error-bar').css('width', receiveErrorPercent + '%');
$('#bat-receive-waiting-bar').css('width', receiveWaitingPercent + '%');
// 처리율 설정
var receiveCompletedPercent = receiveTotal > 0 ?
Math.round((batData.receiveSuccess + batData.receiveError) / receiveTotal * 100) : 0;
$('#bat-receive-rate').text(receiveCompletedPercent + '%');
document.getElementById("batBatchCard").style.display = "block";
}
}
// 배치 처리 데이터 로드 및 업데이트 함수
function loadBatchProcessData() {
ModernDashboardApi.getBatchProcess(function(data) {
updateBatchProcess(data);
});
}
// 초기 로드
$(document).ready(function() {
loadBatchProcessData();
// 대시보드 새로고침 이벤트에 배치 처리 데이터 로드 함수 등록
DashboardEventManager.on('refresh', function() {
loadBatchProcessData();
});
});
</script>
@@ -0,0 +1,9 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<footer>
<div class="footer-info">
<p>© 2025 eLink Monitoring System</p>
</div>
<div class="footer-links">
</div>
</footer>
@@ -0,0 +1,102 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<header>
<div class="header-title">
<i class="bi bi-speedometer2"></i>
<h1>eLink 모니터링 대시보드</h1>
<div class="global-loading-indicator" id="global-loading-indicator"></div>
</div>
<div class="header-controls">
<div class="time-display" id="current-time"></div>
<div class="refresh-controls">
<span class="refresh-status active"></span>
<span class="refresh-label">자동 새로고침:</span>
<div class="btn-group">
<button class="btn refresh-btn btn-sm" data-interval="2">2초</button>
<button class="btn refresh-btn btn-sm" data-interval="5">5초</button>
<button class="btn refresh-btn btn-sm active" data-interval="10">10초</button>
<button class="btn refresh-btn btn-sm" data-interval="0">사용안함</button>
</div>
</div>
<button class="btn btn-sm" id="refresh-btn">
<i class="bi bi-arrow-clockwise"></i> 수동 새로고침
</button>
</div>
</header>
<script>
// 새로고침 간격 버튼 클릭 이벤트
$('.refresh-btn').click(function() {
// 버튼 활성화 상태 변경
$('.refresh-btn').removeClass('active');
$(this).addClass('active');
// 새로고침 간격 설정
refreshInterval = parseInt($(this).data('interval'));
// 로컬 스토리지에 저장
localStorage.setItem('dashboardRefreshInterval', refreshInterval);
// 타이머 재시작
startRefreshTimer();
// 새로고침 상태 표시 업데이트
updateRefreshStatus();
});
// 수동 새로고침 버튼 클릭 이벤트
$('#refresh-btn').click(function() {
refreshDashboardData();
});
// 전역 로딩 인디케이터 제어 함수
function showGlobalLoadingIndicator() {
$('#global-loading-indicator').addClass('active');
}
function hideGlobalLoadingIndicator() {
$('#global-loading-indicator').removeClass('active');
}
// 문서 로드 완료 시 이벤트 리스너 등록
$(document).ready(function() {
// DashboardEventManager 이벤트 구독
if (window.DashboardEventManager) {
// 데이터 로딩 시작 이벤트
window.DashboardEventManager.on('loading-start', function() {
showGlobalLoadingIndicator();
});
// 데이터 로딩 완료 이벤트
window.DashboardEventManager.on('loading-end', function() {
hideGlobalLoadingIndicator();
});
}
});
</script>
<style>
/* 전역 로딩 인디케이터 스타일 */
.global-loading-indicator {
display: inline-block;
width: 18px;
height: 18px;
margin-left: 10px;
border-radius: 50%;
border: 2px solid rgba(79, 70, 229, 0.2);
border-top-color: var(--color-primary);
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease;
}
.global-loading-indicator.active {
opacity: 1;
visibility: visible;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
@@ -0,0 +1,680 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<!-- 인터페이스 로그 조회 모달 -->
<div id="interfaceLogDetailModal" class="modal modal-modern">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">인터페이스 로그 조회</h2>
<span class="close-modal">&times;</span>
</div>
<div class="modal-body">
<!-- 인터페이스 정보 요약 -->
<div class="interface-info-summary">
<div class="info-card">
<div class="info-header">
<span class="info-label">인터페이스 ID</span>
<span class="info-value" id="detail-interface-id">-</span>
</div>
<div class="info-content">
<div class="info-row">
<span class="info-label">발생시간</span>
<span class="info-value" id="detail-error-time">-</span>
</div>
<div class="info-row">
<span class="info-label">인스턴스</span>
<span class="info-value" id="detail-instance-name">-</span>
</div>
</div>
</div>
<div class="info-card">
<div class="info-header">
<span class="info-label">오류 메시지</span>
</div>
<div class="info-content">
<div class="error-message" id="detail-error-message">-</div>
</div>
</div>
</div>
<!-- 로그 리스트 -->
<div class="log-list-container">
<div class="log-list-header">
<h3>관련 거래 로그</h3>
<div class="log-search-controls">
<div class="time-range-selector">
<span>조회 시간:</span>
<select id="log-time-range">
<option value="1" selected>±1분</option>
<option value="5">±5분</option>
<option value="10">±10분</option>
<option value="30">±30분</option>
</select>
</div>
<button class="btn btn-primary refresh-logs-btn">
<i class="bi bi-arrow-clockwise"></i> 새로고침
</button>
</div>
</div>
<div class="log-table-container">
<table class="log-table" id="log-grid">
<thead>
<tr>
<th class="narrow-column">처리번호</th>
<th>거래키</th>
<th>업무구분</th>
<th>서비스명</th>
<th>수신시간</th>
<th>처리시간</th>
<th>상태</th>
</tr>
</thead>
<tbody id="log-grid-body">
<!-- 로그 데이터가 동적으로 추가됩니다 -->
<tr>
<td colspan="7" class="empty-message">조회 버튼을 클릭하여 관련 로그를 검색하세요.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary close-modal-btn">닫기</button>
</div>
</div>
</div>
<style>
/* 모달 기본 스타일 */
.modal-modern {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.modal-modern .modal-content {
position: relative;
background-color: #fff;
margin: 2% auto;
padding: 0;
width: 95%;
max-width: 1200px;
border: none;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
animation: modalSlideUp 0.3s;
}
@keyframes modalSlideUp {
from {opacity: 0; transform: translateY(30px);}
to {opacity: 1; transform: translateY(0);}
}
.modal-modern .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e0e0e0;
background-color: #f8f9fa;
border-radius: 16px 16px 0 0;
}
.modal-modern .modal-title {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #333;
}
.modal-modern .close-modal {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.05);
}
.modal-modern .close-modal:hover {
color: #333;
background-color: rgba(0, 0, 0, 0.1);
}
.modal-modern .modal-body {
padding: 1.5rem 2rem;
max-height: 75vh;
overflow-y: auto;
background-color: #f8f9fa;
}
.modal-modern .modal-footer {
padding: 1.2rem 2rem;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: flex-end;
gap: 1rem;
border-radius: 0 0 16px 16px;
background-color: #fff;
}
/* 인터페이스 정보 요약 스타일 */
.interface-info-summary {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.info-card {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.info-header {
padding: 1rem;
background-color: #007bff;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.info-header .info-label {
font-weight: 600;
font-size: 1rem;
}
.info-header .info-value {
font-weight: 600;
font-size: 1.1rem;
}
.info-content {
padding: 1rem;
}
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-row .info-label {
color: #666;
font-size: 0.9rem;
}
.info-row .info-value {
font-weight: 600;
color: #333;
}
.error-message {
padding: 0.5rem;
background-color: rgba(220, 53, 69, 0.1);
border-radius: 6px;
color: #dc3545;
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
max-height: 100px;
overflow-y: auto;
}
/* 로그 리스트 스타일 */
.log-list-container {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.log-list-header {
padding: 1rem;
background-color: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.log-list-header h3 {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
color: #333;
}
.log-search-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.time-range-selector {
display: flex;
align-items: center;
gap: 0.5rem;
}
.time-range-selector select {
padding: 0.25rem 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
}
.log-table-container {
max-height: 400px;
overflow-y: auto;
}
.log-table {
width: 100%;
border-collapse: collapse;
}
.log-table th {
top: 0;
background-color: #007bff;
color: white;
padding: 0.75rem;
text-align: left;
font-weight: 800;
z-index: 10;
}
.log-table td {
padding: 0.75rem;
border-bottom: 1px solid #e0e0e0;
}
.log-table tbody tr:hover {
background-color: rgba(0, 0, 0, 0.03);
}
.log-table .empty-message {
text-align: center;
padding: 2rem;
color: #888;
font-style: italic;
}
/* 로딩 인디케이터 */
.loading-indicator {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid #007bff;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 버튼 스타일 */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0069d9;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
/* 오류 상태 표시 */
.table-status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
text-align: center;
min-width: 60px;
}
.table-status-error {
background-color: #dc3545;
color: white;
}
.table-status-normal {
background-color: #28a745;
color: white;
}
.log-table th.narrow-column {
width: 80px;
}
.highlight-row {
background-color: rgba(0, 123, 255, 0.1);
}
/* 반응형 설정 */
@media (max-width: 992px) {
.interface-info-summary {
grid-template-columns: 1fr;
}
.modal-modern .modal-content {
width: 98%;
margin: 1% auto;
}
}
</style>
<script>
(function() {
// 모달 요소
const modal = document.getElementById('interfaceLogDetailModal');
const closeButtons = modal.querySelectorAll('.close-modal, .close-modal-btn');
const refreshButton = modal.querySelector('.refresh-logs-btn');
const timeRangeSelect = document.getElementById('log-time-range');
// 데이터 상태
let currentInterfaceId = '';
let currentErrorTime = '';
let currentInstanceName = '';
let currentErrorMessage = '';
let currentServiceType = '';
// 모달 닫기 이벤트
closeButtons.forEach(button => {
button.addEventListener('click', () => {
modal.style.display = 'none';
});
});
// 모달 외부 클릭 시 닫기
window.addEventListener('click', (event) => {
if (event.target === modal) {
modal.style.display = 'none';
}
});
// 새로고침 버튼 이벤트
refreshButton.addEventListener('click', () => {
loadInterfaceLogs();
});
// 로그 시간대 변경 이벤트
timeRangeSelect.addEventListener('change', () => {
loadInterfaceLogs();
});
// 인터페이스 로그 로드 함수
function loadInterfaceLogs() {
if (!currentInterfaceId || !currentErrorTime) {
showEmptyLogGrid("인터페이스 정보가 올바르지 않습니다.");
return;
}
// 로딩 표시
showLoadingLogGrid();
// 시간 범위 계산
const timeRangeMinutes = parseInt(timeRangeSelect.value);
const errorDate = parseErrorDateTime(currentErrorTime);
if (!errorDate) {
showEmptyLogGrid("시간 형식이 올바르지 않습니다.");
return;
}
// 시작 시간과 종료 시간 계산 (±timeRangeMinutes 분)
const startTime = new Date(errorDate.getTime() - timeRangeMinutes * 60 * 1000);
const endTime = new Date(errorDate.getTime() + timeRangeMinutes * 60 * 1000);
// API 호출 시간 형식으로 변환 (YYYYMMDDHHMMSS)
const startTimeStr = formatDateTimeForApi(startTime);
const endTimeStr = formatDateTimeForApi(endTime);
// ModernDashboardApi를 사용하여 로그 데이터 가져오기
ModernDashboardApi.getInterfaceLogs(
currentInterfaceId,
startTimeStr,
endTimeStr,
currentServiceType,
function(response) {
if (response && response.rows) {
updateLogGrid(response.rows);
} else {
showEmptyLogGrid("조회된 로그가 없습니다.");
}
},
function(xhr, status, error) {
console.error("로그 데이터 로드 실패:", error);
showEmptyLogGrid("로그 데이터를 불러오는 중 오류가 발생했습니다.");
}
);
}
// 로그 그리드 업데이트
function updateLogGrid(logs) {
const tbody = document.getElementById('log-grid-body');
if (!logs || logs.length === 0) {
showEmptyLogGrid("조회된 로그가 없습니다.");
return;
}
let html = '';
logs.forEach(log => {
const isError = log.ERRYN === 'Y';
// 시간 형식 변환 (YYYYMMDDHHMMSS -> YYYY-MM-DD HH:MM:SS)
const recvTime = formatDateTime(log.MSGDPSTYMS_TMP);
const procTime = formatDateTime(log.MSGPRCSSYMS);
html += '<tr>' +
'<td>' + (log.LOGPRCSSSERNO || '-') + '</td>' +
'<td>' + (log.KEYMGTMSGCTNT || '-') + '</td>' +
'<td>' + (log.EAIBZWKDSTCD_TMP || '-') + '</td>' +
'<td>' + (log.EAISVCNAME || '-') + '</td>' +
'<td>' + (recvTime || '-') + '</td>' +
'<td>' + (procTime || '-') + '</td>' +
'<td><span class="table-status-badge ' + (isError ? 'table-status-error' : 'table-status-normal') + '">' +
(isError ? '오류' : '정상') + '</span>' +
'<input type="hidden" value="' + (log.EAISVCSERNO_TMP || '') + '"></td>' +
'</tr>';
});
tbody.innerHTML = html;
// 현재 인터페이스 ID를 포함한 행 강조 표시
highlightRows(currentInterfaceId);
}
// 특정 인터페이스 ID를 포함한 행 강조 표시
function highlightRows(interfaceId) {
if (!interfaceId) return;
const tbody = document.getElementById('log-grid-body');
const rows = tbody.querySelectorAll('tr');
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length > 1) {
const keyCell = cells[1].textContent;
if (keyCell && keyCell.includes(interfaceId)) {
row.classList.add('highlight-row');
}
}
});
}
// 로딩 중 표시
function showLoadingLogGrid() {
const tbody = document.getElementById('log-grid-body');
tbody.innerHTML =
'<tr>' +
'<td colspan="7">' +
'<div class="loading-indicator">' +
'<div class="loading-spinner"></div>' +
'</div>' +
'</td>' +
'</tr>';
}
// 빈 그리드 표시
function showEmptyLogGrid(message) {
const tbody = document.getElementById('log-grid-body');
tbody.innerHTML =
'<tr>' +
'<td colspan="7" class="empty-message">' + (message || '조회된 데이터가 없습니다.') + '</td>' +
'</tr>';
}
// 시간 문자열 파싱 (예: "0312 10:52:32.009")
function parseErrorDateTime(dateTimeStr) {
try {
if (!dateTimeStr) return new Date();
// "0312 10:52:32.009" 형식 처리
if (dateTimeStr.includes(' ') && dateTimeStr.length >= 16) {
const datePart = dateTimeStr.split(' ')[0]; // "0312"
const timePart = dateTimeStr.split(' ')[1].split('.')[0]; // "10:52:32"
const month = parseInt(datePart.substring(0, 2));
const day = parseInt(datePart.substring(2, 4));
const timeParts = timePart.split(':');
const hour = parseInt(timeParts[0]);
const minute = parseInt(timeParts[1]);
const second = parseInt(timeParts[2]);
// 현재 연도 사용
const currentYear = new Date().getFullYear();
return new Date(currentYear, month - 1, day, hour, minute, second);
}
// 기존 "2023-03-12 15:30:45" 형식 처리 (이전 코드 유지)
const parts = dateTimeStr.split(/[- :]/);
if (parts.length >= 6) {
return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]);
}
return new Date();
} catch (e) {
console.error("날짜 파싱 오류:", e);
return new Date();
}
}
// API 호출용 날짜 형식 변환 (YYYYMMDDHHMMSS)
function formatDateTimeForApi(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return '' + year + month + day + hours + minutes + seconds;
}
// 화면 표시용 날짜 형식 변환 (YYYY-MM-DD HH:MM:SS)
function formatDateTime(dateTimeStr) {
if (!dateTimeStr || dateTimeStr.length < 14) return '';
const year = dateTimeStr.substr(0, 4);
const month = dateTimeStr.substr(4, 2);
const day = dateTimeStr.substr(6, 2);
const hour = dateTimeStr.substr(8, 2);
const minute = dateTimeStr.substr(10, 2);
const second = dateTimeStr.substr(12, 2);
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
}
// 모달 열기 함수 (전역 스코프에 노출)
window.openInterfaceLogDetailModal = function(interfaceId, errorTime, instanceName, errorMessage, serviceType) {
// 데이터 설정
currentInterfaceId = interfaceId || '';
currentErrorTime = errorTime || '';
currentInstanceName = instanceName || '';
currentErrorMessage = errorMessage || '';
currentServiceType = serviceType || '';
// 조회 기준 일/시 분리해서 표시
let formattedErrorTime = currentErrorTime;
if (currentErrorTime && currentErrorTime.includes(' ')) {
const datePart = currentErrorTime.split(' ')[0]; // "0312"
const timePart = currentErrorTime.split(' ')[1]; // "10:52:32.009"
// 월/일 형식으로 변환
const month = datePart.substring(0, 2);
const day = datePart.substring(2, 4);
formattedErrorTime = month + '월 ' + day + '일 ' + timePart;
}
// UI 업데이트
document.getElementById('detail-interface-id').textContent = currentInterfaceId;
document.getElementById('detail-error-time').textContent = formattedErrorTime;
document.getElementById('detail-instance-name').textContent = currentInstanceName;
document.getElementById('detail-error-message').textContent = currentErrorMessage;
// 모달 표시
modal.style.display = 'block';
// 로그 데이터 로드
loadInterfaceLogs();
};
})();
</script>
@@ -0,0 +1,243 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<div class="dashboard-peak">
<h2 class="section-title">피크 처리 성능
<button class="peak-filter-btn active" id="peakTps">tps</button>
<button class="peak-filter-btn" id="peakTrade">trade</button>
</h2>
<div class="card">
<div class="card-title">
</div>
<div class="peak-grid" id="peak-tps-container">
<!-- 피크 성능 데이터가 동적으로 추가됩니다 -->
</div>
<div class="peak-grid" id="peak-trade-container" style="display:none">
<!-- 피크 성능 데이터가 동적으로 추가됩니다 -->
</div>
</div>
</div>
<!-- 피크 성능 카드 템플릿 -->
<template id="peak-card-template">
<div class="peak-card">
<div class="peak-header">
<div class="peak-header-content">
<h4 class="peak-service-type">서비스 유형</h4>
<span class="peak-tps">tps</span>
<span class="peak-date">날짜</span>
</div>
</div>
</div>
</template>
<style>
.dashboard-peak {
position: relative;
}
/* FEP와 BAP를 상하로 배치하기 위한 스타일 */
.peak-grid {
display: flex;
flex-direction: column;
gap: 0.3rem;
height: 263px; /* 배치 처리 성능 높이와 일관되게 설정 */
overflow-y: auto; /* 내용이 많을 경우 스크롤 가능하도록 */
}
.peak-card {
margin-bottom: 0.3rem;
height: auto; /* 자동 높이 설정 */
background-color: white;
border-radius: var(--rounded-md);
border: 1px solid var(--color-gray-200);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.peak-card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
/* 좁은 공간에 맞게 조정 */
.peak-header {
margin-bottom: 0.1rem;
padding: 0.3rem 0.5rem; /* 패딩 더 축소 */
border-bottom: 1px solid var(--color-gray-200);
}
.peak-header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.peak-service-type {
font-size: 0.85rem;
margin: 0;
font-weight: 600;
color: var(--color-gray-800);
}
.peak-date {
font-size: 0.75rem;
color: var(--color-gray-500);
}
.peak-content {
padding: 0.3rem 0.5rem; /* 패딩 더 축소 */
display: flex;
align-items: center;
}
.peak-value-container {
flex: 1;
}
.peak-value {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-gray-900);
}
.peak-label {
font-size: 0.75rem;
color: var(--color-gray-500);
}
/* 차트 높이 조정 */
.peak-chart {
height: 40px; /* 차트 높이 더 축소 */
flex: 1;
display: flex;
justify-content: center;
align-items: flex-end;
}
.peak-bar-container {
height: 100%;
width: 40px;
display: flex;
justify-content: center;
align-items: flex-end;
}
.peak-bar {
width: 30px;
background-color: var(--color-primary);
border-radius: 3px 3px 0 0;
}
/* 피크 섹션 스타일 조정 */
.peak-container .card {
height: 100%;
}
.peak-tps {
font-size: 1rem;
margin-left: auto;
margin-right: 8px;
font-weight: 600;
color: var(--color-gray-900);
display: flex;
align-items: end;
}
</style>
<script>
// 숫자 형식화 함수 (천 단위 콤마 추가)
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// 피크 성능 데이터 업데이트 함수
function updatePeakPerformance(data) {
if (!data || !data.peakTps || !data.peakTrade) return;
var container = document.getElementById('peak-tps-container');
container.innerHTML = '';
// 최대값 찾기 (스케일링을 위해)
var maxValue = 0;
data.peakTps.forEach(function(peak) {
var value = peak.tps;
if (value > maxValue) maxValue = value;
});
// 피크 카드 생성
data.peakTps.forEach(function(peak) {
var template = document.getElementById('peak-card-template');
var clone = document.importNode(template.content, true);
// 서비스 유형 설정
clone.querySelector('.peak-service-type').textContent = peak.serviceType;
// tps 설정
clone.querySelector('.peak-tps').textContent = (0 || peak.tps);
// 날짜 설정
clone.querySelector('.peak-date').textContent = peak.formattedDate || peak.date;
container.appendChild(clone);
});
var tradeContainer = document.getElementById('peak-trade-container');
tradeContainer.innerHTML = '';
// 피크 카드 생성
data.peakTrade.forEach(function(peak) {
var template = document.getElementById('peak-card-template');
var clone = document.importNode(template.content, true);
// 서비스 유형 설정
clone.querySelector('.peak-service-type').textContent = peak.serviceType;
// tps 설정
clone.querySelector('.peak-tps').textContent = (0 || peak.tps);
// 날짜 설정
clone.querySelector('.peak-date').textContent = peak.formattedDate || peak.date;
tradeContainer.appendChild(clone);
});
}
// 피크 성능 데이터 로드 함수
function loadPeakPerformanceData() {
// 항상 TPS 데이터 로드 (선택 버튼 제거)
ModernDashboardApi.getPeakPerformance('tps', function(data) {
updatePeakPerformance(data);
});
}
// 서버 필터 버튼 클릭 이벤트
$('.peak-filter-btn').click(function() {
var type = $(this).prop("id");
if (type === 'peakTps') {
// 전체 표시
$("#peakTrade").removeClass('active');
$("#peakTps").addClass('active');
$("#peak-tps-container").show();
$("#peak-trade-container").hide();
} else if( type === 'peakTrade' ){
// 필터링
$("#peakTps").removeClass('active');
$("#peakTrade").addClass('active');
$("#peak-tps-container").hide();
$("#peak-trade-container").show();
}
});
// 초기 로드
$(document).ready(function() {
loadPeakPerformanceData();
});
</script>
@@ -0,0 +1,934 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*" %>
<%@ page import="com.eactive.eai.rms.common.datasource.*" %>
<%@ page import="java.util.stream.Collectors" %>
<div class="dashboard-servers">
<div class="dashboard-header">
<h2 class="section-title">서버 상태</h2>
<div class="server-filters">
<div class="filter-group">
<button class="filter-btn active" data-type="all">전체</button>
<%
// serverTypeMap 변수를 가져오기
Map<String, List<DataSourceType>> serverTypeMap =
(Map<String, List<DataSourceType>>)request.getAttribute("serverTypeMap");
if (serverTypeMap == null) {
// application 스코프에서 가져오기 시도
serverTypeMap = (Map<String, List<DataSourceType>>)application.getAttribute("serverTypeMap");
}
Map<String, Integer> priorityMap = new HashMap<>();
priorityMap.put("MCI", 0);
priorityMap.put("EAI", 1);
priorityMap.put("FEP", 2);
priorityMap.put("CGW", 3);
priorityMap.put("FGW", 4);
priorityMap.put("API", 5);
priorityMap.put("APIGW", 6);
priorityMap.put("ONL", 7);
Map<String,List<DataSourceType>> sortedMap = serverTypeMap.entrySet().stream()
.sorted(Comparator.comparingInt(e -> priorityMap.getOrDefault(e.getKey(), Integer.MAX_VALUE)))
.collect(Collectors.toMap(
Map.Entry::getKey
, Map.Entry::getValue
,(e1, e2) -> e1
, LinkedHashMap::new
));
if (sortedMap != null) {
// 서버 타입 필터 생성
for (Map.Entry<String, List<DataSourceType>> entry : sortedMap.entrySet()) {
String serverType = entry.getKey();
List<DataSourceType> servers = entry.getValue();
// BAT 서버는 제외
if (DataSourceTypeManager.BAT.equals(serverType) || DataSourceTypeManager.BAP.equals(serverType)) {
continue;
}
// 서버 타입에 해당하는 텍스트 가져오기 (첫 번째 서버의 텍스트 사용)
String serverTypeText = servers.size() > 0 ? servers.get(0).getText() : serverType;
%>
<button class="filter-btn" data-type="<%= serverType.toLowerCase() %>"><%= serverTypeText %></button>
<% }
} %>
</div>
<div class="search-container">
<i class="bi bi-search"></i>
<input type="text" id="server-search" placeholder="서버 검색..." />
</div>
</div>
</div>
<%
if (sortedMap != null) {
// 서버 타입별로 섹션 생성
for (Map.Entry<String, List<DataSourceType>> entry : sortedMap.entrySet()) {
String serverType = entry.getKey();
List<DataSourceType> servers = entry.getValue();
// BAT 서버는 제외
if (DataSourceTypeManager.BAT.equals(serverType) || DataSourceTypeManager.BAP.equals(serverType)) {
continue;
}
// 서버 타입에 해당하는 텍스트 가져오기 (첫 번째 서버의 텍스트 사용)
String serverTypeText = servers.size() > 0 ? servers.get(0).getText() : serverType;
// 서버 타입별 보더 색상 클래스
String borderColorClass = "";
if (serverType.equalsIgnoreCase("FEP")) {
borderColorClass = "border-primary";
} else if (serverType.equalsIgnoreCase("EAI")) {
borderColorClass = "border-success";
} else if (serverType.equalsIgnoreCase("EMS")) {
borderColorClass = "border-purple";
} else if (serverType.equalsIgnoreCase("EDI")) {
borderColorClass = "border-orange";
}
%>
<div class="server-section" id="server-section-<%= serverType.toLowerCase() %>">
<div class="card server-card-container <%= borderColorClass %>">
<div class="server-section-header">
<h3 class="server-section-title"><%= serverTypeText %> 서버</h3>
<span class="server-status-badge badge-success <%= serverType.toLowerCase() %>-status-badge">0 / 0 정상</span>
</div>
<div class="server-grid server-grid-<%= serverType.toLowerCase() %>" id="<%= serverType.toLowerCase() %>-servers-container">
<!-- 여기에 <%= serverTypeText %> 서버 카드가 동적으로 추가됩니다 -->
</div>
</div>
</div>
<% }
} %>
</div>
<!-- 서버 카드 템플릿 -->
<template id="server-card-template">
<div class="server-card">
<div class="server-header">
<h5 class="server-name">서버 이름</h5>
<span class="server-status-badge badge-success">정상</span>
</div>
<div class="server-metrics">
<!-- 메트릭 헤더 행 -->
<div class="metrics-row">
<!-- CPU 메트릭 -->
<div class="server-metric">
<div class="metric-header">
<span class="metric-name"><i class="bi bi-cpu" style="color: #0dcaf0; font-size: 1.1rem;"></i></span>
<span class="metric-value cpu-value">0%</span>
</div>
</div>
<!-- 메모리 메트릭 -->
<div class="server-metric">
<div class="metric-header">
<span class="metric-name"><i class="bi bi-memory" style="color: #6f42c1; font-size: 1.1rem;"></i></span>
<span class="metric-value memory-value">0%</span>
</div>
</div>
<!-- 디스크 메트릭 -->
<div class="server-metric">
<div class="metric-header">
<span class="metric-name"><i class="bi bi-hdd" style="color: #fd7e14; font-size: 1.1rem;"></i></span>
<span class="metric-value disk-value">0%</span>
</div>
</div>
</div>
<!-- 스파크라인 행 -->
<div class="sparklines-row">
<div class="sparkline cpu-sparkline">
<div class="sparkline-bar success" style="height: 10%"></div>
<div class="sparkline-bar success" style="height: 15%"></div>
<div class="sparkline-bar success" style="height: 20%"></div>
<div class="sparkline-bar success" style="height: 15%"></div>
<div class="sparkline-bar success" style="height: 25%"></div>
<div class="sparkline-bar success" style="height: 20%"></div>
<div class="sparkline-bar success" style="height: 15%"></div>
</div>
<div class="sparkline memory-sparkline">
<div class="sparkline-bar success" style="height: 20%"></div>
<div class="sparkline-bar success" style="height: 25%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 25%"></div>
<div class="sparkline-bar success" style="height: 35%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 25%"></div>
</div>
<div class="sparkline disk-sparkline">
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
</div>
</div>
</div>
<!-- 인스턴스 테이블 -->
<div class="server-instances">
<table class="instance-table">
<thead>
<tr>
<th>인스턴스 ID</th>
<th>상태</th>
<th class="text-right">처리량</th>
</tr>
</thead>
<tbody class="instance-table-body">
<!-- 인스턴스 데이터가 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
</div>
</template>
<!-- 인스턴스 행 템플릿 -->
<template id="instance-row-template">
<tr>
<td>
<span class="instance-status status-normal"></span>
<span class="instance-id">인스턴스 ID</span>
</td>
<td class="instance-status-text">정상</td>
<td class="text-right instance-throughput">0/s</td>
</tr>
</template>
<style>
/* 대시보드 헤더 스타일 */
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--color-gray-200);
}
.dashboard-header .section-title {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
flex-shrink: 0;
margin-right: 0.75rem;
}
.dashboard-header .server-filters {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0;
flex-grow: 1;
}
.dashboard-header .filter-group {
flex-shrink: 0;
}
.dashboard-header .search-container {
margin-left: auto;
}
/* 서버 상태 섹션 */
.server-section {
margin-bottom: 0.75rem;
}
.server-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.server-section-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--color-gray-800);
margin: 0;
}
.server-filters {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
gap: 0.5rem;
}
.filter-group {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.server-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 0.5rem;
}
.server-card {
background-color: white;
border-radius: var(--rounded-md);
box-shadow: var(--shadow);
border: 1px solid var(--color-gray-200);
overflow: hidden;
transition: all 0.2s ease;
height: 100%;
padding: 0.75rem;
}
.server-card:hover {
box-shadow: var(--shadow-md);
}
.server-header {
padding: 1rem 1.25rem;
background-color: var(--color-gray-50);
border-bottom: 1px solid var(--color-gray-200);
display: flex;
align-items: center;
justify-content: space-between;
}
.server-name {
font-weight: 600;
font-size: 1rem;
color: var(--color-gray-800);
margin: 0;
}
.server-metrics {
padding: 1rem 1.25rem;
display: flex;
gap: 1.5rem;
}
.server-metric {
flex: 1;
}
.metric-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.metric-name {
font-size: 0.75rem;
color: var(--color-gray-500);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.server-instances {
padding: 0 16px 16px 16px;
flex: 1;
overflow-y: auto;
max-height: 150px;
}
/* 스파크라인 스타일 */
.sparkline {
height: 28px;
display: flex;
align-items: flex-end;
gap: 2px;
margin-top: 0.5rem;
background-color: var(--color-gray-50);
border-radius: var(--rounded-sm);
padding: 0.25rem;
}
.sparkline-bar {
flex: 1;
background-color: var(--color-primary);
border-radius: 1px 1px 0 0;
}
.sparkline-bar.success {
background-color: var(--color-success);
}
.sparkline-bar.warning {
background-color: var(--color-warning);
}
.sparkline-bar.danger {
background-color: var(--color-danger);
}
/* 요약 섹션 */
.status-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.status-card {
display: flex;
align-items: center;
padding: 1.5rem;
border-radius: var(--rounded-lg);
background-color: white;
box-shadow: var(--shadow);
transition: all 0.2s ease;
}
.status-card:hover {
box-shadow: var(--shadow-md);
}
.status-card.success {
border-left: 4px solid var(--color-success);
}
.status-card.warning {
border-left: 4px solid var(--color-warning);
}
.status-card.danger {
border-left: 4px solid var(--color-danger);
}
.status-icon {
font-size: 2rem;
margin-right: 1.25rem;
display: flex;
align-items: center;
}
.status-icon.success {
color: var(--color-success);
}
.status-icon.warning {
color: var(--color-warning);
}
.status-icon.danger {
color: var(--color-danger);
}
.status-content {
display: flex;
flex-direction: column;
}
.status-label {
font-size: 0.875rem;
color: var(--color-gray-500);
margin-bottom: 0.25rem;
}
.status-value {
font-size: 1.5rem;
font-weight: 600;
color: var(--color-gray-900);
}
/* 서버 섹션 스타일 - 기존 스타일 유지 */
.server-section {
margin-bottom: 1rem;
}
.server-card-container {
padding: 0.75rem;
margin-bottom: 1rem;
min-height: 296px;
}
.server-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.server-section-title {
font-size: 1rem;
font-weight: 600;
color: var(--color-gray-800);
margin: 0;
}
/* 서버 타입별 보더 색상 */
.border-primary {
border-top: 2px solid #0d6efd;
}
.border-success {
border-top: 2px solid #198754;
}
.border-purple {
border-top: 2px solid #6f42c1;
}
.border-orange {
border-top: 2px solid #fd7e14;
}
/* 서버 그리드 - 한 줄에 3개씩 표시 */
.server-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
/* 반응형 처리 - 화면 크기에 따라 그리드 조정 */
@media screen and (max-width: 1200px) {
.server-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (max-width: 768px) {
.server-grid {
grid-template-columns: 1fr;
}
}
/* 서버 카드 높이 축소 */
.server-card {
padding: 0.5rem;
border: 1px solid var(--color-gray-200);
border-radius: var(--rounded-sm);
background-color: white;
}
.server-header {
padding: 0.4rem 0.5rem;
margin-bottom: 0.3rem;
border-bottom: 1px solid var(--color-gray-100);
}
.server-name {
font-size: 0.95rem;
}
/* 메트릭 스타일 조정 */
.server-metrics {
padding: 0.3rem 0;
display: flex;
flex-direction: column;
gap: 0.0rem;
}
.metrics-row {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
.sparklines-row {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
.server-metric {
flex: 1;
min-width: 0; /* 오버플로우 방지 */
}
.metric-header {
margin-bottom: 0.2rem;
display: flex;
justify-content: flex-start;
align-items: center;
white-space: nowrap;
overflow: hidden;
}
.sparkline {
flex: 1;
min-width: 0; /* 오버플로우 방지 */
}
.metric-name {
display: flex;
align-items: center;
}
.metric-value {
margin-left: 0.5rem;
}
/* 인스턴스 테이블 스타일 */
.server-instances {
padding: 0.3rem 0;
max-height: 120px;
}
.instance-table {
font-size: 0.8rem;
}
.instance-table th,
.instance-table td {
padding: 0.2rem 0.3rem;
white-space: nowrap;
}
/* 인스턴스 상태 점 스타일 */
.instance-status {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 4px;
}
/* 오류 상태일 때 인스턴스 ID 색상 변경 */
tr:has(.status-error) .instance-id,
tr:has(.status-inactive) .instance-id {
color: #dc3545;
font-weight: 600;
}
</style>
<script>
// 서버 카드 생성 함수
function createServerCard(server, containerId) {
var template = document.getElementById('server-card-template');
var container = document.getElementById(containerId);
if (!container) return null;
// 템플릿 복제
var clone = document.importNode(template.content, true);
// 서버 이름 설정
clone.querySelector('.server-name').textContent = server.text || server.name;
// 컨테이너에 추가
container.appendChild(clone);
return container.lastElementChild;
}
// 인스턴스 행 생성 함수
function createInstanceRow(instanceData) {
var template = document.getElementById('instance-row-template');
var clone = document.importNode(template.content, true);
// 인스턴스 ID 설정
clone.querySelector('.instance-id').textContent = instanceData.id;
// 상태 설정
var statusClass = 'status-normal';
var statusText = '정상';
if (instanceData.status === 'warning') {
statusClass = 'status-warning';
statusText = '경고';
} else if (instanceData.status === 'error' || instanceData.status === 'inactive') {
statusClass = 'status-error';
statusText = instanceData.status === 'inactive' ? '비활성' : '오류';
}
clone.querySelector('.instance-status').className = 'instance-status ' + statusClass;
clone.querySelector('.instance-status-text').textContent = statusText;
// 처리량 설정
if (instanceData.throughput) {
clone.querySelector('.instance-throughput').textContent = instanceData.throughput;
}
return clone;
}
// 서버 상태 업데이트 함수
function updateServerStatus(data) {
if (!data || !data.servers) return;
var normalCount = 0;
var warningCount = 0;
var errorCount = 0;
// 서버 타입별 상태 카운트
var typeStatusCount = {};
// 각 서버 상태 업데이트
data.servers.forEach(function(server) {
// 서버 타입 추출 (서버 이름에서 괄호 안의 텍스트 추출)
var serverTypeMatch = server.text.match(/\(([^)]+)\)/);
var serverType = serverTypeMatch ? serverTypeMatch[1] : "unknown";
// 서버 이름에서 타입 정보 제거
var displayName = server.name;
// 서버 상태
var serverStatus = server.status;
// 서버 타입별 상태 카운트 초기화
if (!typeStatusCount[serverType]) {
typeStatusCount[serverType] = {
normal: 0,
warning: 0,
error: 0,
total: 0
};
}
typeStatusCount[serverType].total++;
// 전체 상태 카운트 업데이트
if (serverStatus === 'normal') {
normalCount++;
typeStatusCount[serverType].normal++;
} else if (serverStatus === 'warning') {
warningCount++;
typeStatusCount[serverType].warning++;
} else if (serverStatus === 'error') {
errorCount++;
typeStatusCount[serverType].error++;
}
// 서버 타입 소문자로 변환 (ID용)
var serverTypeId = serverType.toLowerCase().replace(/\s+/g, '-');
// 서버 카드 찾기 및 업데이트
var serverContainerId = serverTypeId + '-servers-container';
var serverContainer = document.getElementById(serverContainerId);
if (!serverContainer) return;
// 서버 카드가 없으면 생성
var serverCard = document.querySelector('#' + serverContainerId + ' .server-card[data-server-name="' + server.name + '"]');
if (!serverCard) {
serverCard = createServerCard(server, serverContainerId);
if (serverCard) {
serverCard.setAttribute('data-server-name', server.name);
}
}
if (!serverCard) return;
// 서버 이름 업데이트
var serverNameElement = serverCard.querySelector('.server-name');
if (serverNameElement) {
serverNameElement.textContent = displayName;
}
// 서버 상태 업데이트
var statusBadge = serverCard.querySelector('.server-status-badge');
statusBadge.className = 'server-status-badge';
if (serverStatus === 'normal') {
statusBadge.classList.add('badge-success');
statusBadge.textContent = '정상';
} else if (serverStatus === 'warning') {
statusBadge.classList.add('badge-warning');
statusBadge.textContent = '경고';
} else if (serverStatus === 'error') {
statusBadge.classList.add('badge-danger');
statusBadge.textContent = '오류';
}
// CPU, 메모리, 디스크 업데이트
updateResourceValue(serverCard, 'cpu', server.cpu);
updateResourceValue(serverCard, 'memory', server.memory);
updateResourceValue(serverCard, 'disk', server.disk);
// 인스턴스 테이블 업데이트
var instanceTableBody = serverCard.querySelector('.instance-table-body');
instanceTableBody.innerHTML = '';
if (server.instances && server.instances.length > 0) {
server.instances.forEach(function(instance) {
instanceTableBody.appendChild(createInstanceRow(instance));
});
}
});
// 요약 정보 업데이트
if (document.getElementById('normal-servers')) {
document.getElementById('normal-servers').textContent = normalCount;
}
if (document.getElementById('warning-servers')) {
document.getElementById('warning-servers').textContent = warningCount;
}
if (document.getElementById('error-servers')) {
document.getElementById('error-servers').textContent = errorCount;
}
// 서버 타입별 상태 배지 업데이트
for (var type in typeStatusCount) {
var typeId = type.toLowerCase().replace(/\s+/g, '-');
var badgeElement = document.querySelector('.' + typeId + '-status-badge');
if (badgeElement) {
var countData = typeStatusCount[type];
badgeElement.textContent = countData.normal + ' / ' + countData.total + ' 정상';
badgeElement.className = typeId + '-status-badge server-status-badge';
if (countData.error > 0) {
badgeElement.classList.add('badge-danger');
} else if (countData.warning > 0) {
badgeElement.classList.add('badge-warning');
} else {
badgeElement.classList.add('badge-success');
}
}
}
}
// 리소스 값 업데이트 함수 (CPU, 메모리, 디스크)
function updateResourceValue(serverCard, resourceType, value) {
var valueElement = serverCard.querySelector('.' + resourceType + '-value');
if (!valueElement) return;
valueElement.textContent = value + '%';
valueElement.className = 'metric-value ' + resourceType + '-value';
// 값에 따라 클래스 추가
if (value >= 90) {
valueElement.classList.add('danger');
} else if (value >= 70) {
valueElement.classList.add('warning');
} else {
valueElement.classList.add('success');
}
// 스파크라인 업데이트
var sparkline = serverCard.querySelector('.' + resourceType + '-sparkline');
if (!sparkline) return;
// 서버 이름 가져오기
var serverName = serverCard.getAttribute('data-server-name');
if (!serverName) return;
// 서버 리소스 시계열 데이터 가져오기
ModernDashboardApi.getServerResourceData(serverName, function(data) {
if (data && data.data) {
var resourceData = data.data;
var resourceValues = resourceData[resourceType];
// 스파크라인 업데이트
sparkline.innerHTML = '';
if (resourceValues && resourceValues.length > 0) {
resourceValues.forEach(function(val) {
var bar = document.createElement('div');
bar.className = 'sparkline-bar';
bar.style.height = val + '%';
// 값에 따라 클래스 추가
if (val >= 90) {
bar.classList.add('danger');
} else if (val >= 70) {
bar.classList.add('warning');
} else {
bar.classList.add('success');
}
sparkline.appendChild(bar);
});
} else {
// 데이터가 없는 경우 기본 스파크라인 생성
for (var i = 0; i < 7; i++) {
var bar = document.createElement('div');
bar.className = 'sparkline-bar success';
bar.style.height = value + '%';
sparkline.appendChild(bar);
}
}
} else {
// API 응답이 없는 경우 기본 스파크라인 생성
sparkline.innerHTML = '';
for (var i = 0; i < 7; i++) {
var bar = document.createElement('div');
bar.className = 'sparkline-bar success';
bar.style.height = value + '%';
sparkline.appendChild(bar);
}
}
});
}
// 서버 데이터 로드 함수
function loadServerData() {
console.log("서버 데이터 로드 시작: " + new Date().toLocaleTimeString());
ModernDashboardApi.getServerStatus(null, function(data) {
updateServerStatus(data);
console.log("서버 데이터 로드 완료: " + new Date().toLocaleTimeString());
});
}
// 서버 필터 버튼 클릭 이벤트
$('.filter-btn').click(function() {
var type = $(this).data('type');
if (type === 'all') {
// 전체 표시
$('.server-section').show();
$('.filter-btn').removeClass('active');
$(this).addClass('active');
$(".tran-filter-btn[data-type='all']").click();
} else {
// 필터링
$('.server-section').hide();
$('#server-section-' + type).show();
$('.filter-btn').removeClass('active');
$(this).addClass('active');
$(".tran-filter-btn[data-type='" + type + "']").click();
}
});
// 서버 검색 기능
$('#server-search').on('input', function() {
var searchText = $(this).val().toLowerCase();
if (searchText.length > 0) {
$('.server-card').each(function() {
var serverName = $(this).find('.server-name').text().toLowerCase();
if (serverName.indexOf(searchText) > -1) {
$(this).show();
} else {
$(this).hide();
}
});
} else {
$('.server-card').show();
}
});
// 초기 로드
$(document).ready(function() {
// 서버 데이터 로드
loadServerData();
// 이벤트 리스너 등록 - 중앙 이벤트 관리자 사용
if (window.DashboardEventManager) {
// 초기화 이벤트 리스너
window.DashboardEventManager.on('init', function() {
console.log("서버 상태 초기화 이벤트 수신");
// 이미 초기화되었으므로 추가 작업 필요 없음
});
// 새로고침 이벤트 리스너
window.DashboardEventManager.on('refresh', function() {
console.log("서버 상태 새로고침 이벤트 수신");
loadServerData();
});
} else {
console.error("DashboardEventManager가 정의되지 않았습니다. modern-dashboard.js가 먼저 로드되었는지 확인하세요.");
}
});
</script>
@@ -0,0 +1,267 @@
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<div class="dashboard-sms-summary">
<h2 class="section-title">장애통보(SMS)</h2>
<div class="card shadow mb-0">
<div class="sms-table-container">
<table class="sms-table">
<thead>
<tr>
<th width="18%">인스턴스</th>
<th width="8%">유형</th>
<th width="12%">시간</th>
<th width="10%">건수</th>
<th width="20%">오류대상</th>
<th>메시지</th>
</tr>
</thead>
<tbody id="smsTableBody">
<!-- 데이터가 여기에 동적으로 로드됩니다 -->
</tbody>
</table>
</div>
</div>
</div>
<style>
/* SMS 섹션 스타일 */
.dashboard-sms-summary .section-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.dashboard-sms-summary .card {
border-radius: 0.35rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
}
.sms-table-container {
max-height: 220px;
overflow-y: auto;
}
.sms-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.sms-table th {
background-color: #f8f9fc;
font-weight: 600;
text-align: left;
padding: 0.5rem;
border-bottom: 1px solid #e3e6f0;
position: sticky;
top: 0;
z-index: 10;
}
.sms-table td {
padding: 0.5rem;
border-bottom: 1px solid #f0f0f0;
vertical-align: middle;
}
.sms-table tr:last-child td {
border-bottom: none;
}
.sms-table tr:hover {
background-color: rgba(0, 0, 0, 0.02);
}
.error-type-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
}
.error-type-icon.adapter {
background-color: #4e73df;
color: white;
}
.error-type-icon.interface {
background-color: #e74a3b;
color: white;
}
.error-type-icon i {
font-size: 0.8rem;
}
.error-target {
font-family: monospace;
padding: 0.2rem 0.4rem;
background-color: #f8f9fc;
border-radius: 0.2rem;
font-size: 0.8rem;
}
.text-danger {
color: #e74a3b !important;
}
.empty-message {
padding: 1rem;
text-align: center;
color: #6c757d;
font-style: italic;
}
.occurrence-count {
display: inline-block;
padding: 0.2rem 0.5rem;
background-color: #f8f9fa;
border-radius: 0.25rem;
font-weight: 600;
color: #dc3545;
min-width: 2.5rem;
text-align: center;
}
</style>
<script>
$(document).ready(function() {
// SMS 데이터 로드 함수
function loadSmsData() {
$.ajax({
url: 'modern-dashboard-api.do',
type: 'GET',
data: {
type: 'sms-notification'
, serviceType: sessionStorage['serviceType']
},
dataType: 'json',
success: function(response) {
if (response.success) {
renderSmsTable(response.data);
} else {
console.error('SMS 데이터 로드 실패:', response.message);
showEmptySmsTable();
}
},
error: function(xhr, status, error) {
console.error('SMS 데이터 로드 중 오류 발생:', error);
showEmptySmsTable();
}
});
}
// SMS 테이블 렌더링 함수
function renderSmsTable(data) {
var $tableBody = $('#smsTableBody');
$tableBody.empty();
if (data && data.length > 0) {
// 최대 10개 항목만 표시
var displayData = data.slice(0, 10);
$.each(displayData, function(index, item) {
var errorTypeIcon = getErrorTypeIcon(item.errorType);
var timeStr = formatTime(item.dateTime);
// 오류 대상 정보와 스타일 조건부 적용
var errorTarget = '';
if (item.errorType === 'ADAPTER') {
// 어댑터 오류일 경우 - 클릭 불가능
errorTarget = '<span class="adapter-id">' + (item.adapterId || '-') + '</span>';
} else {
// 인터페이스 오류일 경우 - 클릭 가능, data-service-type 속성 추가
errorTarget = '<span class="error-target interface-id" data-service-type="' +
(item.serviceType || '') + '" data-date-time="' +
(item.dateTime || '') + '">' + (item.interfaceId || '-') + '</span>';
}
var row = '<tr>' +
'<td>' + (item.instanceName || '-') + '</td>' +
'<td>' + errorTypeIcon + '</td>' +
'<td class="text-nowrap">' + timeStr + '</td>' +
'<td><span class="occurrence-count">' + (item.count || '0') + '</span></td>' +
'<td>' + errorTarget + '</td>' +
'<td class="text-danger">' + (item.errorContent || '-') + '</td>' +
'</tr>';
$tableBody.append(row);
});
// 인터페이스ID에만 클릭 이벤트 스타일 적용
$('.interface-id').css({
'cursor': 'pointer',
'color': '#007bff',
'text-decoration': 'underline'
});
// 인터페이스ID 클릭 이벤트 처리 - 인터페이스 타입에만 적용
$('.interface-id').on('click', function(e) {
e.preventDefault();
var row = $(this).closest('tr');
var interfaceId = $(this).text().trim();
var instanceName = row.find('td').eq(0).text().trim();
var errorTime = $(this).data('date-time');
var errorMessage = row.find('td').eq(5).text().trim();
var serviceType = $(this).data('service-type');
// 인터페이스 로그 상세 모달 열기
openInterfaceLogDetailModal(interfaceId, errorTime, instanceName, errorMessage, serviceType);
});
} else {
showEmptySmsTable();
}
}
// 에러 타입 아이콘 생성
function getErrorTypeIcon(errorType) {
if (errorType === 'ADAPTER') {
return '<div class="error-type-icon adapter" title="어댑터 오류"><i class="bi bi-plug"></i></div>';
} else {
return '<div class="error-type-icon interface" title="인터페이스 오류"><i class="bi bi-arrow-left-right"></i></div>';
}
}
// 오류 대상 정보 가져오기
function getErrorTarget(item) {
if (item.errorType === 'ADAPTER') {
return item.adapterId || '-';
} else {
return item.interfaceId || '-';
}
}
// 시간 포맷팅 (HH:mm:ss)
function formatTime(dateTimeStr) {
if (!dateTimeStr) return '-';
var parts = dateTimeStr.split(' ');
if (parts.length >= 2) {
var timePart = parts[1];
return timePart.substring(0, 8); // HH:mm:ss 부분만 추출
}
return '-';
}
// 빈 테이블 표시 함수
function showEmptySmsTable() {
var $tableBody = $('#smsTableBody');
$tableBody.empty();
$tableBody.append('<tr><td colspan="5" class="empty-message">장애 통보 내역이 없습니다</td></tr>');
}
// 초기 데이터 로드
loadSmsData();
// DashboardEventManager를 사용하여 새로고침 이벤트 구독
if (window.DashboardEventManager) {
window.DashboardEventManager.on('refresh', function() {
console.log('SMS 데이터 갱신 시작');
loadSmsData()
});
// 초기화 완료 이벤트 로깅
console.log('SMS 컴포넌트 초기화 완료');
}
});
</script>
@@ -0,0 +1,957 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*" %>
<!-- 소켓 상세 정보 모달 -->
<div id="socketDetailModal" class="modal modal-modern">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">회선 상세 정보</h2>
<span class="close-modal">&times;</span>
</div>
<div class="modal-body">
<!-- 탭 메뉴 -->
<div class="socket-detail-tabs">
<button class="tab-btn active" data-tab="institution-visual">
<i class="bi bi-pie-chart"></i> 기관 현황
</button>
<button class="tab-btn" data-tab="institution-status">
<i class="bi bi-building"></i> 기관 상태
</button>
<button class="tab-btn" data-tab="socket-status">
<i class="bi bi-plug"></i> 어댑터 상태
</button>
<button class="tab-btn" data-tab="socket-process">
<i class="bi bi-arrow-left-right"></i> 세션 상태
</button>
</div>
<!-- 탭 컨텐츠 -->
<div class="socket-detail-content">
<!-- 기관 연결 현황 시각화 탭 -->
<div id="institution-visual" class="tab-content active">
<div class="visual-stats-summary">
<div class="stats-card total-institutions">
<div class="stats-icon"><i class="bi bi-building"></i></div>
<div class="stats-details">
<div class="stats-number" id="total-institutions-count">0</div>
<div class="stats-label">총 기관</div>
</div>
</div>
<div class="stats-card normal-institutions">
<div class="stats-icon"><i class="bi bi-check-circle"></i></div>
<div class="stats-details">
<div class="stats-number" id="normal-institutions-count">0</div>
<div class="stats-label">정상</div>
</div>
</div>
<div class="stats-card warning-institutions">
<div class="stats-icon"><i class="bi bi-exclamation-triangle"></i></div>
<div class="stats-details">
<div class="stats-number" id="warning-institutions-count">0</div>
<div class="stats-label">경고</div>
</div>
</div>
<div class="stats-card error-institutions">
<div class="stats-icon"><i class="bi bi-x-circle"></i></div>
<div class="stats-details">
<div class="stats-number" id="error-institutions-count">0</div>
<div class="stats-label">오류</div>
</div>
</div>
<div class="stats-card inactive-institutions">
<div class="stats-icon"><i class="bi bi-dash-circle"></i></div>
<div class="stats-details">
<div class="stats-number" id="inactive-institutions-count">0</div>
<div class="stats-label">미사용</div>
</div>
</div>
</div>
<div class="connection-visual-container">
<div id="institution-grid" class="institution-grid"></div>
</div>
</div>
<!-- 기관 상태 탭 -->
<div id="institution-status" class="tab-content">
<div class="institution-status-container">
<table class="institution-status-table modal-table">
<thead>
<tr>
<th>기관명</th>
<th>담당자명</th>
<th>연락처</th>
<th>정상어댑터</th>
<th>오류어댑터</th>
<th>회선상태</th>
</tr>
</thead>
<tbody>
<!-- 데이터가 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
</div>
<!-- 회선 상태 탭 -->
<div id="socket-status" class="tab-content">
<div class="socket-status-container">
<table class="socket-status-table modal-table">
<thead>
<tr>
<th>어댑터 그룹</th>
<th>인스턴스명</th>
<th>어댑터명</th>
<th>어댑터 설명</th>
<th>어댑터 유형</th>
<th>상태</th>
<th>기동 상태</th>
</tr>
</thead>
<tbody>
<!-- 데이터가 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
</div>
<!-- 회선 연결 상태 탭 -->
<div id="socket-process" class="tab-content">
<div class="socket-process-container">
<table class="socket-process-table modal-table">
<thead>
<tr>
<th>어댑터 그룹</th>
<th>인스턴스명</th>
<th>어댑터명</th>
<th>어댑터 설명</th>
<th>최대 연결</th>
<th>현재 연결</th>
<th>송신 수</th>
<th>수신 수</th>
<th>오류 수</th>
<th>상태</th>
</tr>
</thead>
<tbody>
<!-- 데이터가 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary refresh-socket-data">
<i class="bi bi-arrow-clockwise" style="color: #ffffff;"></i> 새로고침
</button>
<button class="btn btn-secondary close-modal-btn">닫기</button>
</div>
</div>
</div>
<style>
/* 기존 스타일 강화 */
.modal-modern {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.modal-modern .modal-content {
position: relative;
background-color: #fff;
margin: 2% auto;
padding: 0;
width: 95%;
max-width: 1400px;
border: none;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
animation: modalSlideUp 0.3s;
}
@keyframes modalSlideUp {
from {opacity: 0; transform: translateY(30px);}
to {opacity: 1; transform: translateY(0);}
}
@keyframes modalFadeIn {
from {opacity: 0;}
to {opacity: 1;}
}
.modal-modern .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e0e0e0;
background-color: #f8f9fa;
border-radius: 16px 16px 0 0;
}
.modal-modern .modal-title {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #333;
}
.modal-modern .close-modal {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.05);
}
.modal-modern .close-modal:hover {
color: #333;
background-color: rgba(0, 0, 0, 0.1);
}
.modal-modern .modal-body {
padding: 1.5rem 2rem;
max-height: 75vh;
overflow-y: auto;
background-color: #f8f9fa;
}
.modal-modern .modal-footer {
padding: 1.2rem 2rem;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: flex-end;
gap: 1rem;
border-radius: 0 0 16px 16px;
background-color: #fff;
}
/* 탭 스타일 향상 */
.socket-detail-tabs {
display: flex;
border-bottom: none;
margin-bottom: 1.5rem;
overflow-x: auto;
background-color: #fff;
border-radius: 8px;
padding: 0.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.tab-btn {
padding: 0.9rem 1.5rem;
background: none;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-size: 1rem;
color: #666;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tab-btn:hover {
color: #333;
background-color: rgba(0, 0, 0, 0.05);
}
.tab-btn.active {
color: white;
background-color: #007bff;
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.2);
}
.tab-content {
display: none;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from {opacity: 0;}
to {opacity: 1;}
}
.tab-content.active {
display: block;
}
/* 테이블 스타일 향상 */
.modal-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background-color: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.modal-table th {
background-color: #007bff;
color: white;
padding: 1.2rem;
text-align: left;
font-weight: 800;
top: 0;
z-index: 10;
}
.modal-table td {
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
}
.modal-table tbody tr {
transition: background-color 0.2s;
}
.modal-table tbody tr:hover {
background-color: rgba(0, 0, 0, 0.03);
}
.modal-table tbody tr:last-child td {
border-bottom: none;
}
/* 버튼 스타일 향상 */
.btn {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-size: inherit;
transition: all 0.25s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0069d9;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
.btn-secondary {
background-color: #f1f5f9;
color: #333;
}
.btn-secondary:hover {
background-color: #e2e8f0;
}
/* 로딩 애니메이션 향상 */
.loading-indicator {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid #007bff;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 통계 카드 스타일 향상 */
.visual-stats-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stats-card {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
padding: 1.2rem;
display: flex;
align-items: center;
gap: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.stats-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.stats-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
}
.stats-details {
flex: 1;
}
.stats-number {
font-size: 1.8rem;
font-weight: 700;
line-height: 1.1;
}
.stats-label {
font-size: 0.9rem;
color: #666;
margin-top: 0.2rem;
}
/* 카드 색상 */
.total-institutions .stats-icon {
background-color: rgba(0, 123, 255, 0.3);
color: #007bff;
}
.normal-institutions .stats-icon {
background-color: rgba(40, 167, 69, 0.3);
color: #28a745;
}
.warning-institutions .stats-icon {
background-color: rgba(255, 193, 7, 0.3);
color: #ffc107;
}
.error-institutions .stats-icon {
background-color: rgba(220, 53, 69, 0.3);
color: #dc3545;
}
.inactive-institutions .stats-icon {
background-color: rgba(108, 117, 125, 0.3);
color: #6c757d;
}
/* 기관 카드 그리드 향상 */
.connection-visual-container {
background-color: #fff;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.institution-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.institution-card {
background-color: #f8f9fa;
border-radius: 12px;
padding: 1.2rem;
transition: all 0.3s ease;
border-left: 5px solid #ccc;
cursor: pointer;
position: relative;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.institution-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.institution-status-indicator {
position: absolute;
top: 15px;
right: 15px;
font-size: 1.2rem;
}
.institution-name {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
color: #333;
padding-right: 2rem;
}
.institution-info {
display: grid;
grid-template-columns: 80px 1fr;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.institution-info-label {
color: #666;
}
.institution-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-top: 1rem;
border-top: 1px solid #e0e0e0;
padding-top: 0.8rem;
}
.institution-stat {
text-align: center;
padding: 0.5rem;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.03);
}
/* 상태 배지 스타일 */
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.4rem 0.8rem;
border-radius: 50px;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
}
.status-badge-normal {
background-color: rgba(40, 167, 69, 0.15);
color: #28a745;
}
.status-badge-warning {
background-color: rgba(255, 193, 7, 0.15);
color: #ffc107;
}
.status-badge-error {
background-color: rgba(220, 53, 69, 0.15);
color: #dc3545;
}
.status-badge-inactive {
background-color: rgba(108, 117, 125, 0.15);
color: #6c757d;
}
/* 반응형 디자인 */
@media (max-width: 1024px) {
.visual-stats-summary {
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
}
.modal-modern .modal-content {
width: 98%;
margin: 1% auto;
}
.modal-modern .modal-body {
padding: 1rem 1.5rem;
}
}
@media (max-width: 768px) {
.socket-detail-tabs {
padding: 0.3rem;
flex-wrap: wrap;
}
.tab-btn {
padding: 0.8rem 1rem;
font-size: 0.9rem;
flex-grow: 1;
}
.visual-stats-summary {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.8rem;
}
.stats-icon {
width: 40px;
height: 40px;
font-size: 1.2rem;
}
.stats-number {
font-size: 1.5rem;
}
.institution-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.modal-modern .modal-header,
.modal-modern .modal-footer {
padding: 1rem 1.5rem;
}
}
@media (max-width: 576px) {
.visual-stats-summary {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.institution-grid {
grid-template-columns: 1fr;
}
.modal-modern .modal-body {
padding: 1rem;
}
}
.institution-card.status-normal {
border-left: 5px solid #98e2a6; /* 파스텔 그린 */
}
.institution-card.status-warning {
border-left: 5px solid #ffe5a3; /* 파스텔 옐로우 */
}
.institution-card.status-error {
border-left: 5px solid #ffb3b3; /* 파스텔 레드 */
}
.institution-card.status-inactive {
border-left: 5px solid #d9d9d9; /* 파스텔 그레이 */
}
</style>
<script>
// 소켓 상세 정보 모달 관련 스크립트
(function() {
// 모달 요소
const modal = document.getElementById('socketDetailModal');
const closeButtons = modal.querySelectorAll('.close-modal, .close-modal-btn');
// 탭 관련 요소
const tabButtons = modal.querySelectorAll('.tab-btn');
const tabContents = modal.querySelectorAll('.tab-content');
// 테이블 요소
const institutionTable = modal.querySelector('.institution-status-table tbody');
const statusTable = modal.querySelector('.socket-status-table tbody');
const processTable = modal.querySelector('.socket-process-table tbody');
// 시각화 요소
const institutionGrid = document.getElementById('institution-grid');
const totalInstitutionsCount = document.getElementById('total-institutions-count');
const normalInstitutionsCount = document.getElementById('normal-institutions-count');
const warningInstitutionsCount = document.getElementById('warning-institutions-count');
const errorInstitutionsCount = document.getElementById('error-institutions-count');
const inactiveInstitutionsCount = document.getElementById('inactive-institutions-count');
// 현재 선택된 서버와 어댑터 그룹
let currentServerName = '';
let currentAdapterGroup = '';
// 모달 닫기 이벤트
closeButtons.forEach(button => {
button.addEventListener('click', () => {
modal.style.display = 'none';
});
});
// 모달 외부 클릭 시 닫기
window.addEventListener('click', (event) => {
if (event.target === modal) {
modal.style.display = 'none';
}
});
// 탭 전환 이벤트
tabButtons.forEach(button => {
button.addEventListener('click', () => {
// 활성 탭 변경
tabButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// 탭 컨텐츠 변경
const tabId = button.getAttribute('data-tab');
tabContents.forEach(content => {
content.classList.remove('active');
if (content.id === tabId) {
content.classList.add('active');
}
});
});
});
// 새로고침 버튼 이벤트
const refreshButton = modal.querySelector('.refresh-socket-data');
refreshButton.addEventListener('click', () => {
loadSocketData(currentServerName, currentAdapterGroup);
});
// 소켓 데이터 로드 함수
function loadSocketData(serverName, adapterGroup) {
// 현재 선택된 서버와 어댑터 그룹 저장
currentServerName = serverName || '';
currentAdapterGroup = adapterGroup || '';
// 로딩 표시
institutionTable.innerHTML = '<tr><td colspan="6"><div class="loading-indicator"><div class="loading-spinner"></div></div></td></tr>';
statusTable.innerHTML = '<tr><td colspan="7"><div class="loading-indicator"><div class="loading-spinner"></div></div></td></tr>';
processTable.innerHTML = '<tr><td colspan="10"><div class="loading-indicator"><div class="loading-spinner"></div></div></td></tr>';
institutionGrid.innerHTML = '<div class="loading-indicator"><div class="loading-spinner"></div></div>';
// API 호출
ModernDashboardApi.getSocketCombined(serverName, adapterGroup, function(data) {
if (data) {
// 기관 상태 데이터 표시
updateInstitutionStatusTable(data.institutionStatus);
// 기관 시각화 업데이트
updateInstitutionVisualization(data.institutionStatus);
// 회선 상태 데이터 표시
updateSocketStatusTable(data.socketStatus);
// 회선 연결 상태 데이터 표시
updateSocketProcessTable(data.socketProcess);
}
}, function(xhr, status, error) {
// 에러 처리
const errorMsg = '<div style="padding: 2rem; text-align: center; color: #dc3545;"><i class="fas fa-exclamation-circle" style="font-size: 3rem;"></i><p style="margin-top: 1rem;">데이터를 불러오는 중 오류가 발생했습니다.</p></div>';
institutionTable.innerHTML = '<tr><td colspan="6">' + errorMsg + '</td></tr>';
statusTable.innerHTML = '<tr><td colspan="7">' + errorMsg + '</td></tr>';
processTable.innerHTML = '<tr><td colspan="10">' + errorMsg + '</td></tr>';
institutionGrid.innerHTML = errorMsg;
console.error('소켓 데이터 로드 실패:', error);
});
}
// 기관 상태 테이블 업데이트
function updateInstitutionStatusTable(data) {
if (!data || data.length === 0) {
institutionTable.innerHTML = '<tr><td colspan="6"><div style="padding: 2rem; text-align: center; color: #6c757d;"><i class="fas fa-info-circle" style="font-size: 2rem;"></i><p style="margin-top: 1rem;">표시할 데이터가 없습니다.</p></div></td></tr>';
return;
}
let html = '';
data.forEach(institution => {
let statusClass = '';
let statusText = '';
// 상태에 따른 텍스트 및 클래스 설정
if (institution.lineStatus === 'UNDEFINED') {
statusText = '미사용';
statusClass = 'status-badge status-badge-inactive';
} else if (institution.lineStatus === 'WARN') {
statusText = '경고';
statusClass = 'status-badge status-badge-warning';
} else if (institution.lineStatus === 'ERROR') {
statusText = '오류';
statusClass = 'status-badge status-badge-error';
} else {
statusText = '정상';
statusClass = 'status-badge status-badge-normal';
}
html += '<tr>' +
'<td>' + (institution.institutionName || '-') + '</td>' +
'<td>' + (institution.contactPerson || '-') + '</td>' +
'<td>' + (institution.contactNumber || '-') + '</td>' +
'<td>' + (institution.normalAdapterCount || '0') + '</td>' +
'<td>' + (institution.errorAdapterCount || '0') + '</td>' +
'<td><span class="' + statusClass + '">' + statusText + '</span></td>' +
'</tr>';
});
institutionTable.innerHTML = html;
}
// 기관 시각화 업데이트
function updateInstitutionVisualization(data) {
if (!data || data.length === 0) {
institutionGrid.innerHTML = '<div style="padding: 2rem; text-align: center; color: #6c757d;"><i class="fas fa-info-circle" style="font-size: 2rem;"></i><p style="margin-top: 1rem;">표시할 데이터가 없습니다.</p></div>';
updateStatsCounts(0, 0, 0, 0, 0);
return;
}
let html = '';
let normalCount = 0;
let warningCount = 0;
let errorCount = 0;
let inactiveCount = 0;
data.forEach(institution => {
let statusClass = 'status-normal';
let statusIcon = '<i class="bi bi-check-circle" style="color: #28a745;"></i>';
let statusName = '정상';
// 상태에 따른 시각화 설정
if (institution.lineStatus === 'UNDEFINED') {
statusClass = 'status-inactive';
statusIcon = '<i class="bi bi-dash-circle" style="color: #6c757d;"></i>';
statusName = '미사용';
inactiveCount++;
} else if (institution.lineStatus === 'WARN') {
statusClass = 'status-warning';
statusIcon = '<i class="bi bi-exclamation-triangle" style="color: #ffc107;"></i>';
statusName = '경고';
warningCount++;
} else if (institution.lineStatus === 'ERROR') {
statusClass = 'status-error';
statusIcon = '<i class="bi bi-x-circle" style="color: #dc3545;"></i>';
statusName = '오류';
errorCount++;
} else {
normalCount++;
}
html += '<div class="institution-card ' + statusClass + '" data-code="' + institution.institutionCode + '">' +
'<div class="institution-status-indicator">' + statusIcon + '</div>' +
'<div class="institution-name">' + (institution.institutionName || '미지정 기관') + '</div>' +
'<div class="institution-info">' +
'<span class="institution-info-label">담당자:</span>' +
'<span>' + (institution.contactPerson || '-') + '</span>' +
'</div>' +
'<div class="institution-info">' +
'<span class="institution-info-label">연락처:</span>' +
'<span>' + (institution.contactNumber || '-') + '</span>' +
'</div>' +
'<div class="institution-info">' +
'<span class="institution-info-label">상태:</span>' +
'<span>' + statusName + '</span>' +
'</div>' +
'<div class="institution-stats">' +
'<div class="institution-stat">' +
'<div class="stat-number normal">' + (institution.normalAdapterCount || '0') + '</div>' +
'<div class="stat-label">정상 어댑터</div>' +
'</div>' +
'<div class="institution-stat">' +
'<div class="stat-number error">' + (institution.errorAdapterCount || '0') + '</div>' +
'<div class="stat-label">오류 어댑터</div>' +
'</div>' +
'</div>' +
'</div>';
});
institutionGrid.innerHTML = html;
updateStatsCounts(data.length, normalCount, warningCount, errorCount, inactiveCount);
// 기관 카드 클릭 이벤트 - 해당 기관 정보로 필터링
const institutionCards = institutionGrid.querySelectorAll('.institution-card');
institutionCards.forEach(card => {
card.addEventListener('click', () => {
const institutionCode = card.getAttribute('data-code');
// 여기에 기관 코드로 필터링하는 로직 추가
console.log('기관 선택:', institutionCode);
// 기관에 해당하는 어댑터로 필터링하는 탭으로 이동
const socketStatusTab = document.querySelector('.tab-btn[data-tab="socket-status"]');
socketStatusTab.click();
});
});
}
// 통계 수치 업데이트
function updateStatsCounts(total, normal, warning, error, inactive) {
totalInstitutionsCount.textContent = total;
normalInstitutionsCount.textContent = normal;
warningInstitutionsCount.textContent = warning;
errorInstitutionsCount.textContent = error;
inactiveInstitutionsCount.textContent = inactive;
}
// 회선 상태 테이블 업데이트
function updateSocketStatusTable(data) {
if (!data || !data.rows || data.rows.length === 0) {
statusTable.innerHTML = '<tr><td colspan="7"><div style="padding: 2rem; text-align: center; color: #6c757d;"><i class="fas fa-info-circle" style="font-size: 2rem;"></i><p style="margin-top: 1rem;">표시할 데이터가 없습니다.</p></div></td></tr>';
return;
}
let html = '';
data.rows.forEach(row => {
let statusClass = row.status === 'true' ? 'status-badge status-badge-normal' : 'status-badge status-badge-error';
const statusName = row.status === 'true' ? '정상' : '오류';
html += '<tr>' +
'<td>' + (row.adptrBzwkGroupName || '-') + '</td>' +
'<td>' + (row.eaiSevrInstncName || '-') + '</td>' +
'<td>' + (row.adptrNickName || '-') + '</td>' +
'<td>' + (row.adptrBzwkDesc || '-') + '</td>' +
'<td>' + (row.adptrType || '-') + '</td>' +
'<td><span class="' + statusClass + '">' + statusName + '</span></td>' +
'<td>' + (row.starting || '-') + '</td>' +
'</tr>';
});
statusTable.innerHTML = html;
}
// 회선 연결 상태 테이블 업데이트
function updateSocketProcessTable(data) {
if (!data || !data.rows || data.rows.length === 0) {
processTable.innerHTML = '<tr><td colspan="10"><div style="padding: 2rem; text-align: center; color: #6c757d;"><i class="fas fa-info-circle" style="font-size: 2rem;"></i><p style="margin-top: 1rem;">표시할 데이터가 없습니다.</p></div></td></tr>';
return;
}
let html = '';
data.rows.forEach(row => {
let statusClass = row.status === 'true' ? 'status-badge status-badge-normal' : 'status-badge status-badge-error';
const statusName = row.status === 'true' ? '정상' : '오류';
html += '<tr>' +
'<td>' + (row.adptrBzwkGroupName || '-') + '</td>' +
'<td>' + (row.eaiSevrInstncName || '-') + '</td>' +
'<td>' + (row.adptrBzwkName || '-') + '</td>' +
'<td>' + (row.adptrBzwkDesc || '-') + '</td>' +
'<td>' + (row.maxConnection || '0') + '</td>' +
'<td>' + (row.currentConnection || '0') + '</td>' +
'<td>' + (row.sendCount || '0') + '</td>' +
'<td>' + (row.recvCount || '0') + '</td>' +
'<td>' + (row.errorCount || '0') + '</td>' +
'<td><span class="' + statusClass + '">' + statusName + '</span></td>' +
'</tr>';
});
processTable.innerHTML = html;
}
// 모달 열기 함수 (전역 스코프에 노출)
window.openSocketDetailModal = function(serverName, adapterGroup) {
// 모달 표시
modal.style.display = 'block';
// 데이터 로드
loadSocketData(serverName, adapterGroup);
};
})();
</script>
@@ -0,0 +1,699 @@
<%@ 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>
@@ -0,0 +1,834 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*" %>
<%@ page import="com.eactive.eai.rms.common.datasource.*" %>
<%@ page import="java.util.stream.Collectors" %>
<div class="dashboard-transactions">
<div class="dashboard-header">
<div class="header-title-container">
<h2 class="section-title">거래처리 집계(일누적)</h2>
</div>
</div>
<div class="card shadow mb-0" style="overflow-y:auto;">
<div class="transaction-filters">
<div class="filter-group">
<button class="tran-filter-btn active" data-type="all">전체</button>
<%
// serverTypeMap 변수를 가져오기
Map<String, List<DataSourceType>> serverTypeMap =
(Map<String, List<DataSourceType>>)request.getAttribute("serverTypeMap");
if (serverTypeMap == null) {
// application 스코프에서 가져오기 시도
serverTypeMap = (Map<String, List<DataSourceType>>)application.getAttribute("serverTypeMap");
}
Map<String, Integer> priorityMap = new HashMap<>();
priorityMap.put("MCI", 0);
priorityMap.put("EAI", 1);
priorityMap.put("FEP", 2);
priorityMap.put("CGW", 3);
priorityMap.put("FGW", 4);
priorityMap.put("API", 5);
priorityMap.put("APIGW", 6);
priorityMap.put("ONL", 7);
Map<String,List<DataSourceType>> sortedMap = serverTypeMap.entrySet().stream()
.sorted(Comparator.comparingInt(e -> priorityMap.getOrDefault(e.getKey(), Integer.MAX_VALUE)))
.collect(Collectors.toMap(
Map.Entry::getKey
, Map.Entry::getValue
,(e1, e2) -> e1
, LinkedHashMap::new
));
if (sortedMap != null) {
// 서버 타입별 필터 버튼 생성
for (Map.Entry<String, List<DataSourceType>> entry : sortedMap.entrySet()) {
String serverType = entry.getKey();
List<DataSourceType> servers = entry.getValue();
// BAT 서버는 제외
if (DataSourceTypeManager.BAT.equals(serverType) || DataSourceTypeManager.BAP.equals(serverType)) {
continue;
}
String serverTypeText = servers.size() > 0 ? servers.get(0).getText() : serverType;
if( !serverTypeText.endsWith("GW") ) {
%>
<button class="tran-filter-btn" data-type="<%= serverType.toLowerCase() %>"><%= serverTypeText %></button>
<% }
}
} %>
</div>
</div>
<div class="transaction-table-container">
<table class="transaction-table">
<thead>
<tr>
<th style="width: 1%"></th>
<th>업무그룹</th>
<th>정상처리</th>
<th>업무에러</th>
<th>타임아웃</th>
<th>통신에러</th>
</tr>
</thead>
<tbody id="transaction-table-body">
<!-- 거래 처리 데이터가 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- 거래 처리 행 템플릿 -->
<template id="transaction-row-template">
<tr>
<td>
<span class="instance-status status-normal"></span>
</td>
<td class="group-name">업무그룹</td>
<td>
<div class="metric">
<div class="metric-value">
<span class="clickable transaction-count">0</span>
<span class="metric-change transaction-change" style="visibility: hidden;"><i class="bi bi-arrow-up-short"></i>0</span>
</div>
</div>
</td>
<td>
<div class="metric">
<div class="metric-value">
<span class="clickable error-count">0</span>
<span class="metric-change error-change" style="visibility: hidden;"><i class="bi bi-arrow-up-short"></i>0</span>
</div>
</div>
</td>
<td>
<div class="metric">
<div class="metric-value">
<span class="clickable timeout-count">0</span>
<span class="metric-change timeout-change" style="visibility: hidden;"><i class="bi bi-arrow-up-short"></i>0</span>
</div>
</div>
</td>
<td>
<div class="metric">
<div class="metric-value">
<span class="clickable syserror-count">0</span>
<span class="metric-change syserror-change" style="visibility: hidden;"><i class="bi bi-arrow-up-short"></i>0</span>
</div>
</div>
</td>
</tr>
</template>
<script>
// 데이터 로딩 상태 플래그
var isTransactionDataLoading = false;
// 이전 데이터 저장 객체
var previousTransactionData = {};
// 숫자 형식화 함수 (천 단위 콤마 추가)
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// 기존 행 업데이트 함수 수정
function updateExistingRow(row, transactionData) {
// 상태 업데이트
var statusElement = row.querySelector('.instance-status');
statusElement.className = 'instance-status';
// 정상 처리와 오류 비율에 따라 색상 계산
var totalCount = (transactionData.count || 0);
var errorCount = (transactionData.error || 0) + (transactionData.timeout || 0) + (transactionData.syserror || 0);
var totalTransactions = totalCount + errorCount;
// 색상 설정
if (totalTransactions > 0) {
var successRatio = totalCount / totalTransactions;
var statusColor = calculateStatusColor(successRatio);
statusElement.style.backgroundColor = statusColor;
} else {
// 거래가 없는 경우 회색으로 표시
statusElement.style.backgroundColor = '#9e9e9e';
}
// 이전 데이터 가져오기
var prevData = previousTransactionData[transactionData.name] || {
count: 0,
error: 0,
timeout: 0,
syserror: 0
};
// 정상처리 건수 업데이트
var transactionCount = row.querySelector('.transaction-count');
transactionCount.textContent = formatNumber(transactionData.count || 0);
// 증가량 표시
var transactionChange = row.querySelector('.transaction-change');
var countDiff = (transactionData.count || 0) - prevData.count;
if (countDiff > 0) {
transactionChange.innerHTML = '<i class="bi bi-arrow-up-short"></i>' + formatNumber(countDiff);
transactionChange.style.visibility = 'visible';
// 2초 후 사라지게 설정
setTimeout(function() {
transactionChange.style.visibility = 'hidden';
}, 2000);
} else {
transactionChange.style.visibility = 'hidden';
}
// 업무에러 업데이트
var errorCount = row.querySelector('.error-count');
errorCount.textContent = formatNumber(transactionData.error || 0);
// 증가량 표시
var errorChange = row.querySelector('.error-change');
var errorDiff = (transactionData.error || 0) - prevData.error;
if (errorDiff > 0) {
errorChange.innerHTML = '<i class="bi bi-arrow-up-short"></i>' + formatNumber(errorDiff);
errorChange.style.visibility = 'visible';
// 2초 후 사라지게 설정
setTimeout(function() {
errorChange.style.visibility = 'hidden';
}, 2000);
} else {
errorChange.style.visibility = 'hidden';
}
// 타임아웃 업데이트
var timeoutCount = row.querySelector('.timeout-count');
timeoutCount.textContent = formatNumber(transactionData.timeout || 0);
// 증가량 표시
var timeoutChange = row.querySelector('.timeout-change');
var timeoutDiff = (transactionData.timeout || 0) - prevData.timeout;
if (timeoutDiff > 0) {
timeoutChange.innerHTML = '<i class="bi bi-arrow-up-short"></i>' + formatNumber(timeoutDiff);
timeoutChange.style.visibility = 'visible';
// 2초 후 사라지게 설정
setTimeout(function() {
timeoutChange.style.visibility = 'hidden';
}, 2000);
} else {
timeoutChange.style.visibility = 'hidden';
}
// 통신에러 업데이트
var syserrorCount = row.querySelector('.syserror-count');
syserrorCount.textContent = formatNumber(transactionData.syserror || 0);
// 증가량 표시
var syserrorChange = row.querySelector('.syserror-change');
var syserrorDiff = (transactionData.syserror || 0) - prevData.syserror;
if (syserrorDiff > 0) {
syserrorChange.innerHTML = '<i class="bi bi-arrow-up-short"></i>' + formatNumber(syserrorDiff);
syserrorChange.style.visibility = 'visible';
// 2초 후 사라지게 설정
setTimeout(function() {
syserrorChange.style.visibility = 'hidden';
}, 2000);
} else {
syserrorChange.style.visibility = 'hidden';
}
// 현재 데이터를 이전 데이터로 저장
previousTransactionData[transactionData.name] = {
count: transactionData.count || 0,
error: transactionData.error || 0,
timeout: transactionData.timeout || 0,
syserror: transactionData.syserror || 0
};
}
// 거래 행 생성 함수 수정
function createTransactionRow(transactionData) {
var template = document.getElementById('transaction-row-template');
var clone = document.importNode(template.content, true);
// 업무그룹 이름 설정
clone.querySelector('.group-name').textContent = transactionData.name;
// 정상 처리와 오류 비율에 따라 색상 계산
var totalCount = (transactionData.count || 0);
var errorCount = (transactionData.error || 0) + (transactionData.timeout || 0) + (transactionData.syserror || 0);
var totalTransactions = totalCount + errorCount;
// 상태 설정
var statusElement = clone.querySelector('.instance-status');
// 색상 설정
if (totalTransactions > 0) {
var successRatio = totalCount / totalTransactions;
var statusColor = calculateStatusColor(successRatio);
statusElement.style.backgroundColor = statusColor;
} else {
// 거래가 없는 경우 회색으로 표시
statusElement.style.backgroundColor = '#9e9e9e';
}
// 정상처리 건수 설정
clone.querySelector('.transaction-count').textContent = formatNumber(transactionData.count || 0);
// 업무에러 설정
clone.querySelector('.error-count').textContent = formatNumber(transactionData.error || 0);
// 타임아웃 설정
clone.querySelector('.timeout-count').textContent = formatNumber(transactionData.timeout || 0);
// 통신에러 설정
clone.querySelector('.syserror-count').textContent = formatNumber(transactionData.syserror || 0);
// 현재 데이터를 이전 데이터로 저장
previousTransactionData[transactionData.name] = {
count: transactionData.count || 0,
error: transactionData.error || 0,
timeout: transactionData.timeout || 0,
syserror: transactionData.syserror || 0
};
return clone;
}
// 거래 건수 테이블 업데이트 함수
function updateTransactionTable(data) {
if (!data || !data.transactions) return;
var tableBody = document.getElementById('transaction-table-body');
// 합계 값 초기화
var totalCount = 0;
var totalError = 0;
var totalTimeout = 0;
var totalSyserror = 0;
// 현재 테이블에 있는 행들 저장
var currentRows = {}; // 현재 행들을 저장할 객체
// 현재 테이블의 상태를 저장
Array.from(tableBody.querySelectorAll('tr:not(.total-row)')).forEach(function(row) {
var transactionType = row.querySelector('.group-name').textContent;
currentRows[transactionType] = row;
});
// 합계 행 제거 (나중에 다시 추가)
var totalRow = tableBody.querySelector('.total-row');
if (totalRow) {
tableBody.removeChild(totalRow);
}
// 거래 데이터 행 업데이트 또는 추가
data.transactions.forEach(function(transaction) {
// TOTAL 항목은 표에 표시하지 않고 마지막에 별도로 처리
// APIGW는 표시하고, CGW, FGW는 제외
if (transaction.type === 'TOTAL' || (transaction.type.endsWith("GW") && transaction.type !== 'APIGW') ) {
return;
}
// 각 거래별 데이터 설정
var transactionData = {
name: transaction.type,
status: 'normal',
count: transaction.success || 0,
error: transaction.fail || 0,
timeout: transaction.timeout || 0,
syserror: transaction.syserror || 0
};
// 상태 설정: 에러가 많으면 경고 또는 오류 상태로 표시
if ((transaction.fail + transaction.timeout + transaction.syserror) > 50) {
transactionData.status = 'error';
} else if ((transaction.fail + transaction.timeout + transaction.syserror) > 10) {
transactionData.status = 'warning';
}
// 이미 존재하는 행인지 확인하여 업데이트 또는 새로 생성
if (currentRows[transaction.type]) {
updateExistingRow(currentRows[transaction.type], transactionData);
} else {
// 새 행 생성 및 추가
var newRow = createTransactionRow(transactionData);
tableBody.appendChild(newRow);
}
// 합계 계산
totalCount += transactionData.count;
totalError += transactionData.error;
totalTimeout += transactionData.timeout;
totalSyserror += transactionData.syserror;
});
// 사라진 행 제거 (현재 데이터에 없는 행)
var remainingTypes = data.transactions
.filter(t => t.type !== 'TOTAL')
.map(t => t.type);
for (var type in currentRows) {
if (!remainingTypes.includes(type)) {
tableBody.removeChild(currentRows[type]);
}
}
// 합계 행 생성 및 추가
totalRow = document.createElement('tr');
totalRow.className = 'total-row';
// 상태 아이콘 셀
var statusCell = document.createElement('td');
totalRow.appendChild(statusCell);
// 업무그룹 셀
var nameCell = document.createElement('td');
nameCell.className = 'group-name';
nameCell.textContent = '합계';
totalRow.appendChild(nameCell);
// 정상처리 셀
var countCell = document.createElement('td');
var countMetric = document.createElement('div');
countMetric.className = 'metric';
var countValue = document.createElement('div');
countValue.className = 'metric-value';
var countSpan = document.createElement('span');
countSpan.className = 'transaction-count';
countSpan.textContent = formatNumber(totalCount);
// 가짜 metric-change span 추가 (정상처리)
var countChangeSpan = document.createElement('span');
countChangeSpan.className = 'metric-change transaction-change';
countChangeSpan.style.visibility = 'hidden';
countChangeSpan.innerHTML = '<i class="bi bi-arrow-up-short"></i>0';
countValue.appendChild(countSpan);
countValue.appendChild(countChangeSpan); // 가짜 span 추가
countMetric.appendChild(countValue);
countCell.appendChild(countMetric);
totalRow.appendChild(countCell);
// 업무에러 셀
var errorCell = document.createElement('td');
var errorMetric = document.createElement('div');
errorMetric.className = 'metric';
var errorValue = document.createElement('div');
errorValue.className = 'metric-value';
var errorSpan = document.createElement('span');
errorSpan.className = 'error-count';
errorSpan.textContent = formatNumber(totalError);
// 가짜 metric-change span 추가 (업무에러)
var errorChangeSpan = document.createElement('span');
errorChangeSpan.className = 'metric-change error-change';
errorChangeSpan.style.visibility = 'hidden';
errorChangeSpan.innerHTML = '<i class="bi bi-arrow-up-short"></i>0';
errorValue.appendChild(errorSpan);
errorValue.appendChild(errorChangeSpan); // 가짜 span 추가
errorMetric.appendChild(errorValue);
errorCell.appendChild(errorMetric);
totalRow.appendChild(errorCell);
// 타임아웃 셀
var timeoutCell = document.createElement('td');
var timeoutMetric = document.createElement('div');
timeoutMetric.className = 'metric';
var timeoutValue = document.createElement('div');
timeoutValue.className = 'metric-value';
var timeoutSpan = document.createElement('span');
timeoutSpan.className = 'timeout-count';
timeoutSpan.textContent = formatNumber(totalTimeout);
// 가짜 metric-change span 추가 (타임아웃)
var timeoutChangeSpan = document.createElement('span');
timeoutChangeSpan.className = 'metric-change timeout-change';
timeoutChangeSpan.style.visibility = 'hidden';
timeoutChangeSpan.innerHTML = '<i class="bi bi-arrow-up-short"></i>0';
timeoutValue.appendChild(timeoutSpan);
timeoutValue.appendChild(timeoutChangeSpan); // 가짜 span 추가
timeoutMetric.appendChild(timeoutValue);
timeoutCell.appendChild(timeoutMetric);
totalRow.appendChild(timeoutCell);
// 통신에러 셀
var syserrorCell = document.createElement('td');
var syserrorMetric = document.createElement('div');
syserrorMetric.className = 'metric';
var syserrorValue = document.createElement('div');
syserrorValue.className = 'metric-value';
var syserrorSpan = document.createElement('span');
syserrorSpan.className = 'syserror-count';
syserrorSpan.textContent = formatNumber(totalSyserror);
// 가짜 metric-change span 추가 (통신에러)
var syserrorChangeSpan = document.createElement('span');
syserrorChangeSpan.className = 'metric-change syserror-change';
syserrorChangeSpan.style.visibility = 'hidden';
syserrorChangeSpan.innerHTML = '<i class="bi bi-arrow-up-short"></i>0';
syserrorValue.appendChild(syserrorSpan);
syserrorValue.appendChild(syserrorChangeSpan); // 가짜 span 추가
syserrorMetric.appendChild(syserrorValue);
syserrorCell.appendChild(syserrorMetric);
totalRow.appendChild(syserrorCell);
// 테이블에 합계 행 추가
tableBody.appendChild(totalRow);
}
// 트랜잭션 데이터 로드 함수
function loadTransactionData(type) {
// 이미 로딩 중이면 중복 요청 방지
if (isTransactionDataLoading) {
return;
}
// 로딩 상태 플래그 설정
isTransactionDataLoading = true;
// 전역 로딩 이벤트 발생 (DashboardEventManager 사용)
if (window.DashboardEventManager) {
window.DashboardEventManager.trigger('loading-start');
}
// 필터 조건을 파라미터로 API 호출
ModernDashboardApi.getTransactionInfo(type, function(data) {
// 데이터 처리 완료 후 UI 업데이트
// 테이블 업데이트
updateTransactionTable(data);
// 전역 로딩 완료 이벤트 발생
if (window.DashboardEventManager) {
window.DashboardEventManager.trigger('loading-end');
}
// 로딩 상태 플래그 해제
isTransactionDataLoading = false;
});
}
// 트랜잭션 필터 버튼 클릭 이벤트
$('.tran-filter-btn').click(function() {
// 모든 버튼 비활성화
$('.tran-filter-btn').removeClass('active');
// 클릭한 버튼 활성화
$(this).addClass('active');
// 선택한 타입에 따라 트랜잭션 데이터 필터링 로직 추가
var type = $(this).data('type');
loadTransactionData(type);
});
// 실제 서비스에서 표시할 거래 유형 목록 (정렬 순서 포함)
var preferredTransactionOrder = ['MCI', 'EAI', 'FEP', 'MCU', 'APIGW'];
// refreshDashboardData 함수가 호출될 때 거래 처리 테이블이 초기화되지 않도록
// modern-dashboard.js 파일의 refreshDashboardData 함수를 확장합니다.
$(document).ready(function() {
// DashboardEventManager를 사용하여 새로고침 이벤트 구독
if (window.DashboardEventManager) {
window.DashboardEventManager.on('refresh', function() {
// 트랜잭션 정보 조회 - 현재 선택된 필터 사용
var activeType = $('.tran-filter-btn.active').data('type') || 'all';
console.log('거래처리 집계 데이터 갱신 시작: ' + activeType);
loadTransactionData(activeType);
});
// 초기화 완료 이벤트 로깅
console.log('거래처리 집계 컴포넌트 초기화 완료');
}
// 페이지 로드 시 데이터 로드
var activeType = $('.tran-filter-btn.active').data('type') || 'all';
loadTransactionData(activeType);
});
// 성공 비율에 따른 색상 계산 함수
function calculateStatusColor(successRatio) {
// 성공 비율이 0.5(50%)일 때 주황색(#ff9800)
// 성공 비율이 1.0(100%)에 가까울수록 초록색(#4caf50)
// 성공 비율이 0.0(0%)에 가까울수록 빨간색(#e53935)
// 정확히 50%에 가까울 때는 주황색
if (Math.abs(successRatio - 0.5) < 0.05) {
return '#ff9800'; // 주황색 (50:50 비율 근처)
} else if (successRatio > 0.5) {
// 50% ~ 100% 사이: 주황색에서 초록색으로 매우 급격한 변화
// 55% 이상이면 거의 바로 초록색으로 변화
var greenRatio;
if (successRatio > 0.65) {
// 65% 이상이면 완전한 초록색
greenRatio = 1;
} else {
// 50%~65% 사이에서 급격하게 변화
greenRatio = (successRatio - 0.5) * 6.67; // 0.5에서 0.65 사이를 0에서 1로 매핑
}
// 주황색(#ff9800)에서 초록색(#4caf50)으로 보간
var r = Math.round(255 - (255 - 76) * greenRatio);
var g = Math.round(152 + (175 - 152) * greenRatio);
var b = Math.round(0 + (80 - 0) * greenRatio);
return 'rgb(' + r + ',' + g + ',' + b + ')';
} else {
// 0% ~ 50% 사이: 빨간색에서 주황색으로 매우 급격한 변화
// 45% 이하면 거의 바로 빨간색으로 변화
var orangeRatio;
if (successRatio < 0.35) {
// 35% 이하면 완전한 빨간색
orangeRatio = 0;
} else {
// 35%~50% 사이에서 급격하게 변화
orangeRatio = (successRatio - 0.35) * 6.67; // 0.35에서 0.5 사이를 0에서 1로 매핑
}
// 빨간색(#e53935)에서 주황색(#ff9800)으로 보간
var r = Math.round(229 + (255 - 229) * orangeRatio);
var g = Math.round(57 + (152 - 57) * orangeRatio);
var b = Math.round(53 + (0 - 53) * orangeRatio);
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
}
</script>
<style>
/* 트랜잭션 섹션 */
.dashboard-transactions .section-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
flex-shrink: 0;
}
.dashboard-transactions .card {
border-radius: 0.35rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
overflow: hidden;
height: 242px;
}
.transaction-filters {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid #e3e6f0;
}
.filter-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.filter-btn:hover {
background-color: #e9ecef;
color: #495057;
}
.filter-btn.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.transaction-table-container {
max-height: 300px;
overflow-y: auto;
position: relative;
}
.transaction-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
table-layout: fixed;
}
.transaction-table th {
background-color: #f8f9fc;
font-weight: 600;
text-align: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid #e3e6f0;
position: sticky;
top: 0;
z-index: 10;
}
.transaction-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #f0f0f0;
vertical-align: middle;
}
.transaction-table td:nth-child(1) {
text-align: left;
}
.transaction-table td:nth-child(2) {
text-align: center;
}
.transaction-table td:nth-child(n+3) {
text-align: center;
}
.transaction-table tr:last-child td {
border-bottom: none;
}
.transaction-table tr:hover {
background-color: rgba(0, 0, 0, 0.02);
}
.transaction-table tr.total-row {
background-color: rgba(200, 200, 200, 0.2);
font-weight: 600;
}
.transaction-table tr.total-row td {
border-bottom: 2px solid #9e9e9e;
}
.transaction-table tr.total-row .group-name {
color: #616161;
}
/* 인스턴스 상태 표시 */
.instance-status {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
}
.status-normal {
background-color: #4caf50;
}
.status-warning {
background-color: #ff9800;
}
.status-error {
background-color: #e53935;
}
/* 메트릭 스타일 */
.metric {
display: flex;
flex-direction: column;
position: relative;
min-height: 24px;
}
.metric-value {
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.clickable {
cursor: pointer;
font-weight: 500;
}
.transaction-count {
color: #4caf50;
font-weight: 600;
}
.error-count {
color: #e53935;
font-weight: 600;
}
.timeout-count {
color: #ff9800;
font-weight: 600;
}
.syserror-count {
color: #e53935;
font-weight: 600;
}
.metric-change {
font-size: 0.75rem;
color: #4caf50;
font-weight: 500;
display: inline-flex;
align-items: center;
transition: opacity 0.3s ease;
position: relative;
margin-left: 2px;
white-space: nowrap;
}
.error-change {
color: #e53935;
}
.timeout-change {
color: #ff9800;
}
.syserror-change {
color: #e53935;
}
.metric-change i {
font-size: 1rem;
margin-right: 0;
}
/* 그룹 이름 스타일 */
.group-name {
text-align: center;
}
/* 대시보드 헤더 스타일 */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
/* 제목과 버튼을 한 줄에 나란히 표시하기 위한 컨테이너 */
.header-title-container {
display: flex;
align-items: center;
}
/* 트랜지션 효과 제거 */
#transaction-table-body {
/* transition: opacity 0.2s ease; */
}
</style>
@@ -0,0 +1,275 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.List"%>
<%@ page import="java.util.ArrayList"%>
<%@ page import="java.util.HashMap"%>
<%@ page import="java.util.Map"%>
<%@ page import="com.eactive.eai.rms.common.context.MonitoringContext"%>
<%@ page import="com.eactive.eai.rms.common.util.CommonUtil"%>
<%@ page import="com.eactive.eai.rms.common.datasource.DataSourceType"%>
<%@ page import="com.eactive.eai.rms.common.datasource.DataSourceTypeManager"%>
<%@ page import="com.eactive.eai.common.util.SystemUtil"%>
<%@ page import="javax.persistence.criteria.CriteriaBuilder" %>
<%@ page import="java.util.Comparator" %>
<%@ 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");
MonitoringContext monitoringContext = (MonitoringContext)CommonUtil.getBean(request, "monitoringContext");
String NEW_TITLE = (String) session.getAttribute(MonitoringContext.NEW_DASHBOARD_TITLE);
String fepBatchUrl = monitoringContext.getStringProperty("FEP_BATCH_SERVER", "");
String eaiBatchUrl = monitoringContext.getStringProperty("EAI_BATCH_SERVER", "");
// 온라인 데이터 소스 타입 목록 가져오기
List<DataSourceType> srvList = DataSourceTypeManager.getOnlineDataSourceTypes();
// GW 추가
if( DataSourceTypeManager.getDataSourceType("FGW") != null ){
srvList.add(DataSourceTypeManager.getDataSourceType("FGW"));
}
if( DataSourceTypeManager.getDataSourceType("CGW") != null ){
srvList.add(DataSourceTypeManager.getDataSourceType("CGW"));
}
int srvCnt = srvList.size();
// 서버 타입별 목록을 저장할 맵 생성
Map<String, List<DataSourceType>> serverTypeMap = new HashMap<>();
// 서버타입 맵을 application 스코프에 저장 (모든 컴포넌트에서 접근 가능)
application.setAttribute("serverTypeMap", serverTypeMap);
// 서버 타입별로 분류
for (DataSourceType server : srvList) {
String serverName = server.getName();
// 서버 타입에 해당하는 리스트가 없으면 생성
if (!serverTypeMap.containsKey(serverName)) {
serverTypeMap.put(serverName, new ArrayList<DataSourceType>());
}
// 서버를 해당 타입 리스트에 추가
serverTypeMap.get(serverName).add(server);
}
// BAP(일괄전송) 서버 타입 추가
DataSourceType bapServer = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.BAP);
if (bapServer != null && !serverTypeMap.containsKey(DataSourceTypeManager.BAP)) {
List<DataSourceType> bapList = new ArrayList<>();
bapList.add(bapServer);
serverTypeMap.put(DataSourceTypeManager.BAP, bapList);
}
// 서버 데이터 생성 (JavaScript 용)
StringBuilder serverDataJS = new StringBuilder();
serverDataJS.append("var serverDataArray = [");
boolean first = true;
for (Map.Entry<String, List<DataSourceType>> entry : serverTypeMap.entrySet()) {
String serverType = entry.getKey();
List<DataSourceType> servers = entry.getValue();
// BAT 서버는 제외
if (DataSourceTypeManager.BAT.equals(serverType)) {
continue;
}
for (DataSourceType server : servers) {
if (!first) {
serverDataJS.append(",");
}
String name = server.getName();
String text = server.getText();
serverDataJS.append("{");
serverDataJS.append("type:'").append(serverType).append("',");
serverDataJS.append("name:'").append(name).append("',");
serverDataJS.append("text:'").append(text).append("'");
serverDataJS.append("}");
first = false;
}
}
serverDataJS.append("];");
%>
<!DOCTYPE html>
<html lang="ko">
<head>
<title>eLink 모니터링 대시보드</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<!-- 폰트 -->
<jsp:include page="/jsp/common/include/fonts.jsp"/>
<!-- 아이콘 -->
<link href="<%=request.getContextPath()%>/addon/bootstrap-icons-1.11.3/bootstrap-icons.min.css" rel="stylesheet" />
<!-- 커스텀 CSS -->
<link href="<%=request.getContextPath()%>/dashboard-modern/css/main.css" rel="stylesheet" />
<!-- jQuery -->
<script src="<%=request.getContextPath()%>/js/jquery-3.7.1.min.js"></script>
<!-- 커스텀 JavaScript -->
<script src="<%=request.getContextPath()%>/dashboard-modern/js/modern-dashboard-api.js"></script>
<script src="<%=request.getContextPath()%>/dashboard-modern/js/modern-dashboard.js"></script>
<!-- API 초기화 (가장 먼저 실행) -->
<script>
// 페이지 로드 전 API 초기화
ModernDashboardApi.init('<%=request.getContextPath()%>');
</script>
<style>
/* 1번 줄 (4:3:1 비율) - 서버 상태, 배치 처리 상태, 피크 처리 성능 */
.first-row {
grid-template-columns: 4fr 3fr 1fr;
}
/* 2번 줄 (1:1 비율) - 거래 처리 집계, 실시간 트랜잭션 현황 */
.second-row {
grid-template-columns: 1fr 1fr;
}
/* 3번 줄 (7:3 비율) - 어댑터 상태, 장애통보(SMS) */
.third-row {
grid-template-columns: 6fr 4fr;
}
/* 컴팩트한 대시보드를 위한 추가 스타일 */
.container {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
/* 카드 내부 여백 줄임 */
.card {
padding: 0.75rem;
}
/* 테이블 행 높이 줄임 */
table tr td, table tr th {
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
/* 반응형 디자인 */
@media (max-width: 1200px) {
.first-row {
grid-template-columns: 1fr;
}
.second-row {
grid-template-columns: 1fr;
}
.third-row {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<!-- 헤더 영역 -->
<jsp:include page="components/dashboard-header.jsp" />
<!-- 1번 줄 (4:3:1 비율) - 서버 상태, 배치 처리 상태, 피크 처리 성능 -->
<div class="dashboard-row first-row">
<!-- 서버 상태 섹션 (4/8 비율) -->
<div class="server-container">
<jsp:include page="components/dashboard-servers.jsp">
<jsp:param name="serverTypeMap" value="<%= serverTypeMap %>" />
</jsp:include>
</div>
<!-- 배치 처리 상태 섹션 (3/8 비율) -->
<div class="batch-container">
<jsp:include page="components/dashboard-batch.jsp" />
</div>
<!-- 피크 처리 성능 섹션 (1/8 비율) -->
<div class="peak-container">
<jsp:include page="components/dashboard-peak.jsp" />
</div>
</div>
<!-- 2번 줄 (1:1 비율) - 거래 처리 집계, 실시간 트랜잭션 현황 -->
<div class="dashboard-row second-row">
<!-- 거래 처리 집계 섹션 (좌측) -->
<div class="transaction-container" style="overflow-y:auto;">
<jsp:include page="components/dashboard-transactions.jsp">
<jsp:param name="serverTypeMap" value="<%= serverTypeMap %>" />
</jsp:include>
</div>
<!-- 실시간 트랜잭션 현황 섹션 (우측) -->
<div class="transaction-overview-container">
<jsp:include page="components/dashboard-tran-chart.jsp" />
</div>
</div>
<!-- 3번 줄 (8:2 비율) - 어댑터 상태, 장애통보(SMS) -->
<div class="dashboard-row third-row">
<!-- 어댑터 섹션 (8/10 비율) -->
<div class="adapters-container">
<jsp:include page="components/dashboard-adapter.jsp">
<jsp:param name="serverTypeMap" value="<%= serverTypeMap %>" />
</jsp:include>
</div>
<!-- 장애통보(SMS) 섹션 (2/10 비율) -->
<div class="sms-container">
<jsp:include page="components/dashboard-sms.jsp" />
</div>
</div>
<!-- 인터페이스 로그 조회 모달 -->
<jsp:include page="components/dashboard-interface-detail.jsp" />
<!-- 푸터 영역 -->
<jsp:include page="components/dashboard-footer.jsp" />
</div>
<script type="text/javascript">
// 서버 데이터 초기화
<%= serverDataJS.toString() %>
// 자동 새로고침 타이머 변수 (window 객체에 할당)
window.refreshTimer = null;
window.refreshInterval = 5; // 기본값 10초
// 숫자 포맷 유틸리티 함수
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// 서버 데이터 설정 및 대시보드 초기화
$(document).ready(function() {
// 대시보드 초기화
initDashboard('<%=request.getContextPath()%>');
// 현재 시간 표시 및 업데이트
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
// 초기 데이터 로드
refreshDashboardData();
// 로컬 스토리지에서 저장된 새로고침 간격 불러오기
if (localStorage.getItem('dashboardRefreshInterval')) {
window.refreshInterval = parseInt(localStorage.getItem('dashboardRefreshInterval'));
// 저장된 간격에 해당하는 버튼 활성화
$('.refresh-btn').removeClass('active');
$('.refresh-btn[data-interval="' + window.refreshInterval + '"]').addClass('active');
}
// modern-dashboard.js의 함수 호출
window.startRefreshTimer();
window.updateRefreshStatus();
});
</script>
</body>
</html>