Merge remote-tracking branch 'z-eactive/feature/modern-dashboard' into devs/dashboard
This commit is contained in:
@@ -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">×</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">×</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>
|
||||
Reference in New Issue
Block a user