모던 대시보드 패치

- ApexCharts 기반 실시간 모니터링 대시보드
- 어댑터/배치/서버/소켓/트랜잭션 모니터링 컴포넌트
- 처리량 계산 및 데이터 매핑 서비스
This commit is contained in:
youna
2025-11-14 17:59:06 +09:00
parent 38057809cf
commit 972819b492
72 changed files with 49662 additions and 547 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,21 @@
/*!
* @svgdotjs/svg.resize.js - An extension for svg.js which allows to resize elements which are selected
* @version 2.0.4
* https://github.com/svgdotjs/svg.resize.js
*
* @copyright [object Object]
* @license MIT
*
* BUILT: Fri Sep 13 2024 12:43:14 GMT+0200 (Central European Summer Time)
*/
/*!
* @svgdotjs/svg.select.js - An extension of svg.js which allows to select elements with mouse
* @version 4.0.1
* https://github.com/svgdotjs/svg.select.js
*
* @copyright Ulrich-Matthias Schäfer
* @license MIT
*
* BUILT: Mon Jul 01 2024 15:04:42 GMT+0200 (Central European Summer Time)
*/
File diff suppressed because one or more lines are too long
+679
View File
@@ -0,0 +1,679 @@
@keyframes opaque {
0% {
opacity: 0
}
to {
opacity: 1
}
}
@keyframes resizeanim {
0%,
to {
opacity: 0
}
}
.apexcharts-canvas {
position: relative;
direction: ltr !important;
user-select: none
}
.apexcharts-canvas ::-webkit-scrollbar {
-webkit-appearance: none;
width: 6px
}
.apexcharts-canvas ::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: rgba(0, 0, 0, .5);
box-shadow: 0 0 1px rgba(255, 255, 255, .5);
-webkit-box-shadow: 0 0 1px rgba(255, 255, 255, .5)
}
.apexcharts-inner {
position: relative
}
.apexcharts-text tspan {
font-family: inherit
}
rect.legend-mouseover-inactive,
.legend-mouseover-inactive rect,
.legend-mouseover-inactive path,
.legend-mouseover-inactive circle,
.legend-mouseover-inactive line,
.legend-mouseover-inactive text.apexcharts-yaxis-title-text,
.legend-mouseover-inactive text.apexcharts-yaxis-label {
transition: .15s ease all;
opacity: .2
}
.apexcharts-legend-text {
padding-left: 15px;
margin-left: -15px;
}
.apexcharts-series-collapsed {
opacity: 0
}
.apexcharts-tooltip {
border-radius: 5px;
box-shadow: 2px 2px 6px -4px #999;
cursor: default;
font-size: 14px;
left: 62px;
opacity: 0;
pointer-events: none;
position: absolute;
top: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
white-space: nowrap;
z-index: 12;
transition: .15s ease all
}
.apexcharts-tooltip.apexcharts-active {
opacity: 1;
transition: .15s ease all
}
.apexcharts-tooltip.apexcharts-theme-light {
border: 1px solid #e3e3e3;
background: rgba(255, 255, 255, .96)
}
.apexcharts-tooltip.apexcharts-theme-dark {
color: #fff;
background: rgba(30, 30, 30, .8)
}
.apexcharts-tooltip * {
font-family: inherit
}
.apexcharts-tooltip-title {
padding: 6px;
font-size: 15px;
margin-bottom: 4px
}
.apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title {
background: #eceff1;
border-bottom: 1px solid #ddd
}
.apexcharts-tooltip.apexcharts-theme-dark .apexcharts-tooltip-title {
background: rgba(0, 0, 0, .7);
border-bottom: 1px solid #333
}
.apexcharts-tooltip-text-goals-value,
.apexcharts-tooltip-text-y-value,
.apexcharts-tooltip-text-z-value {
display: inline-block;
margin-left: 5px;
font-weight: 600
}
.apexcharts-tooltip-text-goals-label:empty,
.apexcharts-tooltip-text-goals-value:empty,
.apexcharts-tooltip-text-y-label:empty,
.apexcharts-tooltip-text-y-value:empty,
.apexcharts-tooltip-text-z-value:empty,
.apexcharts-tooltip-title:empty {
display: none
}
.apexcharts-tooltip-text-goals-label,
.apexcharts-tooltip-text-goals-value {
padding: 6px 0 5px
}
.apexcharts-tooltip-goals-group,
.apexcharts-tooltip-text-goals-label,
.apexcharts-tooltip-text-goals-value {
display: flex
}
.apexcharts-tooltip-text-goals-label:not(:empty),
.apexcharts-tooltip-text-goals-value:not(:empty) {
margin-top: -6px
}
.apexcharts-tooltip-marker {
width: 12px;
height: 12px;
position: relative;
top: 0;
margin-right: 10px;
border-radius: 50%
}
.apexcharts-tooltip-series-group {
padding: 0 10px;
display: none;
text-align: left;
justify-content: left;
align-items: center
}
.apexcharts-tooltip-series-group.apexcharts-active .apexcharts-tooltip-marker {
opacity: 1
}
.apexcharts-tooltip-series-group.apexcharts-active,
.apexcharts-tooltip-series-group:last-child {
padding-bottom: 4px
}
.apexcharts-tooltip-y-group {
padding: 6px 0 5px
}
.apexcharts-custom-tooltip,
.apexcharts-tooltip-box {
padding: 4px 8px
}
.apexcharts-tooltip-boxPlot {
display: flex;
flex-direction: column-reverse
}
.apexcharts-tooltip-box>div {
margin: 4px 0
}
.apexcharts-tooltip-box span.value {
font-weight: 700
}
.apexcharts-tooltip-rangebar {
padding: 5px 8px
}
.apexcharts-tooltip-rangebar .category {
font-weight: 600;
color: #777
}
.apexcharts-tooltip-rangebar .series-name {
font-weight: 700;
display: block;
margin-bottom: 5px
}
.apexcharts-xaxistooltip,
.apexcharts-yaxistooltip {
opacity: 0;
pointer-events: none;
color: #373d3f;
font-size: 13px;
text-align: center;
border-radius: 2px;
position: absolute;
z-index: 10;
background: #eceff1;
border: 1px solid #90a4ae
}
.apexcharts-xaxistooltip {
padding: 9px 10px;
transition: .15s ease all
}
.apexcharts-xaxistooltip.apexcharts-theme-dark {
background: rgba(0, 0, 0, .7);
border: 1px solid rgba(0, 0, 0, .5);
color: #fff
}
.apexcharts-xaxistooltip:after,
.apexcharts-xaxistooltip:before {
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none
}
.apexcharts-xaxistooltip:after {
border-color: transparent;
border-width: 6px;
margin-left: -6px
}
.apexcharts-xaxistooltip:before {
border-color: transparent;
border-width: 7px;
margin-left: -7px
}
.apexcharts-xaxistooltip-bottom:after,
.apexcharts-xaxistooltip-bottom:before {
bottom: 100%
}
.apexcharts-xaxistooltip-top:after,
.apexcharts-xaxistooltip-top:before {
top: 100%
}
.apexcharts-xaxistooltip-bottom:after {
border-bottom-color: #eceff1
}
.apexcharts-xaxistooltip-bottom:before {
border-bottom-color: #90a4ae
}
.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:after,
.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:before {
border-bottom-color: rgba(0, 0, 0, .5)
}
.apexcharts-xaxistooltip-top:after {
border-top-color: #eceff1
}
.apexcharts-xaxistooltip-top:before {
border-top-color: #90a4ae
}
.apexcharts-xaxistooltip-top.apexcharts-theme-dark:after,
.apexcharts-xaxistooltip-top.apexcharts-theme-dark:before {
border-top-color: rgba(0, 0, 0, .5)
}
.apexcharts-xaxistooltip.apexcharts-active {
opacity: 1;
transition: .15s ease all
}
.apexcharts-yaxistooltip {
padding: 4px 10px
}
.apexcharts-yaxistooltip.apexcharts-theme-dark {
background: rgba(0, 0, 0, .7);
border: 1px solid rgba(0, 0, 0, .5);
color: #fff
}
.apexcharts-yaxistooltip:after,
.apexcharts-yaxistooltip:before {
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none
}
.apexcharts-yaxistooltip:after {
border-color: transparent;
border-width: 6px;
margin-top: -6px
}
.apexcharts-yaxistooltip:before {
border-color: transparent;
border-width: 7px;
margin-top: -7px
}
.apexcharts-yaxistooltip-left:after,
.apexcharts-yaxistooltip-left:before {
left: 100%
}
.apexcharts-yaxistooltip-right:after,
.apexcharts-yaxistooltip-right:before {
right: 100%
}
.apexcharts-yaxistooltip-left:after {
border-left-color: #eceff1
}
.apexcharts-yaxistooltip-left:before {
border-left-color: #90a4ae
}
.apexcharts-yaxistooltip-left.apexcharts-theme-dark:after,
.apexcharts-yaxistooltip-left.apexcharts-theme-dark:before {
border-left-color: rgba(0, 0, 0, .5)
}
.apexcharts-yaxistooltip-right:after {
border-right-color: #eceff1
}
.apexcharts-yaxistooltip-right:before {
border-right-color: #90a4ae
}
.apexcharts-yaxistooltip-right.apexcharts-theme-dark:after,
.apexcharts-yaxistooltip-right.apexcharts-theme-dark:before {
border-right-color: rgba(0, 0, 0, .5)
}
.apexcharts-yaxistooltip.apexcharts-active {
opacity: 1
}
.apexcharts-yaxistooltip-hidden {
display: none
}
.apexcharts-xcrosshairs,
.apexcharts-ycrosshairs {
pointer-events: none;
opacity: 0;
transition: .15s ease all
}
.apexcharts-xcrosshairs.apexcharts-active,
.apexcharts-ycrosshairs.apexcharts-active {
opacity: 1;
transition: .15s ease all
}
.apexcharts-ycrosshairs-hidden {
opacity: 0
}
.apexcharts-selection-rect {
cursor: move
}
.svg_select_shape {
stroke-width: 1;
stroke-dasharray: 10 10;
stroke: black;
stroke-opacity: 0.1;
pointer-events: none;
fill: none;
}
.svg_select_handle {
stroke-width: 3;
stroke: black;
fill: none;
}
.svg_select_handle_r {
cursor: e-resize;
}
.svg_select_handle_l {
cursor: w-resize;
}
.apexcharts-svg.apexcharts-zoomable.hovering-zoom {
cursor: crosshair
}
.apexcharts-svg.apexcharts-zoomable.hovering-pan {
cursor: move
}
.apexcharts-menu-icon,
.apexcharts-pan-icon,
.apexcharts-reset-icon,
.apexcharts-selection-icon,
.apexcharts-toolbar-custom-icon,
.apexcharts-zoom-icon,
.apexcharts-zoomin-icon,
.apexcharts-zoomout-icon {
cursor: pointer;
width: 20px;
height: 20px;
line-height: 24px;
color: #6e8192;
text-align: center
}
.apexcharts-menu-icon svg,
.apexcharts-reset-icon svg,
.apexcharts-zoom-icon svg,
.apexcharts-zoomin-icon svg,
.apexcharts-zoomout-icon svg {
fill: #6e8192
}
.apexcharts-selection-icon svg {
fill: #444;
transform: scale(.76)
}
.apexcharts-theme-dark .apexcharts-menu-icon svg,
.apexcharts-theme-dark .apexcharts-pan-icon svg,
.apexcharts-theme-dark .apexcharts-reset-icon svg,
.apexcharts-theme-dark .apexcharts-selection-icon svg,
.apexcharts-theme-dark .apexcharts-toolbar-custom-icon svg,
.apexcharts-theme-dark .apexcharts-zoom-icon svg,
.apexcharts-theme-dark .apexcharts-zoomin-icon svg,
.apexcharts-theme-dark .apexcharts-zoomout-icon svg {
fill: #f3f4f5
}
.apexcharts-canvas .apexcharts-reset-zoom-icon.apexcharts-selected svg,
.apexcharts-canvas .apexcharts-selection-icon.apexcharts-selected svg,
.apexcharts-canvas .apexcharts-zoom-icon.apexcharts-selected svg {
fill: #008ffb
}
.apexcharts-theme-light .apexcharts-menu-icon:hover svg,
.apexcharts-theme-light .apexcharts-reset-icon:hover svg,
.apexcharts-theme-light .apexcharts-selection-icon:not(.apexcharts-selected):hover svg,
.apexcharts-theme-light .apexcharts-zoom-icon:not(.apexcharts-selected):hover svg,
.apexcharts-theme-light .apexcharts-zoomin-icon:hover svg,
.apexcharts-theme-light .apexcharts-zoomout-icon:hover svg {
fill: #333
}
.apexcharts-menu-icon,
.apexcharts-selection-icon {
position: relative
}
.apexcharts-reset-icon {
margin-left: 5px
}
.apexcharts-menu-icon,
.apexcharts-reset-icon,
.apexcharts-zoom-icon {
transform: scale(.85)
}
.apexcharts-zoomin-icon,
.apexcharts-zoomout-icon {
transform: scale(.7)
}
.apexcharts-zoomout-icon {
margin-right: 3px
}
.apexcharts-pan-icon {
transform: scale(.62);
position: relative;
left: 1px;
top: 0
}
.apexcharts-pan-icon svg {
fill: #fff;
stroke: #6e8192;
stroke-width: 2
}
.apexcharts-pan-icon.apexcharts-selected svg {
stroke: #008ffb
}
.apexcharts-pan-icon:not(.apexcharts-selected):hover svg {
stroke: #333
}
.apexcharts-toolbar {
position: absolute;
z-index: 11;
max-width: 176px;
text-align: right;
border-radius: 3px;
padding: 0 6px 2px;
display: flex;
justify-content: space-between;
align-items: center
}
.apexcharts-menu {
background: #fff;
position: absolute;
top: 100%;
border: 1px solid #ddd;
border-radius: 3px;
padding: 3px;
right: 10px;
opacity: 0;
min-width: 110px;
transition: .15s ease all;
pointer-events: none
}
.apexcharts-menu.apexcharts-menu-open {
opacity: 1;
pointer-events: all;
transition: .15s ease all
}
.apexcharts-menu-item {
padding: 6px 7px;
font-size: 12px;
cursor: pointer
}
.apexcharts-theme-light .apexcharts-menu-item:hover {
background: #eee
}
.apexcharts-theme-dark .apexcharts-menu {
background: rgba(0, 0, 0, .7);
color: #fff
}
@media screen and (min-width:768px) {
.apexcharts-canvas:hover .apexcharts-toolbar {
opacity: 1
}
}
.apexcharts-canvas .apexcharts-element-hidden,
.apexcharts-datalabel.apexcharts-element-hidden,
.apexcharts-hide .apexcharts-series-points {
opacity: 0;
}
.apexcharts-hidden-element-shown {
opacity: 1;
transition: 0.25s ease all;
}
.apexcharts-datalabel,
.apexcharts-datalabel-label,
.apexcharts-datalabel-value,
.apexcharts-datalabels,
.apexcharts-pie-label {
cursor: default;
pointer-events: none
}
.apexcharts-pie-label-delay {
opacity: 0;
animation-name: opaque;
animation-duration: .3s;
animation-fill-mode: forwards;
animation-timing-function: ease
}
.apexcharts-radialbar-label {
cursor: pointer;
}
.apexcharts-annotation-rect,
.apexcharts-area-series .apexcharts-area,
.apexcharts-gridline,
.apexcharts-line,
.apexcharts-point-annotation-label,
.apexcharts-radar-series path:not(.apexcharts-marker),
.apexcharts-radar-series polygon,
.apexcharts-toolbar svg,
.apexcharts-tooltip .apexcharts-marker,
.apexcharts-xaxis-annotation-label,
.apexcharts-yaxis-annotation-label,
.apexcharts-zoom-rect,
.no-pointer-events {
pointer-events: none
}
.apexcharts-tooltip-active .apexcharts-marker {
transition: .15s ease all
}
.resize-triggers {
animation: 1ms resizeanim;
visibility: hidden;
opacity: 0;
height: 100%;
width: 100%;
overflow: hidden
}
.contract-trigger:before,
.resize-triggers,
.resize-triggers>div {
content: " ";
display: block;
position: absolute;
top: 0;
left: 0
}
.resize-triggers>div {
height: 100%;
width: 100%;
background: #eee;
overflow: auto
}
.contract-trigger:before {
overflow: hidden;
width: 200%;
height: 200%
}
.apexcharts-bar-goals-markers {
pointer-events: none
}
.apexcharts-bar-shadows {
pointer-events: none
}
.apexcharts-rangebar-goals-markers {
pointer-events: none
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,55 @@
{
"name": "en",
"options": {
"months": [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
],
"shortMonths": [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
],
"days": [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
"shortDays": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
"toolbar": {
"exportToSVG": "Download SVG",
"exportToPNG": "Download PNG",
"exportToCSV": "Download CSV",
"menu": "Menu",
"selection": "Selection",
"selectionZoom": "Selection Zoom",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"pan": "Panning",
"reset": "Reset Zoom"
}
}
}
@@ -0,0 +1,55 @@
{
"name": "ko",
"options": {
"months": [
"1월",
"2월",
"3월",
"4월",
"5월",
"6월",
"7월",
"8월",
"9월",
"10월",
"11월",
"12월"
],
"shortMonths": [
"1월",
"2월",
"3월",
"4월",
"5월",
"6월",
"7월",
"8월",
"9월",
"10월",
"11월",
"12월"
],
"days": [
"일요일",
"월요일",
"화요일",
"수요일",
"목요일",
"금요일",
"토요일"
],
"shortDays": ["일", "월", "화", "수", "목", "금", "토"],
"toolbar": {
"exportToSVG": "SVG 다운로드",
"exportToPNG": "PNG 다운로드",
"exportToCSV": "CSV 다운로드",
"menu": "메뉴",
"selection": "선택",
"selectionZoom": "선택영역 확대",
"zoomIn": "확대",
"zoomOut": "축소",
"pan": "패닝",
"reset": "원래대로"
}
}
}
@@ -0,0 +1,758 @@
/* components.css - 공통 컴포넌트 스타일 정의 */
/* 카드 컴포넌트 */
.card {
background-color: white;
border-radius: var(--rounded-md);
box-shadow: var(--shadow);
border: 1px solid var(--color-gray-200);
padding: 0.75rem;
height: 100%;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-gray-200);
}
.card-title {
font-size: 1rem;
font-weight: 600;
color: var(--color-gray-800);
margin: 0;
}
.card-subtitle {
font-size: 0.875rem;
color: var(--color-gray-600);
margin-top: 0.25rem;
}
.card-actions {
display: flex;
gap: 0.5rem;
}
.card-content {
height: calc(100% - 2.25rem);
}
.summary-card {
background-color: white;
padding: 1.5rem;
border-radius: var(--rounded-lg);
box-shadow: var(--shadow);
margin-bottom: 0;
border: 1px solid var(--color-gray-200);
height: 100%;
}
/* 버튼 컴포넌트 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.4rem 0.75rem;
border-radius: var(--rounded);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
background-color: var(--color-gray-100);
color: var(--color-gray-700);
}
.btn:hover {
background-color: var(--color-gray-200);
}
.btn:active {
transform: translateY(1px);
}
.btn i {
margin-right: 0.25rem;
}
.btn.btn-primary {
background-color: var(--color-primary);
color: white;
}
.btn.btn-primary:hover {
background-color: #20198e;
}
.btn.btn-outline {
background-color: transparent;
border-color: var(--color-gray-300);
}
.btn.btn-outline:hover {
background-color: var(--color-gray-100);
}
.btn.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.btn-group {
display: flex;
border-radius: var(--rounded);
overflow: hidden;
}
.btn-group .btn {
border-radius: 0;
border-right: 1px solid rgba(0, 0, 0, 0.1);
}
.btn-group .btn:first-child {
border-top-left-radius: var(--rounded);
border-bottom-left-radius: var(--rounded);
}
.btn-group .btn:last-child {
border-top-right-radius: var(--rounded);
border-bottom-right-radius: var(--rounded);
border-right: none;
}
.btn-group .btn.active {
background-color: var(--color-primary);
color: white;
}
/* 필터 및 탭 컴포넌트 */
.filter-group {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.24rem 0.5rem;
font-size: 0.75rem;
font-weight: 500;
border-radius: var(--rounded);
background-color: white;
color: var(--color-gray-700);
border: 1px solid var(--color-gray-300);
cursor: pointer;
transition: all 0.2s ease;
}
.filter-btn:hover {
background-color: var(--color-gray-100);
}
.filter-btn.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.tran-filter-btn {
padding: 0.24rem 0.5rem;
font-size: 0.75rem;
font-weight: 500;
border-radius: var(--rounded);
background-color: white;
color: var(--color-gray-700);
border: 1px solid var(--color-gray-300);
cursor: pointer;
transition: all 0.2s ease;
}
.tran-filter-btn:hover {
background-color: var(--color-gray-100);
}
.tran-filter-btn.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.card-controls {
display: flex;
gap: 0.5rem;
}
.card-tabs {
display: flex;
border-bottom: 1px solid var(--color-gray-200);
margin-bottom: 0.75rem;
overflow-x: auto;
}
.tab-btn {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-gray-600);
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.tab-btn:hover {
color: var(--color-primary);
}
.tab-btn.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
font-weight: 600;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* 상태 표시 컴포넌트 */
.server-status-badge,
.status-badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem;
border-radius: var(--rounded);
font-size: 0.75rem;
font-weight: 500;
}
.status-badge.success {
background-color: var(--color-success-light);
color: var(--color-success-dark);
}
.status-badge.warning {
background-color: var(--color-warning-light);
color: var(--color-warning-dark);
}
.status-badge.danger {
background-color: var(--color-danger-light);
color: var(--color-danger-dark);
}
.status-badge.info {
background-color: var(--color-info-light);
color: var(--color-info-dark);
}
.status-badge i {
margin-right: 0.25rem;
}
/* 상태 인디케이터 */
.instance-status {
display: inline-block;
width: 0.625rem;
height: 0.625rem;
border-radius: var(--rounded-full);
margin-right: 0.5rem;
}
.status-normal {
background-color: var(--color-success);
}
.status-warning {
background-color: var(--color-warning);
}
.status-error {
background-color: var(--color-danger);
}
.status-inactive {
background-color: var(--color-gray-400);
}
/* 테이블 컴포넌트 */
.instance-table,
.transaction-table,
.adapter-table {
width: 100%;
border-collapse: collapse;
font-size: 0.813rem;
}
.instance-table th,
.instance-table td,
.transaction-table th,
.transaction-table td,
.adapter-table th,
.adapter-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--color-gray-200);
}
.instance-table th,
.transaction-table th,
.adapter-table th {
font-weight: 600;
color: var(--color-gray-600);
background-color: var(--color-gray-50);
font-size: 0.75rem;
}
.transaction-table th {
padding: 0.875rem 1rem;
border-bottom: 2px solid var(--color-gray-200);
}
.instance-table tr:last-child td,
.adapter-table tr:last-child td {
border-bottom: none;
}
.transaction-table tr:hover {
background-color: var(--color-gray-50);
}
.transaction-table .text-right {
text-align: right;
}
.transaction-table .group-name {
font-weight: 600;
color: var(--color-gray-800);
}
/* 검색 컴포넌트 */
.search-container {
position: relative;
width: 100%;
max-width: 300px;
}
/* 검색 컴포넌트 */
.search-container {
position: relative;
width: 250px;
}
.search-container i {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--color-gray-500);
}
#server-search {
width: 100%;
padding: 0.3rem 0.75rem 0.3rem 2rem;
font-size: 0.813rem;
border-radius: var(--rounded);
border: 1px solid var(--color-gray-300);
background-color: white;
}
#server-search:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2);
}
/* 차트 컨테이너 */
.chart-container {
height: 184px;
width: 100%;
background-color: white;
border-radius: var(--rounded-md);
overflow: hidden;
position: relative;
}
.chart-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-gray-400);
font-size: 1.5rem;
font-weight: 500;
}
/* 로딩 애니메이션 */
.loading {
position: relative;
}
.loading:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
border-radius: var(--rounded-md);
z-index: 10;
}
.loading:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 24px;
height: 24px;
margin-top: -12px;
margin-left: -12px;
border-radius: 50%;
border: 2px solid rgba(79, 70, 229, 0.3);
border-top-color: var(--color-primary);
animation: spin 0.8s linear infinite;
z-index: 11;
}
/* 메트릭 컴포넌트 */
.metric-group {
display: flex;
align-items: center;
gap: 1.5rem;
}
.metric {
display: flex;
flex-direction: column;
}
.metric-label {
font-size: 0.6875rem;
color: var(--color-gray-500);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.25rem;
}
.metric-value {
font-weight: 600;
color: var(--color-gray-800);
font-size: 0.875rem;
display: flex;
align-items: center;
}
.metric-value.success {
color: var(--color-success);
}
.metric-value.warning {
color: var(--color-warning);
}
.metric-value.danger {
color: var(--color-danger);
}
.metric-change {
font-size: 0.6875rem;
color: var(--color-success);
margin-left: 0.375rem;
font-weight: normal;
}
.clickable {
color: var(--color-primary);
cursor: pointer;
}
.clickable:hover {
text-decoration: underline;
}
/* 토글 버튼 */
.toggle-details {
background: none;
border: none;
color: var(--color-gray-500);
cursor: pointer;
padding: 0.25rem;
font-size: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
border-radius: var(--rounded);
}
.toggle-details:hover {
color: var(--color-primary);
background-color: var(--color-gray-100);
}
.toggle-details.open {
transform: rotate(180deg);
}
/* 테이블 컴포넌트 */
.table-container {
overflow-x: auto;
margin-bottom: 0.75rem;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
table th,
table td {
padding: 0.4rem 0.5rem;
text-align: left;
border-bottom: 1px solid var(--color-gray-200);
}
table th {
font-weight: 600;
color: var(--color-gray-700);
background-color: var(--color-gray-50);
}
table tr:last-child td {
border-bottom: none;
}
table tr:hover td {
background-color: var(--color-gray-50);
}
/* 알림 컴포넌트 */
.alert {
padding: 0.75rem;
border-radius: var(--rounded);
margin-bottom: 0.75rem;
font-size: 0.875rem;
display: flex;
align-items: flex-start;
}
.alert-icon {
margin-right: 0.5rem;
font-size: 1rem;
flex-shrink: 0;
}
.alert-content {
flex-grow: 1;
}
.alert-title {
font-weight: 600;
margin-bottom: 0.25rem;
}
.alert-message {
margin: 0;
}
.alert.alert-info {
background-color: var(--color-info-light);
color: var(--color-info-dark);
}
.alert.alert-success {
background-color: var(--color-success-light);
color: var(--color-success-dark);
}
.alert.alert-warning {
background-color: var(--color-warning-light);
color: var(--color-warning-dark);
}
.alert.alert-danger {
background-color: var(--color-danger-light);
color: var(--color-danger-dark);
}
/* 로딩 스피너 */
.spinner {
display: inline-block;
width: 1.5rem;
height: 1.5rem;
border: 2px solid rgba(var(--color-primary-rgb), 0.3);
border-radius: 50%;
border-top-color: var(--color-primary);
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 페이지네이션 */
.pagination {
display: flex;
justify-content: center;
margin-top: 0.75rem;
}
.pagination-item {
margin: 0 0.25rem;
}
.pagination-link {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: var(--rounded);
font-size: 0.875rem;
color: var(--color-gray-700);
text-decoration: none;
transition: all 0.2s ease;
}
.pagination-link:hover {
background-color: var(--color-gray-100);
}
.pagination-link.active {
background-color: var(--color-primary);
color: white;
}
.pagination-link.disabled {
color: var(--color-gray-400);
pointer-events: none;
}
/* 툴팁 */
.tooltip {
position: relative;
display: inline-block;
}
.tooltip-text {
visibility: hidden;
width: 120px;
background-color: var(--color-gray-800);
color: white;
text-align: center;
border-radius: var(--rounded);
padding: 0.4rem 0.5rem;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.2s;
font-size: 0.75rem;
pointer-events: none;
}
.tooltip-text::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: var(--color-gray-800) transparent transparent transparent;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
/* 애니메이션 */
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
/* 새로고침 버튼 애니메이션 */
.refresh-btn-spin {
animation: spin 1s linear infinite;
}
/* 컴팩트 모드 추가 스타일 */
.compact-mode .card {
padding: 0.5rem;
}
.compact-mode .card-header {
margin-bottom: 0.5rem;
padding-bottom: 0.4rem;
}
.compact-mode .card-title {
font-size: 0.95rem;
}
.compact-mode table th,
.compact-mode table td {
padding: 0.3rem 0.4rem;
}
.compact-mode .section-title {
font-size: 1rem;
margin-bottom: 0.5rem;
padding-bottom: 0.4rem;
}
/* peak 필터 스타일 */
.peak-filter-group {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.peak-filter-btn {
padding: 0.24rem 0.5rem;
font-size: 0.75rem;
font-weight: 500;
border-radius: var(--rounded);
background-color: white;
color: var(--color-gray-700);
border: 1px solid var(--color-gray-300);
cursor: pointer;
transition: all 0.2s ease;
}
.peak-filter-btn:hover {
background-color: var(--color-gray-100);
}
.peak-filter-btn.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
+245
View File
@@ -0,0 +1,245 @@
/* layout.css - 레이아웃 및 구조 스타일 */
/* 대시보드 메인 그리드 레이아웃 */
.dashboard-grid {
display: grid;
grid-template-columns: 2fr 1fr; /* 메인 콘텐츠(2/3), 사이드바(1/3) 비율 */
gap: 1.5rem;
margin-bottom: 1rem;
}
.dashboard-main-content {
width: 100%;
}
.dashboard-sidebar {
width: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* 기본 그리드 레이아웃 */
.dashboard-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 0.2rem;
}
/* 특별한 그리드 레이아웃 - 배치/피크 섹션용 */
.dashboard-row.batch-peak-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
/* 특별한 그리드 레이아웃 - 배치/피크 섹션용에서 중첩 방지 */
.dashboard-row.batch-peak-row > .dashboard-batch,
.dashboard-row.batch-peak-row > .dashboard-peak,
.dashboard-row.batch-peak-row > .batch-container,
.dashboard-row.batch-peak-row > .peak-container {
width: 100%;
margin-bottom: 0;
}
/* 요약 섹션 (1:2 비율) */
.dashboard-transactions {
margin-bottom: 0; /* 사이드바 내에서 하단 마진 제거 */
}
/* 어댑터 섹션 */
.dashboard-adapters {
margin-bottom: 0; /* 사이드바 내에서 하단 마진 제거 */
}
/* 트랜잭션 섹션 */
.dashboard-transactions {
margin-bottom: 0; /* 사이드바 내에서 하단 마진 제거 */
}
/* 헤더 */
header {
background-color: white;
padding: 0.8rem 1.5rem;
border-radius: var(--rounded-lg);
box-shadow: var(--shadow);
margin-bottom: 1.2rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-title {
display: flex;
align-items: center;
}
.header-title h1 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--color-gray-900);
}
.header-title i {
font-size: 1.5rem;
color: var(--color-primary);
margin-right: 0.75rem;
}
.header-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.time-display {
background-color: var(--color-gray-100);
padding: 0.5rem 0.75rem;
border-radius: var(--rounded);
font-size: 0.813rem;
font-weight: 500;
color: var(--color-gray-700);
}
/* 자동 새로고침 컨트롤 */
.refresh-controls {
display: flex;
align-items: center;
margin-right: 15px;
}
.refresh-label {
margin-right: 8px;
font-size: 0.875rem;
color: var(--color-gray-600);
}
.refresh-status {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.refresh-status.active {
background-color: var(--color-success);
animation: pulse 2s infinite;
}
.refresh-status.inactive {
background-color: var(--color-gray-400);
}
/* 푸터 */
footer {
margin-top: 1.5rem;
padding: 1rem 1.5rem 0.5rem 1.5rem;
border-top: 1px solid var(--color-gray-200);
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-info {
font-size: 0.813rem;
color: var(--color-gray-500);
}
.footer-info p {
margin: 0;
}
.footer-links {
display: flex;
gap: 1.5rem;
}
.footer-link {
font-size: 0.813rem;
color: var(--color-gray-600);
text-decoration: none;
}
.footer-link:hover {
color: var(--color-primary);
text-decoration: underline;
}
/* 서버-트랜잭션 행 (한 줄에 나오도록) */
.dashboard-row.server-transaction-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.server-container, .transaction-container {
width: 100%;
}
/* 어댑터 섹션 컨테이너 */
.dashboard-adapters-container {
width: 100%;
margin-bottom: 1rem;
}
/* 기존 대시보드 그리드 스타일은 비활성화 */
.dashboard-grid {
display: none;
}
/* 반응형 디자인 */
@media (max-width: 1200px) {
.dashboard-row.server-transaction-row {
grid-template-columns: 1fr;
}
}
/* 반응형 디자인 */
@media (max-width: 1200px) {
.dashboard-grid {
grid-template-columns: 1fr; /* 좁은 화면에서는 세로로 배치 */
}
.dashboard-summary {
grid-template-columns: 1fr;
}
.dashboard-row.batch-peak-row {
grid-template-columns: 1fr;
}
}
@media (max-width: 992px) {
header {
flex-direction: column;
align-items: flex-start;
}
.header-controls {
margin-top: 1rem;
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
}
.server-metrics {
flex-direction: column;
gap: 1rem;
}
}
@media (max-width: 768px) {
.transaction-table-container,
.adapter-table-container {
max-height: 300px;
overflow-y: auto;
}
.card-tabs {
overflow-x: auto;
}
}
+16
View File
@@ -0,0 +1,16 @@
/* main.css - 메인 CSS 파일로 모든 모듈을 가져옵니다 */
/* 기본 변수 및 리셋 */
@import 'variables.css';
/* 공통 컴포넌트 */
@import 'components.css';
/* 레이아웃 및 구조 */
@import 'layout.css';
/* 대시보드 섹션별 스타일 */
@import 'sections.css';
/* 유틸리티 및 반응형 */
@import 'utils.css';
@@ -0,0 +1,52 @@
/* sections.css - 대시보드 섹션별 스타일 */
/* 공통 섹션 스타일 */
.dashboard-servers,
.dashboard-transactions,
.dashboard-adapters,
.dashboard-batch,
.dashboard-peak,
.dashboard-sms-summary {
margin-bottom: 0.75rem;
}
.section-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-gray-800);
margin-bottom: 0.5rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--color-gray-200);
}
/* 대시보드 헤더 스타일 - 서버 JSP로 이동됨 */
/* 서버 상태 섹션 - 서버 JSP로 이동됨 */
/* 스파크라인 스타일 - 서버 JSP로 이동됨 */
/* 요약 섹션 - 서버 JSP로 이동됨 */
/* 카드 내부 여백 줄임 */
.card {
padding: 0.6rem;
}
/* 카드 헤더 여백 줄임 */
.card-header {
margin-bottom: 0.5rem;
padding-bottom: 0.4rem;
}
/* 테이블 여백 줄임 */
table th,
table td {
padding: 0.3rem 0.4rem;
}
/* 트랜잭션 현황 섹션 스타일 조정 */
.transaction-overview-container .card {
height: 100%;
}
/* SMS 섹션 스타일 - 개별 JSP 파일로 이동됨 */
+101
View File
@@ -0,0 +1,101 @@
/* utils.css - 유틸리티 클래스 및 반응형 디자인 */
/* 텍스트 정렬 */
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
/* 반응형 디자인 */
@media (max-width: 1200px) {
.dashboard-row {
grid-template-columns: 1fr;
}
.dashboard-summary {
grid-template-columns: 1fr;
}
.batch-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 992px) {
header {
flex-direction: column;
align-items: flex-start;
}
.header-controls {
margin-top: 1rem;
width: 100%;
justify-content: flex-end;
}
.server-metrics {
flex-direction: column;
gap: 1rem;
}
.metric-group {
flex-wrap: wrap;
gap: 1rem;
}
}
@media (max-width: 768px) {
body {
padding: 0.75rem;
}
.server-filters {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.search-container {
width: 100%;
}
.transaction-filters {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
}
@media (max-width: 576px) {
header {
padding: 1rem;
}
.header-title h1 {
font-size: 1.125rem;
}
.header-controls {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.transaction-table th,
.transaction-table td {
padding: 0.5rem;
}
.metric-group {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
@@ -0,0 +1,86 @@
/* variables.css - 모든 CSS 변수와 기본 리셋 스타일 정의 */
/* 변수 정의 */
:root {
/* 기본 색상 */
--color-primary: #4f46e5;
--color-primary-light: #eef2ff;
--color-success: #22c55e;
--color-success-light: #dcfce7;
--color-warning: #f59e0b;
--color-warning-light: #fef3c7;
--color-danger: #ef4444;
--color-danger-light: #fee2e2;
/* 배경 및 텍스트 색상 */
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-200: #e5e7eb;
--color-gray-300: #d1d5db;
--color-gray-400: #9ca3af;
--color-gray-500: #6b7280;
--color-gray-600: #4b5563;
--color-gray-700: #374151;
--color-gray-800: #1f2937;
--color-gray-900: #111827;
/* 테두리 및 그림자 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--rounded-sm: 0.125rem;
--rounded: 0.25rem;
--rounded-md: 0.375rem;
--rounded-lg: 0.5rem;
--rounded-full: 9999px;
}
/* 기본 리셋 및 공통 스타일 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: var(--color-gray-50);
color: var(--color-gray-800);
font-size: 0.875rem;
line-height: 1.5;
padding: 0.6rem 1rem;
}
h1, h2, h3, h4, h5, h6 {
color: var(--color-gray-900);
font-weight: 600;
margin: 0;
}
h1 {
font-size: 1.5rem;
margin-bottom: 1.5rem;
}
p {
margin-bottom: 1.5rem;
color: var(--color-gray-600);
}
.container {
width: 100%;
margin: 0 auto;
}
/* 애니메이션 */
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.4; }
100% { opacity: 1; }
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@@ -0,0 +1,311 @@
/**
* eLink 모니터링 대시보드 API 통신 라이브러리
* ModernDashboardController의 API와 통신하는 함수들을 제공합니다.
*/
const ModernDashboardApi = {
/**
* 컨텍스트 경로
*/
contextPath: '',
/**
* API 초기화 함수
* @param {string} contextPath - 애플리케이션 컨텍스트 경로
*/
init: function(contextPath) {
this.contextPath = contextPath;
},
/**
* API 기본 요청 함수
* @param {string} type - 요청 타입
* @param {Object} params - 추가 파라미터
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
_request: function(type, params, successCallback, errorCallback) {
// 기본 파라미터 설정
const requestParams = {
type: type
, serviceType : sessionStorage['serviceType']
};
// 추가 파라미터 병합
if (params) {
Object.assign(requestParams, params);
}
// AJAX 요청 실행
$.ajax({
url: this.contextPath + '/modern-dashboard-api.do',
data: requestParams,
dataType: 'json',
success: function(data) {
if (successCallback) {
successCallback(data);
}
},
error: function(xhr, status, error) {
console.error(`API 요청 실패 (${type}):`, error);
if (errorCallback) {
errorCallback(xhr, status, error);
}
}
});
},
/**
* 서버 상태 정보 요청
* @param {string} serverName - 서버 이름 (선택적)
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getServerStatus: function(serverName, successCallback, errorCallback) {
const params = {};
if (serverName) {
params.serverName = serverName;
}
this._request('server-status', params, successCallback, errorCallback);
},
/**
* 어댑터 상태 정보 요청
* @param {string} serverName - 서버 이름 (선택적)
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getAdapterStatus: function(serverName, successCallback, errorCallback) {
const params = {};
if (serverName) {
params.serverName = serverName;
}
this._request('adapter-status', params, successCallback, errorCallback);
},
/**
* 트랜잭션 정보 요청
* @param {string} serverName - 서버 이름 (선택적)
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getTransactionInfo: function(serverName, successCallback, errorCallback) {
const params = {};
if (serverName) {
params.serverName = serverName;
}
this._request('transaction', params, successCallback, errorCallback);
},
/**
* 피크 성능 정보 요청
* @param {string} type - 피크 타입 (tps 또는 trade)
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getPeakPerformance: function(type, successCallback, errorCallback) {
const params = {};
if (type) {
params.peakType = type; // type을 peakType으로 수정
}
this._request('peak-performance', params, successCallback, errorCallback);
},
/**
* 배치 처리 상태 요청
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getBatchProcess: function(successCallback, errorCallback) {
$.ajax({
url: this.contextPath + '/modern-dashboard-api.do',
data: {
type: 'batch-process'
, serviceType : sessionStorage['serviceType']
},
dataType: 'json',
success: function(response) {
if (response && response.batchData) {
// BAP 데이터 파싱
const bapData = response.batchData[0];
if (bapData) {
const bapValues = bapData.procBatch.split(',');
bapData.sendSuccess = parseInt(bapValues[0]) || 0;
bapData.receiveSuccess = parseInt(bapValues[1]) || 0;
bapData.sendError = parseInt(bapValues[2]) || 0;
bapData.receiveError = parseInt(bapValues[3]) || 0;
bapData.sendWaiting = parseInt(bapValues[4]) || 0;
bapData.receiveWaiting = parseInt(bapValues[5]) || 0;
}
// BAT 데이터 파싱
const batData = response.batchData[1];
if (batData) {
const batValues = batData.procBatch.split(',');
batData.sendSuccess = parseInt(batValues[0]) || 0;
batData.receiveSuccess = parseInt(batValues[1]) || 0;
batData.sendError = parseInt(batValues[2]) || 0;
batData.receiveError = parseInt(batValues[3]) || 0;
batData.sendWaiting = parseInt(batValues[4]) || 0;
batData.receiveWaiting = parseInt(batValues[5]) || 0;
}
}
if (successCallback) {
successCallback(response);
}
},
error: function(xhr, status, error) {
console.error('배치 처리 상태 조회 실패:', error);
if (errorCallback) {
errorCallback(xhr, status, error);
}
}
});
},
/**
* 인스턴스 상세 정보 요청
* @param {string} serverName - 서버 이름 (선택적)
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getInstanceDetail: function(serverName, successCallback, errorCallback) {
const params = {};
if (serverName) {
params.serverName = serverName;
}
this._request('instance-detail', params, successCallback, errorCallback);
},
/**
* 실시간 성능 데이터 요청
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getPerformanceData: function(successCallback, errorCallback) {
this._request('performance-data', {}, successCallback, errorCallback);
},
/**
* 트랜잭션 시계열 데이터 요청
* @param {string} serverName - 서버 이름 (선택적)
* @param {number} minutes - 가져올 데이터의 시간 범위(분) (선택적, 기본값 10)
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getTransactionTimeSeries: function(serverName, minutes, successCallback, errorCallback) {
const params = {};
if (serverName) {
params.serverName = serverName;
}
if (minutes) {
params.minutes = minutes;
}
this._request('transaction-timeseries', params, successCallback, errorCallback);
},
/**
* 서버 리소스 시계열 데이터 요청
* @param {string} hostname - 호스트명
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getServerResourceData: function(hostname, successCallback, errorCallback) {
const params = {
hostname: hostname
};
this._request('server-resource', params, successCallback, errorCallback);
},
/**
* 소켓 상태 정보 요청
* @param {string} serverName - 서버 이름 (선택적)
* @param {string} adapterGroup - 어댑터 그룹 이름 (선택적)
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getSocketStatus: function(serverName, adapterGroup, successCallback, errorCallback) {
const params = {};
if (serverName) {
params.serverName = serverName;
}
if (adapterGroup) {
params.adapterGroup = adapterGroup;
}
this._request('socket-status', params, successCallback, errorCallback);
},
/**
* 소켓 프로세스 정보 요청
* @param {string} serverName - 서버 이름 (선택적)
* @param {string} adapterGroup - 어댑터 그룹 이름 (선택적)
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getSocketProcess: function(serverName, adapterGroup, successCallback, errorCallback) {
const params = {};
if (serverName) {
params.serverName = serverName;
}
if (adapterGroup) {
params.adapterGroup = adapterGroup;
}
this._request('socket-process', params, successCallback, errorCallback);
},
/**
* 소켓 통합 정보 요청 (상태 + 프로세스)
* @param {string} serverName - 서버 이름 (선택적)
* @param {string} adapterGroup - 어댑터 그룹 이름 (선택적)
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getSocketCombined: function(serverName, adapterGroup, successCallback, errorCallback) {
const params = {};
if (serverName) {
params.serverName = serverName;
}
if (adapterGroup) {
params.adapterGroup = adapterGroup;
}
this._request('socket-combined', params, successCallback, errorCallback);
},
/**
* 인터페이스 로그 조회
* @param {string} interfaceId - 인터페이스 ID
* @param {string} startTime - 조회 시작 시간 (YYYYMMDDHHMMSS 형식)
* @param {string} endTime - 조회 종료 시간 (YYYYMMDDHHMMSS 형식)
* @param {string} serviceType - 서비스 타입
* @param {Function} successCallback - 성공 콜백 함수
* @param {Function} errorCallback - 에러 콜백 함수
*/
getInterfaceLogs: function(interfaceId, startTime, endTime, serviceType, successCallback, errorCallback) {
const params = {
searchEaiSvcName: interfaceId,
searchStartTime: startTime,
searchEndTime: endTime,
searchErrorYn: 'N' // 모든 로그 조회 (Y인 경우 오류만 조회)
};
// 트랜잭션 로그 API 호출
$.ajax({
url: this.contextPath + '/onl/transaction/tracking/trackingMan.json',
type: 'POST',
data: Object.assign({cmd: 'LIST', serviceType: serviceType || 'FEP'}, params),
dataType: 'json',
success: function(data) {
if (successCallback) {
successCallback(data);
}
},
error: function(xhr, status, error) {
console.error(`인터페이스 로그 조회 실패:`, error);
if (errorCallback) {
errorCallback(xhr, status, error);
}
}
});
},
};
@@ -0,0 +1,145 @@
/**
* eLink 모니터링 대시보드 JavaScript 라이브러리
* 모듈화된 대시보드 핵심 기능 제공
*/
// 대시보드 공통 변수
var refreshTimer = null;
var refreshInterval = 10; // 기본값 10초
// 서버 목록 데이터를 저장할 전역 변수
var serverData = [];
// 대시보드 이벤트 관리자
const DashboardEventManager = {
// 이벤트 리스너 저장소
listeners: {
'refresh': [],
'init': [],
'update': [],
'loading-start': [],
'loading-end': []
},
// 이벤트 리스너 등록
on: function(eventName, callback) {
if (this.listeners[eventName]) {
this.listeners[eventName].push(callback);
console.log(`이벤트 리스너 등록: ${eventName}, 현재 ${this.listeners[eventName].length}`);
}
},
// 이벤트 발생
trigger: function(eventName, data) {
console.log(`이벤트 발생: ${eventName}`);
if (this.listeners[eventName]) {
this.listeners[eventName].forEach(callback => {
try {
callback(data);
} catch (e) {
console.error(`이벤트 처리 중 오류 발생: ${eventName}`, e);
}
});
}
}
};
// 전역 스코프에 노출
window.DashboardEventManager = DashboardEventManager;
// 대시보드 초기화
function initDashboard(contextPath) {
console.log('대시보드 초기화 시작');
// API 초기화
ModernDashboardApi.init(contextPath);
// 초기화 이벤트 발생
DashboardEventManager.trigger('init');
// 수동 새로고침 버튼 이벤트 핸들러
$('#refresh-btn').on('click', function() {
console.log('수동 새로고침 버튼 클릭');
refreshDashboardData();
});
console.log('대시보드 초기화 완료');
}
// 현재 시간 업데이트
function updateCurrentTime() {
const now = new Date();
const timeString = now.toLocaleTimeString();
const dateString = now.toLocaleDateString();
$('#current-time').text(`${dateString} ${timeString}`);
}
// 대시보드 데이터 새로고침
function refreshDashboardData() {
console.log("대시보드 데이터 새로고침 시작: " + new Date().toLocaleTimeString());
// 현재 시간 업데이트
updateCurrentTime();
// 로딩 시작 이벤트 발생
DashboardEventManager.trigger('loading-start');
// 새로고침 이벤트 발생
DashboardEventManager.trigger('refresh');
// 약간의 지연 후 로딩 완료 이벤트 발생 (모든 컴포넌트가 데이터를 로드할 시간 제공)
setTimeout(function() {
DashboardEventManager.trigger('loading-end');
}, 2000);
console.log("대시보드 데이터 새로고침 완료: " + new Date().toLocaleTimeString());
}
// 자동 새로고침 타이머 시작
function startRefreshTimer() {
// 기존 타이머가 있으면 중지
if (window.refreshTimer) {
clearInterval(window.refreshTimer);
window.refreshTimer = null;
}
// 새로고침 간격이 0이 아니면 타이머 시작
if (window.refreshInterval > 0) {
window.refreshTimer = setInterval(refreshDashboardData, window.refreshInterval * 1000);
console.log(`자동 새로고침 타이머 시작: ${window.refreshInterval}초 간격`);
}
}
// 새로고침 상태 표시 업데이트
function updateRefreshStatus() {
var statusIndicator = $('.refresh-status');
if (window.refreshInterval > 0) {
statusIndicator.addClass('active');
statusIndicator.removeClass('inactive');
} else {
statusIndicator.removeClass('active');
statusIndicator.addClass('inactive');
}
}
// 문서 로드 완료 시 초기화
$(document).ready(function() {
// 새로고침 간격 버튼 클릭 이벤트
$('.refresh-btn').click(function() {
// 버튼 활성화 상태 변경
$('.refresh-btn').removeClass('active');
$(this).addClass('active');
// 새로고침 간격 설정
window.refreshInterval = parseInt($(this).data('interval'));
// 로컬 스토리지에 저장
localStorage.setItem('dashboardRefreshInterval', window.refreshInterval);
// 타이머 재시작
startRefreshTimer();
// 새로고침 상태 표시 업데이트
updateRefreshStatus();
});
});
File diff suppressed because one or more lines are too long
+15 -1
View File
@@ -401,6 +401,14 @@
w.focus();
}
function goDashboard2() {
// You must open from parent window !
var width = window.screen.availWidth;
var height = window.screen.availHeight;
var w = parent.window.open("<%=request.getContextPath()%>/modern-dashboard.do", "RMSDASHBOARD<%=OPS%>", "width=" + width + ",height=" + height + ",left=0,top=0,scrollbars=no,resizable=yes,status=no");
w.focus();
}
function menuRender() {
<%if(topListSize > 0){%>
// 처음 로딩시 제일첫번째 화면 보여주기
@@ -472,6 +480,12 @@
$('.depth2 a').on("click", function (e) {
$(top.document).find(".topMenu").css("height", "80px");
});
$("#newDashboardShow").on("dblclick", function(){
<% if(((String)SessionManager.getRoleIdString(request)).indexOf("admin") > -1 || ((String)SessionManager.getRoleIdString(request)).indexOf("eai_monitor") > -1) { %>
goDashboard2();
<% } %>
});
});
function logout() {
@@ -535,7 +549,7 @@
</h1><!-- /monitoring/images/top_logo.png -->
<div class="topmenu_box">
<ul>
<li style="width:240px;">
<li style="width:240px;" id="newDashboardShow">
<a style="width:240px;"
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>)</span>
<span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span>
@@ -0,0 +1,415 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*" %>
<%@ page import="com.eactive.eai.rms.common.datasource.*" %>
<div class="dashboard-adapter-summary">
<div class="dashboard-header">
<div class="header-title-container">
<h2 class="section-title">회선 상태</h2>
<%--<div class="adapter-actions">
<button class="filter-btn view-socket-detail active">상세보기</button>
</div>--%>
</div>
</div>
<div class="card adapter-summary-card">
<div class="adapter-summary-container">
<div class="adapter-summary-header">
<div class="adapter-type-column">어댑터 유형</div>
<div class="adapter-status-column">
<div class="status-label error-label">에러</div>
<div class="status-label warning-label">경고</div>
<div class="status-label normal-label">정상</div>
<div class="status-label inactive-label">정지</div>
<div class="status-label total-label">합계</div>
</div>
</div>
<div class="adapter-summary-body">
<!-- 어댑터 요약 데이터가 동적으로 추가됩니다 -->
</div>
</div>
</div>
</div>
<!-- 어댑터 요약 행 템플릿 -->
<template id="adapter-summary-row-template">
<div class="adapter-summary-row">
<div class="adapter-type-column">
<div class="adapter-type-info">
<span class="adapter-type-name">Socket</span>
<span class="status-badge adapter-status-normal">정상</span>
</div>
</div>
<div class="adapter-status-column">
<div class="status-count error-count">0</div>
<div class="status-count warning-count">0</div>
<div class="status-count normal-count">0</div>
<div class="status-count inactive-count">0</div>
<div class="status-count total-count">0</div>
</div>
</div>
</template>
<!-- 소켓 상세 정보 모달 포함 -->
<jsp:include page="dashboard-socket-detail.jsp" />
<style>
/* 어댑터 섹션 */
.adapter-card {
overflow: hidden;
}
.adapter-table-container {
max-height: 300px;
overflow-y: auto;
}
/* 어댑터 탭 스타일 개선 */
.dashboard-adapters .card-tabs {
display: flex;
border-bottom: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
overflow-x: auto;
}
.dashboard-adapters .tab-btn {
padding: 0.75rem 1.25rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-gray-600);
background: none;
border: none;
cursor: pointer;
white-space: nowrap;
border-bottom: 2px solid transparent;
}
.dashboard-adapters .tab-btn:hover {
color: var(--color-primary);
}
.dashboard-adapters .tab-btn.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
background-color: white;
}
.dashboard-adapters .tab-content {
display: none;
padding: 1rem;
}
.dashboard-adapters .tab-content.active {
display: block;
}
/* 어댑터 섹션 스타일 조정 */
.adapters-container .card {
height: 100%;
min-height: 249px;
}
.dashboard-adapter-summary {
margin-bottom: 1rem;
}
.adapter-summary-card {
padding: 0;
overflow: hidden;
}
.adapter-summary-container {
padding: 0.5rem 1rem;
}
.adapter-summary-header {
display: flex;
font-weight: bold;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 0.25rem;
margin-bottom: 0.25rem;
}
.adapter-summary-body {
max-height: 300px;
overflow-y: auto;
}
.adapter-summary-row {
display: flex;
padding: 0.25rem 0;
border-bottom: 1px solid #f0f0f0;
}
.adapter-summary-row:last-child {
border-bottom: none;
}
.adapter-type-column {
flex: 1;
display: flex;
align-items: center;
}
.adapter-type-info {
display: flex;
align-items: center;
}
.adapter-type-name {
font-weight: bold;
margin-right: 0.5rem;
}
.adapter-status-column {
flex: 3;
display: flex;
justify-content: space-between;
align-items: center;
}
.status-label, .status-count {
flex: 1;
text-align: center;
padding: 0.5rem;
}
.status-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-size: 0.7rem;
font-weight: bold;
text-transform: uppercase;
}
.adapter-status-normal {
background-color: rgba(76, 175, 80, 0.15);
color: #4caf50;
}
.adapter-status-warning {
background-color: rgba(255, 152, 0, 0.15);
color: #ff9800;
}
.adapter-status-error {
background-color: rgba(229, 57, 53, 0.15);
color: #e53935;
}
.adapter-status-inactive {
background-color: rgba(158, 158, 158, 0.15);
color: #9e9e9e;
}
.error-label, .error-count {
color: #e53935;
}
.warning-label, .warning-count {
color: #ff9800;
}
.normal-label, .normal-count {
color: #4caf50;
}
.inactive-label, .inactive-count {
color: #9e9e9e;
}
.total-label, .total-count {
font-weight: bold;
}
.empty-data-message {
padding: 1rem;
text-align: center;
color: #757575;
font-style: italic;
}
/* 어댑터 헤더 스타일 */
.adapter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
/* 대시보드 헤더 스타일 */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
/* 제목과 버튼을 한 줄에 나란히 표시하기 위한 컨테이너 */
.header-title-container {
display: flex;
align-items: center;
}
.adapter-actions {
margin-left: 10px; /* 제목과 버튼 사이 간격 */
display: flex;
align-items: center;
}
.filter-btn:hover {
background-color: var(--color-gray-200);
color: var(--color-gray-800);
}
.filter-btn.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0069d9;
opacity: 1 !important;
}
</style>
<script>
// 어댑터 요약 행 생성 함수
function createAdapterSummaryRow(adapterSummary) {
var template = document.getElementById('adapter-summary-row-template');
var clone = document.importNode(template.content, true);
// 어댑터 유형 설정
clone.querySelector('.adapter-type-name').textContent = adapterSummary.adapterType || '—';
// 상태 설정
var statusBadge = clone.querySelector('.status-badge');
statusBadge.className = 'status-badge';
if (adapterSummary.overallStatus === 'normal') {
statusBadge.classList.add('adapter-status-normal');
statusBadge.textContent = '정상';
} else if (adapterSummary.overallStatus === 'warning') {
statusBadge.classList.add('adapter-status-warning');
statusBadge.textContent = '경고';
} else if (adapterSummary.overallStatus === 'error') {
statusBadge.classList.add('adapter-status-error');
statusBadge.textContent = '오류';
} else if (adapterSummary.overallStatus === 'inactive') {
statusBadge.classList.add('adapter-status-inactive');
statusBadge.textContent = '정지';
} else {
statusBadge.classList.add('adapter-status-inactive');
statusBadge.textContent = '알 수 없음';
}
// 상태별 카운트 설정
clone.querySelector('.error-count').textContent = formatNumber(adapterSummary.errorCount || 0);
clone.querySelector('.warning-count').textContent = formatNumber(adapterSummary.warningCount || 0);
clone.querySelector('.normal-count').textContent = formatNumber(adapterSummary.normalCount || 0);
clone.querySelector('.inactive-count').textContent = formatNumber(adapterSummary.inactiveCount || 0);
clone.querySelector('.total-count').textContent = formatNumber(adapterSummary.totalCount || 0);
return clone;
}
// 어댑터 요약 테이블 업데이트 함수
function updateAdapterSummaryTable(data) {
if (!data || !data.serverAdapters) return;
// 어댑터 요약 컨테이너 찾기
var summaryBody = document.querySelector('.adapter-summary-body');
if (!summaryBody) return;
// 컨테이너 초기화
summaryBody.innerHTML = '';
// 모든 서버의 어댑터 요약 데이터 통합
var allAdapterSummaries = [];
data.serverAdapters.forEach(function(serverAdapter) {
if (serverAdapter.adapterSummaries && serverAdapter.adapterSummaries.length > 0) {
allAdapterSummaries = allAdapterSummaries.concat(serverAdapter.adapterSummaries);
}
});
// 어댑터 유형별로 그룹화
var adapterTypeMap = {};
allAdapterSummaries.forEach(function(summary) {
if (!adapterTypeMap[summary.adapterType]) {
adapterTypeMap[summary.adapterType] = summary;
} else {
// 기존 데이터에 카운트 누적
var existing = adapterTypeMap[summary.adapterType];
existing.errorCount = (existing.errorCount || 0) + (summary.errorCount || 0);
existing.warningCount = (existing.warningCount || 0) + (summary.warningCount || 0);
existing.normalCount = (existing.normalCount || 0) + (summary.normalCount || 0);
existing.inactiveCount = (existing.inactiveCount || 0) + (summary.inactiveCount || 0);
existing.totalCount = (existing.totalCount || 0) + (summary.totalCount || 0);
// 전체 상태 업데이트
if (summary.overallStatus === 'error' || existing.overallStatus === 'error') {
existing.overallStatus = 'error';
} else if (summary.overallStatus === 'warning' || existing.overallStatus === 'warning') {
existing.overallStatus = 'warning';
}
}
});
// 필터링된 어댑터 유형만 표시 (Socket, Socket2, HTTP)
var filteredAdapterTypes = Object.keys(adapterTypeMap).filter(function(type) {
return type === 'Socket' || type === 'Socket2' || type === 'HTTP';
});
if (filteredAdapterTypes.length > 0) {
// 어댑터 유형별로 행 추가
filteredAdapterTypes.forEach(function(type) {
var summaryRow = createAdapterSummaryRow(adapterTypeMap[type]);
summaryBody.appendChild(summaryRow);
});
} else {
// 데이터가 없는 경우 안내 메시지 표시
var emptyRow = document.createElement('div');
emptyRow.className = 'empty-data-message';
emptyRow.textContent = '표시할 어댑터 데이터가 없습니다.';
summaryBody.appendChild(emptyRow);
}
}
// 어댑터 요약 데이터 로드 함수
function loadAdapterSummaryData() {
// 어댑터 요약 데이터 API 호출
ModernDashboardApi.getAdapterStatus(null, function(data) {
if (data) {
updateAdapterSummaryTable(data);
}
});
}
// 문서 로드 완료 시 초기화
$(document).ready(function() {
// 초기 데이터 로드
loadAdapterSummaryData();
// 소켓 어댑터 상세 보기 버튼 이벤트
$('.view-socket-detail').on('click', function() {
// 소켓 상세 정보 모달 열기
openSocketDetailModal();
});
// DashboardEventManager를 사용하여 새로고침 이벤트 구독
if (window.DashboardEventManager) {
window.DashboardEventManager.on('refresh', function() {
// 어댑터 요약 데이터 새로고침
loadAdapterSummaryData();
});
}
});
</script>
@@ -0,0 +1,405 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<div class="dashboard-batch">
<h2 class="section-title">배치 처리 상태</h2>
<div class="card">
<div class="batch-grid">
<div class="batch-card" id="bapBatchCard" style="display: none">
<div class="batch-header">
<h4>일괄 전송 처리 (BAP)</h4>
</div>
<div class="batch-content">
<div class="batch-metric">
<div class="batch-metric-header">
<span class="metric-label">송신</span>
<div class="metric-values">
<span class="metric-value" id="bap-send-success">0</span>
<span class="metric-title">성공</span>
</div>
<div class="metric-values">
<span class="metric-value error" id="bap-send-error">0</span>
<span class="metric-title">에러</span>
</div>
<div class="metric-values">
<span class="metric-value warning" id="bap-send-waiting">0</span>
<span class="metric-title">대기</span>
</div>
</div>
<div class="metric-progress">
<div class="progress-bar">
<div class="progress-fill success" id="bap-send-success-bar" style="width: 80%"></div>
<div class="progress-fill danger" id="bap-send-error-bar" style="width: 10%"></div>
<div class="progress-fill warning" id="bap-send-waiting-bar" style="width: 10%"></div>
</div>
<div class="progress-info">
<span class="progress-label">처리율:</span>
<span class="progress-value" id="bap-send-rate">0%</span>
</div>
</div>
</div>
<div class="batch-metric">
<div class="batch-metric-header">
<span class="metric-label">수신</span>
<div class="metric-values">
<span class="metric-value" id="bap-receive-success">0</span>
<span class="metric-title">성공</span>
</div>
<div class="metric-values">
<span class="metric-value error" id="bap-receive-error">0</span>
<span class="metric-title">에러</span>
</div>
<div class="metric-values">
<span class="metric-value warning" id="bap-receive-waiting">0</span>
<span class="metric-title">대기</span>
</div>
</div>
<div class="metric-progress">
<div class="progress-bar">
<div class="progress-fill success" id="bap-receive-success-bar" style="width: 70%"></div>
<div class="progress-fill danger" id="bap-receive-error-bar" style="width: 5%"></div>
<div class="progress-fill warning" id="bap-receive-waiting-bar" style="width: 25%"></div>
</div>
<div class="progress-info">
<span class="progress-label">처리율:</span>
<span class="progress-value" id="bap-receive-rate">0%</span>
</div>
</div>
</div>
</div>
</div>
<div class="batch-card" id="batBatchCard" style="display: none">
<div class="batch-header">
<h4>배치 처리 (BAT)</h4>
</div>
<div class="batch-content">
<div class="batch-metric">
<div class="batch-metric-header">
<span class="metric-label">송신</span>
<div class="metric-values">
<span class="metric-value" id="bat-send-success">0</span>
<span class="metric-title">성공</span>
</div>
<div class="metric-values">
<span class="metric-value error" id="bat-send-error">0</span>
<span class="metric-title">에러</span>
</div>
<div class="metric-values">
<span class="metric-value warning" id="bat-send-waiting">0</span>
<span class="metric-title">대기</span>
</div>
</div>
<div class="metric-progress">
<div class="progress-bar">
<div class="progress-fill success" id="bat-send-success-bar" style="width: 75%"></div>
<div class="progress-fill danger" id="bat-send-error-bar" style="width: 15%"></div>
<div class="progress-fill warning" id="bat-send-waiting-bar" style="width: 10%"></div>
</div>
<div class="progress-info">
<span class="progress-label">처리율:</span>
<span class="progress-value" id="bat-send-rate">0%</span>
</div>
</div>
</div>
<div class="batch-metric">
<div class="batch-metric-header">
<span class="metric-label">수신</span>
<div class="metric-values">
<span class="metric-value" id="bat-receive-success">0</span>
<span class="metric-title">성공</span>
</div>
<div class="metric-values">
<span class="metric-value error" id="bat-receive-error">0</span>
<span class="metric-title">에러</span>
</div>
<div class="metric-values">
<span class="metric-value warning" id="bat-receive-waiting">0</span>
<span class="metric-title">대기</span>
</div>
</div>
<div class="metric-progress">
<div class="progress-bar">
<div class="progress-fill success" id="bat-receive-success-bar" style="width: 65%"></div>
<div class="progress-fill danger" id="bat-receive-error-bar" style="width: 10%"></div>
<div class="progress-fill warning" id="bat-receive-waiting-bar" style="width: 25%"></div>
</div>
<div class="progress-info">
<span class="progress-label">처리율:</span>
<span class="progress-value" id="bat-receive-rate">0%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* 배치 처리 섹션 */
.batch-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.batch-card {
background-color: white;
border-radius: var(--rounded-md);
border: 1px solid var(--color-gray-200);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.batch-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.25rem;
background-color: var(--color-gray-50);
border-bottom: 1px solid var(--color-gray-200);
}
.batch-header h4 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--color-gray-800);
}
.batch-date {
font-size: 0.75rem;
color: var(--color-gray-500);
}
.batch-content {
padding: 0.75rem 1.25rem;
}
.batch-metric {
margin-bottom: 0.75rem;
}
.batch-metric:last-child {
margin-bottom: 0;
}
.batch-metric-header {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
.batch-metric .metric-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-gray-700);
width: 60px;
}
.batch-metric .metric-values {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 1.5rem;
}
.batch-metric .metric-value {
font-size: 1rem;
font-weight: 600;
color: var(--color-gray-800);
}
.batch-metric .metric-value.error {
color: var(--color-danger);
}
.batch-metric .metric-value.warning {
color: var(--color-warning);
}
.batch-metric .metric-title {
font-size: 0.75rem;
color: var(--color-gray-500);
margin-top: 0.25rem;
}
.metric-progress {
margin-top: 0.25rem;
}
.progress-bar {
height: 8px;
background-color: var(--color-gray-200);
border-radius: var(--rounded-full);
display: flex;
overflow: hidden;
}
.progress-fill {
height: 100%;
transition: width 0.3s ease;
}
.progress-fill.success {
background-color: var(--color-success);
}
.progress-fill.danger {
background-color: var(--color-danger);
}
.progress-fill.warning {
background-color: var(--color-warning);
}
.progress-info {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--color-gray-600);
margin-top: 0.5rem;
}
/* 배치 섹션 스타일 조정 */
.batch-container .card {
height: 100%;
}
</style>
<script>
// 숫자 형식화 함수 (천 단위 콤마 추가)
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// 배치 프로세스 업데이트 함수
function updateBatchProcess(data) {
if (!data || !data.batchData) return;
// BAP 배치 데이터 업데이트
var bapData = data.batchData.filter(function(batch) {
return batch.serviceType === 'BAP';
})[0];
if (bapData) {
// 송신 데이터 설정
$('#bap-send-success').text(formatNumber(bapData.sendSuccess));
$('#bap-send-error').text(formatNumber(bapData.sendError));
$('#bap-send-waiting').text(formatNumber(bapData.sendWaiting));
// 총 합계 계산
var sendTotal = bapData.sendSuccess + bapData.sendError + bapData.sendWaiting;
// 바 너비 계산
var sendSuccessPercent = sendTotal > 0 ? (bapData.sendSuccess / sendTotal * 100) : 0;
var sendErrorPercent = sendTotal > 0 ? (bapData.sendError / sendTotal * 100) : 0;
var sendWaitingPercent = sendTotal > 0 ? (bapData.sendWaiting / sendTotal * 100) : 0;
// 바 너비 설정
$('#bap-send-success-bar').css('width', sendSuccessPercent + '%');
$('#bap-send-error-bar').css('width', sendErrorPercent + '%');
$('#bap-send-waiting-bar').css('width', sendWaitingPercent + '%');
// 처리율 설정
var sendCompletedPercent = sendTotal > 0 ?
Math.round((bapData.sendSuccess + bapData.sendError) / sendTotal * 100) : 0;
$('#bap-send-rate').text(sendCompletedPercent + '%');
// 수신 데이터 설정
$('#bap-receive-success').text(formatNumber(bapData.receiveSuccess));
$('#bap-receive-error').text(formatNumber(bapData.receiveError));
$('#bap-receive-waiting').text(formatNumber(bapData.receiveWaiting));
// 총 합계 계산
var receiveTotal = bapData.receiveSuccess + bapData.receiveError + bapData.receiveWaiting;
// 바 너비 계산
var receiveSuccessPercent = receiveTotal > 0 ? (bapData.receiveSuccess / receiveTotal * 100) : 0;
var receiveErrorPercent = receiveTotal > 0 ? (bapData.receiveError / receiveTotal * 100) : 0;
var receiveWaitingPercent = receiveTotal > 0 ? (bapData.receiveWaiting / receiveTotal * 100) : 0;
// 바 너비 설정
$('#bap-receive-success-bar').css('width', receiveSuccessPercent + '%');
$('#bap-receive-error-bar').css('width', receiveErrorPercent + '%');
$('#bap-receive-waiting-bar').css('width', receiveWaitingPercent + '%');
// 처리율 설정
var receiveCompletedPercent = receiveTotal > 0 ?
Math.round((bapData.receiveSuccess + bapData.receiveError) / receiveTotal * 100) : 0;
$('#bap-receive-rate').text(receiveCompletedPercent + '%');
document.getElementById("bapBatchCard").style.display = "block";
}
// BAT 배치 데이터 업데이트
var batData = data.batchData.filter(function(batch) {
return batch.serviceType === 'BAT';
})[0];
if (batData) {
// 송신 데이터 설정
$('#bat-send-success').text(formatNumber(batData.sendSuccess));
$('#bat-send-error').text(formatNumber(batData.sendError));
$('#bat-send-waiting').text(formatNumber(batData.sendWaiting));
// 총 합계 계산
var sendTotal = batData.sendSuccess + batData.sendError + batData.sendWaiting;
// 바 너비 계산
var sendSuccessPercent = sendTotal > 0 ? (batData.sendSuccess / sendTotal * 100) : 0;
var sendErrorPercent = sendTotal > 0 ? (batData.sendError / sendTotal * 100) : 0;
var sendWaitingPercent = sendTotal > 0 ? (batData.sendWaiting / sendTotal * 100) : 0;
// 바 너비 설정
$('#bat-send-success-bar').css('width', sendSuccessPercent + '%');
$('#bat-send-error-bar').css('width', sendErrorPercent + '%');
$('#bat-send-waiting-bar').css('width', sendWaitingPercent + '%');
// 처리율 설정
var sendCompletedPercent = sendTotal > 0 ?
Math.round((batData.sendSuccess + batData.sendError) / sendTotal * 100) : 0;
$('#bat-send-rate').text(sendCompletedPercent + '%');
// 수신 데이터 설정
$('#bat-receive-success').text(formatNumber(batData.receiveSuccess));
$('#bat-receive-error').text(formatNumber(batData.receiveError));
$('#bat-receive-waiting').text(formatNumber(batData.receiveWaiting));
// 총 합계 계산
var receiveTotal = batData.receiveSuccess + batData.receiveError + batData.receiveWaiting;
// 바 너비 계산
var receiveSuccessPercent = receiveTotal > 0 ? (batData.receiveSuccess / receiveTotal * 100) : 0;
var receiveErrorPercent = receiveTotal > 0 ? (batData.receiveError / receiveTotal * 100) : 0;
var receiveWaitingPercent = receiveTotal > 0 ? (batData.receiveWaiting / receiveTotal * 100) : 0;
// 바 너비 설정
$('#bat-receive-success-bar').css('width', receiveSuccessPercent + '%');
$('#bat-receive-error-bar').css('width', receiveErrorPercent + '%');
$('#bat-receive-waiting-bar').css('width', receiveWaitingPercent + '%');
// 처리율 설정
var receiveCompletedPercent = receiveTotal > 0 ?
Math.round((batData.receiveSuccess + batData.receiveError) / receiveTotal * 100) : 0;
$('#bat-receive-rate').text(receiveCompletedPercent + '%');
document.getElementById("batBatchCard").style.display = "block";
}
}
// 배치 처리 데이터 로드 및 업데이트 함수
function loadBatchProcessData() {
ModernDashboardApi.getBatchProcess(function(data) {
updateBatchProcess(data);
});
}
// 초기 로드
$(document).ready(function() {
loadBatchProcessData();
// 대시보드 새로고침 이벤트에 배치 처리 데이터 로드 함수 등록
DashboardEventManager.on('refresh', function() {
loadBatchProcessData();
});
});
</script>
@@ -0,0 +1,9 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<footer>
<div class="footer-info">
<p>© 2025 eLink Monitoring System</p>
</div>
<div class="footer-links">
</div>
</footer>
@@ -0,0 +1,102 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<header>
<div class="header-title">
<i class="bi bi-speedometer2"></i>
<h1>eLink 모니터링 대시보드</h1>
<div class="global-loading-indicator" id="global-loading-indicator"></div>
</div>
<div class="header-controls">
<div class="time-display" id="current-time"></div>
<div class="refresh-controls">
<span class="refresh-status active"></span>
<span class="refresh-label">자동 새로고침:</span>
<div class="btn-group">
<button class="btn refresh-btn btn-sm" data-interval="2">2초</button>
<button class="btn refresh-btn btn-sm" data-interval="5">5초</button>
<button class="btn refresh-btn btn-sm active" data-interval="10">10초</button>
<button class="btn refresh-btn btn-sm" data-interval="0">사용안함</button>
</div>
</div>
<button class="btn btn-sm" id="refresh-btn">
<i class="bi bi-arrow-clockwise"></i> 수동 새로고침
</button>
</div>
</header>
<script>
// 새로고침 간격 버튼 클릭 이벤트
$('.refresh-btn').click(function() {
// 버튼 활성화 상태 변경
$('.refresh-btn').removeClass('active');
$(this).addClass('active');
// 새로고침 간격 설정
refreshInterval = parseInt($(this).data('interval'));
// 로컬 스토리지에 저장
localStorage.setItem('dashboardRefreshInterval', refreshInterval);
// 타이머 재시작
startRefreshTimer();
// 새로고침 상태 표시 업데이트
updateRefreshStatus();
});
// 수동 새로고침 버튼 클릭 이벤트
$('#refresh-btn').click(function() {
refreshDashboardData();
});
// 전역 로딩 인디케이터 제어 함수
function showGlobalLoadingIndicator() {
$('#global-loading-indicator').addClass('active');
}
function hideGlobalLoadingIndicator() {
$('#global-loading-indicator').removeClass('active');
}
// 문서 로드 완료 시 이벤트 리스너 등록
$(document).ready(function() {
// DashboardEventManager 이벤트 구독
if (window.DashboardEventManager) {
// 데이터 로딩 시작 이벤트
window.DashboardEventManager.on('loading-start', function() {
showGlobalLoadingIndicator();
});
// 데이터 로딩 완료 이벤트
window.DashboardEventManager.on('loading-end', function() {
hideGlobalLoadingIndicator();
});
}
});
</script>
<style>
/* 전역 로딩 인디케이터 스타일 */
.global-loading-indicator {
display: inline-block;
width: 18px;
height: 18px;
margin-left: 10px;
border-radius: 50%;
border: 2px solid rgba(79, 70, 229, 0.2);
border-top-color: var(--color-primary);
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease;
}
.global-loading-indicator.active {
opacity: 1;
visibility: visible;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
@@ -0,0 +1,680 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<!-- 인터페이스 로그 조회 모달 -->
<div id="interfaceLogDetailModal" class="modal modal-modern">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">인터페이스 로그 조회</h2>
<span class="close-modal">&times;</span>
</div>
<div class="modal-body">
<!-- 인터페이스 정보 요약 -->
<div class="interface-info-summary">
<div class="info-card">
<div class="info-header">
<span class="info-label">인터페이스 ID</span>
<span class="info-value" id="detail-interface-id">-</span>
</div>
<div class="info-content">
<div class="info-row">
<span class="info-label">발생시간</span>
<span class="info-value" id="detail-error-time">-</span>
</div>
<div class="info-row">
<span class="info-label">인스턴스</span>
<span class="info-value" id="detail-instance-name">-</span>
</div>
</div>
</div>
<div class="info-card">
<div class="info-header">
<span class="info-label">오류 메시지</span>
</div>
<div class="info-content">
<div class="error-message" id="detail-error-message">-</div>
</div>
</div>
</div>
<!-- 로그 리스트 -->
<div class="log-list-container">
<div class="log-list-header">
<h3>관련 거래 로그</h3>
<div class="log-search-controls">
<div class="time-range-selector">
<span>조회 시간:</span>
<select id="log-time-range">
<option value="1" selected>±1분</option>
<option value="5">±5분</option>
<option value="10">±10분</option>
<option value="30">±30분</option>
</select>
</div>
<button class="btn btn-primary refresh-logs-btn">
<i class="bi bi-arrow-clockwise"></i> 새로고침
</button>
</div>
</div>
<div class="log-table-container">
<table class="log-table" id="log-grid">
<thead>
<tr>
<th class="narrow-column">처리번호</th>
<th>거래키</th>
<th>업무구분</th>
<th>서비스명</th>
<th>수신시간</th>
<th>처리시간</th>
<th>상태</th>
</tr>
</thead>
<tbody id="log-grid-body">
<!-- 로그 데이터가 동적으로 추가됩니다 -->
<tr>
<td colspan="7" class="empty-message">조회 버튼을 클릭하여 관련 로그를 검색하세요.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary close-modal-btn">닫기</button>
</div>
</div>
</div>
<style>
/* 모달 기본 스타일 */
.modal-modern {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.modal-modern .modal-content {
position: relative;
background-color: #fff;
margin: 2% auto;
padding: 0;
width: 95%;
max-width: 1200px;
border: none;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
animation: modalSlideUp 0.3s;
}
@keyframes modalSlideUp {
from {opacity: 0; transform: translateY(30px);}
to {opacity: 1; transform: translateY(0);}
}
.modal-modern .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e0e0e0;
background-color: #f8f9fa;
border-radius: 16px 16px 0 0;
}
.modal-modern .modal-title {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #333;
}
.modal-modern .close-modal {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.05);
}
.modal-modern .close-modal:hover {
color: #333;
background-color: rgba(0, 0, 0, 0.1);
}
.modal-modern .modal-body {
padding: 1.5rem 2rem;
max-height: 75vh;
overflow-y: auto;
background-color: #f8f9fa;
}
.modal-modern .modal-footer {
padding: 1.2rem 2rem;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: flex-end;
gap: 1rem;
border-radius: 0 0 16px 16px;
background-color: #fff;
}
/* 인터페이스 정보 요약 스타일 */
.interface-info-summary {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.info-card {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.info-header {
padding: 1rem;
background-color: #007bff;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.info-header .info-label {
font-weight: 600;
font-size: 1rem;
}
.info-header .info-value {
font-weight: 600;
font-size: 1.1rem;
}
.info-content {
padding: 1rem;
}
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-row .info-label {
color: #666;
font-size: 0.9rem;
}
.info-row .info-value {
font-weight: 600;
color: #333;
}
.error-message {
padding: 0.5rem;
background-color: rgba(220, 53, 69, 0.1);
border-radius: 6px;
color: #dc3545;
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
max-height: 100px;
overflow-y: auto;
}
/* 로그 리스트 스타일 */
.log-list-container {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.log-list-header {
padding: 1rem;
background-color: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.log-list-header h3 {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
color: #333;
}
.log-search-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.time-range-selector {
display: flex;
align-items: center;
gap: 0.5rem;
}
.time-range-selector select {
padding: 0.25rem 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
}
.log-table-container {
max-height: 400px;
overflow-y: auto;
}
.log-table {
width: 100%;
border-collapse: collapse;
}
.log-table th {
top: 0;
background-color: #007bff;
color: white;
padding: 0.75rem;
text-align: left;
font-weight: 800;
z-index: 10;
}
.log-table td {
padding: 0.75rem;
border-bottom: 1px solid #e0e0e0;
}
.log-table tbody tr:hover {
background-color: rgba(0, 0, 0, 0.03);
}
.log-table .empty-message {
text-align: center;
padding: 2rem;
color: #888;
font-style: italic;
}
/* 로딩 인디케이터 */
.loading-indicator {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid #007bff;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 버튼 스타일 */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0069d9;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
/* 오류 상태 표시 */
.table-status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
text-align: center;
min-width: 60px;
}
.table-status-error {
background-color: #dc3545;
color: white;
}
.table-status-normal {
background-color: #28a745;
color: white;
}
.log-table th.narrow-column {
width: 80px;
}
.highlight-row {
background-color: rgba(0, 123, 255, 0.1);
}
/* 반응형 설정 */
@media (max-width: 992px) {
.interface-info-summary {
grid-template-columns: 1fr;
}
.modal-modern .modal-content {
width: 98%;
margin: 1% auto;
}
}
</style>
<script>
(function() {
// 모달 요소
const modal = document.getElementById('interfaceLogDetailModal');
const closeButtons = modal.querySelectorAll('.close-modal, .close-modal-btn');
const refreshButton = modal.querySelector('.refresh-logs-btn');
const timeRangeSelect = document.getElementById('log-time-range');
// 데이터 상태
let currentInterfaceId = '';
let currentErrorTime = '';
let currentInstanceName = '';
let currentErrorMessage = '';
let currentServiceType = '';
// 모달 닫기 이벤트
closeButtons.forEach(button => {
button.addEventListener('click', () => {
modal.style.display = 'none';
});
});
// 모달 외부 클릭 시 닫기
window.addEventListener('click', (event) => {
if (event.target === modal) {
modal.style.display = 'none';
}
});
// 새로고침 버튼 이벤트
refreshButton.addEventListener('click', () => {
loadInterfaceLogs();
});
// 로그 시간대 변경 이벤트
timeRangeSelect.addEventListener('change', () => {
loadInterfaceLogs();
});
// 인터페이스 로그 로드 함수
function loadInterfaceLogs() {
if (!currentInterfaceId || !currentErrorTime) {
showEmptyLogGrid("인터페이스 정보가 올바르지 않습니다.");
return;
}
// 로딩 표시
showLoadingLogGrid();
// 시간 범위 계산
const timeRangeMinutes = parseInt(timeRangeSelect.value);
const errorDate = parseErrorDateTime(currentErrorTime);
if (!errorDate) {
showEmptyLogGrid("시간 형식이 올바르지 않습니다.");
return;
}
// 시작 시간과 종료 시간 계산 (±timeRangeMinutes 분)
const startTime = new Date(errorDate.getTime() - timeRangeMinutes * 60 * 1000);
const endTime = new Date(errorDate.getTime() + timeRangeMinutes * 60 * 1000);
// API 호출 시간 형식으로 변환 (YYYYMMDDHHMMSS)
const startTimeStr = formatDateTimeForApi(startTime);
const endTimeStr = formatDateTimeForApi(endTime);
// ModernDashboardApi를 사용하여 로그 데이터 가져오기
ModernDashboardApi.getInterfaceLogs(
currentInterfaceId,
startTimeStr,
endTimeStr,
currentServiceType,
function(response) {
if (response && response.rows) {
updateLogGrid(response.rows);
} else {
showEmptyLogGrid("조회된 로그가 없습니다.");
}
},
function(xhr, status, error) {
console.error("로그 데이터 로드 실패:", error);
showEmptyLogGrid("로그 데이터를 불러오는 중 오류가 발생했습니다.");
}
);
}
// 로그 그리드 업데이트
function updateLogGrid(logs) {
const tbody = document.getElementById('log-grid-body');
if (!logs || logs.length === 0) {
showEmptyLogGrid("조회된 로그가 없습니다.");
return;
}
let html = '';
logs.forEach(log => {
const isError = log.ERRYN === 'Y';
// 시간 형식 변환 (YYYYMMDDHHMMSS -> YYYY-MM-DD HH:MM:SS)
const recvTime = formatDateTime(log.MSGDPSTYMS_TMP);
const procTime = formatDateTime(log.MSGPRCSSYMS);
html += '<tr>' +
'<td>' + (log.LOGPRCSSSERNO || '-') + '</td>' +
'<td>' + (log.KEYMGTMSGCTNT || '-') + '</td>' +
'<td>' + (log.EAIBZWKDSTCD_TMP || '-') + '</td>' +
'<td>' + (log.EAISVCNAME || '-') + '</td>' +
'<td>' + (recvTime || '-') + '</td>' +
'<td>' + (procTime || '-') + '</td>' +
'<td><span class="table-status-badge ' + (isError ? 'table-status-error' : 'table-status-normal') + '">' +
(isError ? '오류' : '정상') + '</span>' +
'<input type="hidden" value="' + (log.EAISVCSERNO_TMP || '') + '"></td>' +
'</tr>';
});
tbody.innerHTML = html;
// 현재 인터페이스 ID를 포함한 행 강조 표시
highlightRows(currentInterfaceId);
}
// 특정 인터페이스 ID를 포함한 행 강조 표시
function highlightRows(interfaceId) {
if (!interfaceId) return;
const tbody = document.getElementById('log-grid-body');
const rows = tbody.querySelectorAll('tr');
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length > 1) {
const keyCell = cells[1].textContent;
if (keyCell && keyCell.includes(interfaceId)) {
row.classList.add('highlight-row');
}
}
});
}
// 로딩 중 표시
function showLoadingLogGrid() {
const tbody = document.getElementById('log-grid-body');
tbody.innerHTML =
'<tr>' +
'<td colspan="7">' +
'<div class="loading-indicator">' +
'<div class="loading-spinner"></div>' +
'</div>' +
'</td>' +
'</tr>';
}
// 빈 그리드 표시
function showEmptyLogGrid(message) {
const tbody = document.getElementById('log-grid-body');
tbody.innerHTML =
'<tr>' +
'<td colspan="7" class="empty-message">' + (message || '조회된 데이터가 없습니다.') + '</td>' +
'</tr>';
}
// 시간 문자열 파싱 (예: "0312 10:52:32.009")
function parseErrorDateTime(dateTimeStr) {
try {
if (!dateTimeStr) return new Date();
// "0312 10:52:32.009" 형식 처리
if (dateTimeStr.includes(' ') && dateTimeStr.length >= 16) {
const datePart = dateTimeStr.split(' ')[0]; // "0312"
const timePart = dateTimeStr.split(' ')[1].split('.')[0]; // "10:52:32"
const month = parseInt(datePart.substring(0, 2));
const day = parseInt(datePart.substring(2, 4));
const timeParts = timePart.split(':');
const hour = parseInt(timeParts[0]);
const minute = parseInt(timeParts[1]);
const second = parseInt(timeParts[2]);
// 현재 연도 사용
const currentYear = new Date().getFullYear();
return new Date(currentYear, month - 1, day, hour, minute, second);
}
// 기존 "2023-03-12 15:30:45" 형식 처리 (이전 코드 유지)
const parts = dateTimeStr.split(/[- :]/);
if (parts.length >= 6) {
return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]);
}
return new Date();
} catch (e) {
console.error("날짜 파싱 오류:", e);
return new Date();
}
}
// API 호출용 날짜 형식 변환 (YYYYMMDDHHMMSS)
function formatDateTimeForApi(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return '' + year + month + day + hours + minutes + seconds;
}
// 화면 표시용 날짜 형식 변환 (YYYY-MM-DD HH:MM:SS)
function formatDateTime(dateTimeStr) {
if (!dateTimeStr || dateTimeStr.length < 14) return '';
const year = dateTimeStr.substr(0, 4);
const month = dateTimeStr.substr(4, 2);
const day = dateTimeStr.substr(6, 2);
const hour = dateTimeStr.substr(8, 2);
const minute = dateTimeStr.substr(10, 2);
const second = dateTimeStr.substr(12, 2);
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
}
// 모달 열기 함수 (전역 스코프에 노출)
window.openInterfaceLogDetailModal = function(interfaceId, errorTime, instanceName, errorMessage, serviceType) {
// 데이터 설정
currentInterfaceId = interfaceId || '';
currentErrorTime = errorTime || '';
currentInstanceName = instanceName || '';
currentErrorMessage = errorMessage || '';
currentServiceType = serviceType || '';
// 조회 기준 일/시 분리해서 표시
let formattedErrorTime = currentErrorTime;
if (currentErrorTime && currentErrorTime.includes(' ')) {
const datePart = currentErrorTime.split(' ')[0]; // "0312"
const timePart = currentErrorTime.split(' ')[1]; // "10:52:32.009"
// 월/일 형식으로 변환
const month = datePart.substring(0, 2);
const day = datePart.substring(2, 4);
formattedErrorTime = month + '월 ' + day + '일 ' + timePart;
}
// UI 업데이트
document.getElementById('detail-interface-id').textContent = currentInterfaceId;
document.getElementById('detail-error-time').textContent = formattedErrorTime;
document.getElementById('detail-instance-name').textContent = currentInstanceName;
document.getElementById('detail-error-message').textContent = currentErrorMessage;
// 모달 표시
modal.style.display = 'block';
// 로그 데이터 로드
loadInterfaceLogs();
};
})();
</script>
@@ -0,0 +1,243 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<div class="dashboard-peak">
<h2 class="section-title">피크 처리 성능
<button class="peak-filter-btn active" id="peakTps">tps</button>
<button class="peak-filter-btn" id="peakTrade">trade</button>
</h2>
<div class="card">
<div class="card-title">
</div>
<div class="peak-grid" id="peak-tps-container">
<!-- 피크 성능 데이터가 동적으로 추가됩니다 -->
</div>
<div class="peak-grid" id="peak-trade-container" style="display:none">
<!-- 피크 성능 데이터가 동적으로 추가됩니다 -->
</div>
</div>
</div>
<!-- 피크 성능 카드 템플릿 -->
<template id="peak-card-template">
<div class="peak-card">
<div class="peak-header">
<div class="peak-header-content">
<h4 class="peak-service-type">서비스 유형</h4>
<span class="peak-tps">tps</span>
<span class="peak-date">날짜</span>
</div>
</div>
</div>
</template>
<style>
.dashboard-peak {
position: relative;
}
/* FEP와 BAP를 상하로 배치하기 위한 스타일 */
.peak-grid {
display: flex;
flex-direction: column;
gap: 0.3rem;
height: 263px; /* 배치 처리 성능 높이와 일관되게 설정 */
overflow-y: auto; /* 내용이 많을 경우 스크롤 가능하도록 */
}
.peak-card {
margin-bottom: 0.3rem;
height: auto; /* 자동 높이 설정 */
background-color: white;
border-radius: var(--rounded-md);
border: 1px solid var(--color-gray-200);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.peak-card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
/* 좁은 공간에 맞게 조정 */
.peak-header {
margin-bottom: 0.1rem;
padding: 0.3rem 0.5rem; /* 패딩 더 축소 */
border-bottom: 1px solid var(--color-gray-200);
}
.peak-header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.peak-service-type {
font-size: 0.85rem;
margin: 0;
font-weight: 600;
color: var(--color-gray-800);
}
.peak-date {
font-size: 0.75rem;
color: var(--color-gray-500);
}
.peak-content {
padding: 0.3rem 0.5rem; /* 패딩 더 축소 */
display: flex;
align-items: center;
}
.peak-value-container {
flex: 1;
}
.peak-value {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-gray-900);
}
.peak-label {
font-size: 0.75rem;
color: var(--color-gray-500);
}
/* 차트 높이 조정 */
.peak-chart {
height: 40px; /* 차트 높이 더 축소 */
flex: 1;
display: flex;
justify-content: center;
align-items: flex-end;
}
.peak-bar-container {
height: 100%;
width: 40px;
display: flex;
justify-content: center;
align-items: flex-end;
}
.peak-bar {
width: 30px;
background-color: var(--color-primary);
border-radius: 3px 3px 0 0;
}
/* 피크 섹션 스타일 조정 */
.peak-container .card {
height: 100%;
}
.peak-tps {
font-size: 1rem;
margin-left: auto;
margin-right: 8px;
font-weight: 600;
color: var(--color-gray-900);
display: flex;
align-items: end;
}
</style>
<script>
// 숫자 형식화 함수 (천 단위 콤마 추가)
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// 피크 성능 데이터 업데이트 함수
function updatePeakPerformance(data) {
if (!data || !data.peakTps || !data.peakTrade) return;
var container = document.getElementById('peak-tps-container');
container.innerHTML = '';
// 최대값 찾기 (스케일링을 위해)
var maxValue = 0;
data.peakTps.forEach(function(peak) {
var value = peak.tps;
if (value > maxValue) maxValue = value;
});
// 피크 카드 생성
data.peakTps.forEach(function(peak) {
var template = document.getElementById('peak-card-template');
var clone = document.importNode(template.content, true);
// 서비스 유형 설정
clone.querySelector('.peak-service-type').textContent = peak.serviceType;
// tps 설정
clone.querySelector('.peak-tps').textContent = (0 || peak.tps);
// 날짜 설정
clone.querySelector('.peak-date').textContent = peak.formattedDate || peak.date;
container.appendChild(clone);
});
var tradeContainer = document.getElementById('peak-trade-container');
tradeContainer.innerHTML = '';
// 피크 카드 생성
data.peakTrade.forEach(function(peak) {
var template = document.getElementById('peak-card-template');
var clone = document.importNode(template.content, true);
// 서비스 유형 설정
clone.querySelector('.peak-service-type').textContent = peak.serviceType;
// tps 설정
clone.querySelector('.peak-tps').textContent = (0 || peak.tps);
// 날짜 설정
clone.querySelector('.peak-date').textContent = peak.formattedDate || peak.date;
tradeContainer.appendChild(clone);
});
}
// 피크 성능 데이터 로드 함수
function loadPeakPerformanceData() {
// 항상 TPS 데이터 로드 (선택 버튼 제거)
ModernDashboardApi.getPeakPerformance('tps', function(data) {
updatePeakPerformance(data);
});
}
// 서버 필터 버튼 클릭 이벤트
$('.peak-filter-btn').click(function() {
var type = $(this).prop("id");
if (type === 'peakTps') {
// 전체 표시
$("#peakTrade").removeClass('active');
$("#peakTps").addClass('active');
$("#peak-tps-container").show();
$("#peak-trade-container").hide();
} else if( type === 'peakTrade' ){
// 필터링
$("#peakTps").removeClass('active');
$("#peakTrade").addClass('active');
$("#peak-tps-container").hide();
$("#peak-trade-container").show();
}
});
// 초기 로드
$(document).ready(function() {
loadPeakPerformanceData();
});
</script>
@@ -0,0 +1,934 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*" %>
<%@ page import="com.eactive.eai.rms.common.datasource.*" %>
<%@ page import="java.util.stream.Collectors" %>
<div class="dashboard-servers">
<div class="dashboard-header">
<h2 class="section-title">서버 상태</h2>
<div class="server-filters">
<div class="filter-group">
<button class="filter-btn active" data-type="all">전체</button>
<%
// serverTypeMap 변수를 가져오기
Map<String, List<DataSourceType>> serverTypeMap =
(Map<String, List<DataSourceType>>)request.getAttribute("serverTypeMap");
if (serverTypeMap == null) {
// application 스코프에서 가져오기 시도
serverTypeMap = (Map<String, List<DataSourceType>>)application.getAttribute("serverTypeMap");
}
Map<String, Integer> priorityMap = new HashMap<>();
priorityMap.put("MCI", 0);
priorityMap.put("EAI", 1);
priorityMap.put("FEP", 2);
priorityMap.put("CGW", 3);
priorityMap.put("FGW", 4);
priorityMap.put("API", 5);
priorityMap.put("APIGW", 6);
priorityMap.put("ONL", 7);
Map<String,List<DataSourceType>> sortedMap = serverTypeMap.entrySet().stream()
.sorted(Comparator.comparingInt(e -> priorityMap.getOrDefault(e.getKey(), Integer.MAX_VALUE)))
.collect(Collectors.toMap(
Map.Entry::getKey
, Map.Entry::getValue
,(e1, e2) -> e1
, LinkedHashMap::new
));
if (sortedMap != null) {
// 서버 타입 필터 생성
for (Map.Entry<String, List<DataSourceType>> entry : sortedMap.entrySet()) {
String serverType = entry.getKey();
List<DataSourceType> servers = entry.getValue();
// BAT 서버는 제외
if (DataSourceTypeManager.BAT.equals(serverType) || DataSourceTypeManager.BAP.equals(serverType)) {
continue;
}
// 서버 타입에 해당하는 텍스트 가져오기 (첫 번째 서버의 텍스트 사용)
String serverTypeText = servers.size() > 0 ? servers.get(0).getText() : serverType;
%>
<button class="filter-btn" data-type="<%= serverType.toLowerCase() %>"><%= serverTypeText %></button>
<% }
} %>
</div>
<div class="search-container">
<i class="bi bi-search"></i>
<input type="text" id="server-search" placeholder="서버 검색..." />
</div>
</div>
</div>
<%
if (sortedMap != null) {
// 서버 타입별로 섹션 생성
for (Map.Entry<String, List<DataSourceType>> entry : sortedMap.entrySet()) {
String serverType = entry.getKey();
List<DataSourceType> servers = entry.getValue();
// BAT 서버는 제외
if (DataSourceTypeManager.BAT.equals(serverType) || DataSourceTypeManager.BAP.equals(serverType)) {
continue;
}
// 서버 타입에 해당하는 텍스트 가져오기 (첫 번째 서버의 텍스트 사용)
String serverTypeText = servers.size() > 0 ? servers.get(0).getText() : serverType;
// 서버 타입별 보더 색상 클래스
String borderColorClass = "";
if (serverType.equalsIgnoreCase("FEP")) {
borderColorClass = "border-primary";
} else if (serverType.equalsIgnoreCase("EAI")) {
borderColorClass = "border-success";
} else if (serverType.equalsIgnoreCase("EMS")) {
borderColorClass = "border-purple";
} else if (serverType.equalsIgnoreCase("EDI")) {
borderColorClass = "border-orange";
}
%>
<div class="server-section" id="server-section-<%= serverType.toLowerCase() %>">
<div class="card server-card-container <%= borderColorClass %>">
<div class="server-section-header">
<h3 class="server-section-title"><%= serverTypeText %> 서버</h3>
<span class="server-status-badge badge-success <%= serverType.toLowerCase() %>-status-badge">0 / 0 정상</span>
</div>
<div class="server-grid server-grid-<%= serverType.toLowerCase() %>" id="<%= serverType.toLowerCase() %>-servers-container">
<!-- 여기에 <%= serverTypeText %> 서버 카드가 동적으로 추가됩니다 -->
</div>
</div>
</div>
<% }
} %>
</div>
<!-- 서버 카드 템플릿 -->
<template id="server-card-template">
<div class="server-card">
<div class="server-header">
<h5 class="server-name">서버 이름</h5>
<span class="server-status-badge badge-success">정상</span>
</div>
<div class="server-metrics">
<!-- 메트릭 헤더 행 -->
<div class="metrics-row">
<!-- CPU 메트릭 -->
<div class="server-metric">
<div class="metric-header">
<span class="metric-name"><i class="bi bi-cpu" style="color: #0dcaf0; font-size: 1.1rem;"></i></span>
<span class="metric-value cpu-value">0%</span>
</div>
</div>
<!-- 메모리 메트릭 -->
<div class="server-metric">
<div class="metric-header">
<span class="metric-name"><i class="bi bi-memory" style="color: #6f42c1; font-size: 1.1rem;"></i></span>
<span class="metric-value memory-value">0%</span>
</div>
</div>
<!-- 디스크 메트릭 -->
<div class="server-metric">
<div class="metric-header">
<span class="metric-name"><i class="bi bi-hdd" style="color: #fd7e14; font-size: 1.1rem;"></i></span>
<span class="metric-value disk-value">0%</span>
</div>
</div>
</div>
<!-- 스파크라인 행 -->
<div class="sparklines-row">
<div class="sparkline cpu-sparkline">
<div class="sparkline-bar success" style="height: 10%"></div>
<div class="sparkline-bar success" style="height: 15%"></div>
<div class="sparkline-bar success" style="height: 20%"></div>
<div class="sparkline-bar success" style="height: 15%"></div>
<div class="sparkline-bar success" style="height: 25%"></div>
<div class="sparkline-bar success" style="height: 20%"></div>
<div class="sparkline-bar success" style="height: 15%"></div>
</div>
<div class="sparkline memory-sparkline">
<div class="sparkline-bar success" style="height: 20%"></div>
<div class="sparkline-bar success" style="height: 25%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 25%"></div>
<div class="sparkline-bar success" style="height: 35%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 25%"></div>
</div>
<div class="sparkline disk-sparkline">
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
<div class="sparkline-bar success" style="height: 30%"></div>
</div>
</div>
</div>
<!-- 인스턴스 테이블 -->
<div class="server-instances">
<table class="instance-table">
<thead>
<tr>
<th>인스턴스 ID</th>
<th>상태</th>
<th class="text-right">처리량</th>
</tr>
</thead>
<tbody class="instance-table-body">
<!-- 인스턴스 데이터가 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
</div>
</template>
<!-- 인스턴스 행 템플릿 -->
<template id="instance-row-template">
<tr>
<td>
<span class="instance-status status-normal"></span>
<span class="instance-id">인스턴스 ID</span>
</td>
<td class="instance-status-text">정상</td>
<td class="text-right instance-throughput">0/s</td>
</tr>
</template>
<style>
/* 대시보드 헤더 스타일 */
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--color-gray-200);
}
.dashboard-header .section-title {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
flex-shrink: 0;
margin-right: 0.75rem;
}
.dashboard-header .server-filters {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0;
flex-grow: 1;
}
.dashboard-header .filter-group {
flex-shrink: 0;
}
.dashboard-header .search-container {
margin-left: auto;
}
/* 서버 상태 섹션 */
.server-section {
margin-bottom: 0.75rem;
}
.server-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.server-section-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--color-gray-800);
margin: 0;
}
.server-filters {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
gap: 0.5rem;
}
.filter-group {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.server-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 0.5rem;
}
.server-card {
background-color: white;
border-radius: var(--rounded-md);
box-shadow: var(--shadow);
border: 1px solid var(--color-gray-200);
overflow: hidden;
transition: all 0.2s ease;
height: 100%;
padding: 0.75rem;
}
.server-card:hover {
box-shadow: var(--shadow-md);
}
.server-header {
padding: 1rem 1.25rem;
background-color: var(--color-gray-50);
border-bottom: 1px solid var(--color-gray-200);
display: flex;
align-items: center;
justify-content: space-between;
}
.server-name {
font-weight: 600;
font-size: 1rem;
color: var(--color-gray-800);
margin: 0;
}
.server-metrics {
padding: 1rem 1.25rem;
display: flex;
gap: 1.5rem;
}
.server-metric {
flex: 1;
}
.metric-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.metric-name {
font-size: 0.75rem;
color: var(--color-gray-500);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.server-instances {
padding: 0 16px 16px 16px;
flex: 1;
overflow-y: auto;
max-height: 150px;
}
/* 스파크라인 스타일 */
.sparkline {
height: 28px;
display: flex;
align-items: flex-end;
gap: 2px;
margin-top: 0.5rem;
background-color: var(--color-gray-50);
border-radius: var(--rounded-sm);
padding: 0.25rem;
}
.sparkline-bar {
flex: 1;
background-color: var(--color-primary);
border-radius: 1px 1px 0 0;
}
.sparkline-bar.success {
background-color: var(--color-success);
}
.sparkline-bar.warning {
background-color: var(--color-warning);
}
.sparkline-bar.danger {
background-color: var(--color-danger);
}
/* 요약 섹션 */
.status-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.status-card {
display: flex;
align-items: center;
padding: 1.5rem;
border-radius: var(--rounded-lg);
background-color: white;
box-shadow: var(--shadow);
transition: all 0.2s ease;
}
.status-card:hover {
box-shadow: var(--shadow-md);
}
.status-card.success {
border-left: 4px solid var(--color-success);
}
.status-card.warning {
border-left: 4px solid var(--color-warning);
}
.status-card.danger {
border-left: 4px solid var(--color-danger);
}
.status-icon {
font-size: 2rem;
margin-right: 1.25rem;
display: flex;
align-items: center;
}
.status-icon.success {
color: var(--color-success);
}
.status-icon.warning {
color: var(--color-warning);
}
.status-icon.danger {
color: var(--color-danger);
}
.status-content {
display: flex;
flex-direction: column;
}
.status-label {
font-size: 0.875rem;
color: var(--color-gray-500);
margin-bottom: 0.25rem;
}
.status-value {
font-size: 1.5rem;
font-weight: 600;
color: var(--color-gray-900);
}
/* 서버 섹션 스타일 - 기존 스타일 유지 */
.server-section {
margin-bottom: 1rem;
}
.server-card-container {
padding: 0.75rem;
margin-bottom: 1rem;
min-height: 296px;
}
.server-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.server-section-title {
font-size: 1rem;
font-weight: 600;
color: var(--color-gray-800);
margin: 0;
}
/* 서버 타입별 보더 색상 */
.border-primary {
border-top: 2px solid #0d6efd;
}
.border-success {
border-top: 2px solid #198754;
}
.border-purple {
border-top: 2px solid #6f42c1;
}
.border-orange {
border-top: 2px solid #fd7e14;
}
/* 서버 그리드 - 한 줄에 3개씩 표시 */
.server-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
/* 반응형 처리 - 화면 크기에 따라 그리드 조정 */
@media screen and (max-width: 1200px) {
.server-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (max-width: 768px) {
.server-grid {
grid-template-columns: 1fr;
}
}
/* 서버 카드 높이 축소 */
.server-card {
padding: 0.5rem;
border: 1px solid var(--color-gray-200);
border-radius: var(--rounded-sm);
background-color: white;
}
.server-header {
padding: 0.4rem 0.5rem;
margin-bottom: 0.3rem;
border-bottom: 1px solid var(--color-gray-100);
}
.server-name {
font-size: 0.95rem;
}
/* 메트릭 스타일 조정 */
.server-metrics {
padding: 0.3rem 0;
display: flex;
flex-direction: column;
gap: 0.0rem;
}
.metrics-row {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
.sparklines-row {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
.server-metric {
flex: 1;
min-width: 0; /* 오버플로우 방지 */
}
.metric-header {
margin-bottom: 0.2rem;
display: flex;
justify-content: flex-start;
align-items: center;
white-space: nowrap;
overflow: hidden;
}
.sparkline {
flex: 1;
min-width: 0; /* 오버플로우 방지 */
}
.metric-name {
display: flex;
align-items: center;
}
.metric-value {
margin-left: 0.5rem;
}
/* 인스턴스 테이블 스타일 */
.server-instances {
padding: 0.3rem 0;
max-height: 120px;
}
.instance-table {
font-size: 0.8rem;
}
.instance-table th,
.instance-table td {
padding: 0.2rem 0.3rem;
white-space: nowrap;
}
/* 인스턴스 상태 점 스타일 */
.instance-status {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 4px;
}
/* 오류 상태일 때 인스턴스 ID 색상 변경 */
tr:has(.status-error) .instance-id,
tr:has(.status-inactive) .instance-id {
color: #dc3545;
font-weight: 600;
}
</style>
<script>
// 서버 카드 생성 함수
function createServerCard(server, containerId) {
var template = document.getElementById('server-card-template');
var container = document.getElementById(containerId);
if (!container) return null;
// 템플릿 복제
var clone = document.importNode(template.content, true);
// 서버 이름 설정
clone.querySelector('.server-name').textContent = server.text || server.name;
// 컨테이너에 추가
container.appendChild(clone);
return container.lastElementChild;
}
// 인스턴스 행 생성 함수
function createInstanceRow(instanceData) {
var template = document.getElementById('instance-row-template');
var clone = document.importNode(template.content, true);
// 인스턴스 ID 설정
clone.querySelector('.instance-id').textContent = instanceData.id;
// 상태 설정
var statusClass = 'status-normal';
var statusText = '정상';
if (instanceData.status === 'warning') {
statusClass = 'status-warning';
statusText = '경고';
} else if (instanceData.status === 'error' || instanceData.status === 'inactive') {
statusClass = 'status-error';
statusText = instanceData.status === 'inactive' ? '비활성' : '오류';
}
clone.querySelector('.instance-status').className = 'instance-status ' + statusClass;
clone.querySelector('.instance-status-text').textContent = statusText;
// 처리량 설정
if (instanceData.throughput) {
clone.querySelector('.instance-throughput').textContent = instanceData.throughput;
}
return clone;
}
// 서버 상태 업데이트 함수
function updateServerStatus(data) {
if (!data || !data.servers) return;
var normalCount = 0;
var warningCount = 0;
var errorCount = 0;
// 서버 타입별 상태 카운트
var typeStatusCount = {};
// 각 서버 상태 업데이트
data.servers.forEach(function(server) {
// 서버 타입 추출 (서버 이름에서 괄호 안의 텍스트 추출)
var serverTypeMatch = server.text.match(/\(([^)]+)\)/);
var serverType = serverTypeMatch ? serverTypeMatch[1] : "unknown";
// 서버 이름에서 타입 정보 제거
var displayName = server.name;
// 서버 상태
var serverStatus = server.status;
// 서버 타입별 상태 카운트 초기화
if (!typeStatusCount[serverType]) {
typeStatusCount[serverType] = {
normal: 0,
warning: 0,
error: 0,
total: 0
};
}
typeStatusCount[serverType].total++;
// 전체 상태 카운트 업데이트
if (serverStatus === 'normal') {
normalCount++;
typeStatusCount[serverType].normal++;
} else if (serverStatus === 'warning') {
warningCount++;
typeStatusCount[serverType].warning++;
} else if (serverStatus === 'error') {
errorCount++;
typeStatusCount[serverType].error++;
}
// 서버 타입 소문자로 변환 (ID용)
var serverTypeId = serverType.toLowerCase().replace(/\s+/g, '-');
// 서버 카드 찾기 및 업데이트
var serverContainerId = serverTypeId + '-servers-container';
var serverContainer = document.getElementById(serverContainerId);
if (!serverContainer) return;
// 서버 카드가 없으면 생성
var serverCard = document.querySelector('#' + serverContainerId + ' .server-card[data-server-name="' + server.name + '"]');
if (!serverCard) {
serverCard = createServerCard(server, serverContainerId);
if (serverCard) {
serverCard.setAttribute('data-server-name', server.name);
}
}
if (!serverCard) return;
// 서버 이름 업데이트
var serverNameElement = serverCard.querySelector('.server-name');
if (serverNameElement) {
serverNameElement.textContent = displayName;
}
// 서버 상태 업데이트
var statusBadge = serverCard.querySelector('.server-status-badge');
statusBadge.className = 'server-status-badge';
if (serverStatus === 'normal') {
statusBadge.classList.add('badge-success');
statusBadge.textContent = '정상';
} else if (serverStatus === 'warning') {
statusBadge.classList.add('badge-warning');
statusBadge.textContent = '경고';
} else if (serverStatus === 'error') {
statusBadge.classList.add('badge-danger');
statusBadge.textContent = '오류';
}
// CPU, 메모리, 디스크 업데이트
updateResourceValue(serverCard, 'cpu', server.cpu);
updateResourceValue(serverCard, 'memory', server.memory);
updateResourceValue(serverCard, 'disk', server.disk);
// 인스턴스 테이블 업데이트
var instanceTableBody = serverCard.querySelector('.instance-table-body');
instanceTableBody.innerHTML = '';
if (server.instances && server.instances.length > 0) {
server.instances.forEach(function(instance) {
instanceTableBody.appendChild(createInstanceRow(instance));
});
}
});
// 요약 정보 업데이트
if (document.getElementById('normal-servers')) {
document.getElementById('normal-servers').textContent = normalCount;
}
if (document.getElementById('warning-servers')) {
document.getElementById('warning-servers').textContent = warningCount;
}
if (document.getElementById('error-servers')) {
document.getElementById('error-servers').textContent = errorCount;
}
// 서버 타입별 상태 배지 업데이트
for (var type in typeStatusCount) {
var typeId = type.toLowerCase().replace(/\s+/g, '-');
var badgeElement = document.querySelector('.' + typeId + '-status-badge');
if (badgeElement) {
var countData = typeStatusCount[type];
badgeElement.textContent = countData.normal + ' / ' + countData.total + ' 정상';
badgeElement.className = typeId + '-status-badge server-status-badge';
if (countData.error > 0) {
badgeElement.classList.add('badge-danger');
} else if (countData.warning > 0) {
badgeElement.classList.add('badge-warning');
} else {
badgeElement.classList.add('badge-success');
}
}
}
}
// 리소스 값 업데이트 함수 (CPU, 메모리, 디스크)
function updateResourceValue(serverCard, resourceType, value) {
var valueElement = serverCard.querySelector('.' + resourceType + '-value');
if (!valueElement) return;
valueElement.textContent = value + '%';
valueElement.className = 'metric-value ' + resourceType + '-value';
// 값에 따라 클래스 추가
if (value >= 90) {
valueElement.classList.add('danger');
} else if (value >= 70) {
valueElement.classList.add('warning');
} else {
valueElement.classList.add('success');
}
// 스파크라인 업데이트
var sparkline = serverCard.querySelector('.' + resourceType + '-sparkline');
if (!sparkline) return;
// 서버 이름 가져오기
var serverName = serverCard.getAttribute('data-server-name');
if (!serverName) return;
// 서버 리소스 시계열 데이터 가져오기
ModernDashboardApi.getServerResourceData(serverName, function(data) {
if (data && data.data) {
var resourceData = data.data;
var resourceValues = resourceData[resourceType];
// 스파크라인 업데이트
sparkline.innerHTML = '';
if (resourceValues && resourceValues.length > 0) {
resourceValues.forEach(function(val) {
var bar = document.createElement('div');
bar.className = 'sparkline-bar';
bar.style.height = val + '%';
// 값에 따라 클래스 추가
if (val >= 90) {
bar.classList.add('danger');
} else if (val >= 70) {
bar.classList.add('warning');
} else {
bar.classList.add('success');
}
sparkline.appendChild(bar);
});
} else {
// 데이터가 없는 경우 기본 스파크라인 생성
for (var i = 0; i < 7; i++) {
var bar = document.createElement('div');
bar.className = 'sparkline-bar success';
bar.style.height = value + '%';
sparkline.appendChild(bar);
}
}
} else {
// API 응답이 없는 경우 기본 스파크라인 생성
sparkline.innerHTML = '';
for (var i = 0; i < 7; i++) {
var bar = document.createElement('div');
bar.className = 'sparkline-bar success';
bar.style.height = value + '%';
sparkline.appendChild(bar);
}
}
});
}
// 서버 데이터 로드 함수
function loadServerData() {
console.log("서버 데이터 로드 시작: " + new Date().toLocaleTimeString());
ModernDashboardApi.getServerStatus(null, function(data) {
updateServerStatus(data);
console.log("서버 데이터 로드 완료: " + new Date().toLocaleTimeString());
});
}
// 서버 필터 버튼 클릭 이벤트
$('.filter-btn').click(function() {
var type = $(this).data('type');
if (type === 'all') {
// 전체 표시
$('.server-section').show();
$('.filter-btn').removeClass('active');
$(this).addClass('active');
$(".tran-filter-btn[data-type='all']").click();
} else {
// 필터링
$('.server-section').hide();
$('#server-section-' + type).show();
$('.filter-btn').removeClass('active');
$(this).addClass('active');
$(".tran-filter-btn[data-type='" + type + "']").click();
}
});
// 서버 검색 기능
$('#server-search').on('input', function() {
var searchText = $(this).val().toLowerCase();
if (searchText.length > 0) {
$('.server-card').each(function() {
var serverName = $(this).find('.server-name').text().toLowerCase();
if (serverName.indexOf(searchText) > -1) {
$(this).show();
} else {
$(this).hide();
}
});
} else {
$('.server-card').show();
}
});
// 초기 로드
$(document).ready(function() {
// 서버 데이터 로드
loadServerData();
// 이벤트 리스너 등록 - 중앙 이벤트 관리자 사용
if (window.DashboardEventManager) {
// 초기화 이벤트 리스너
window.DashboardEventManager.on('init', function() {
console.log("서버 상태 초기화 이벤트 수신");
// 이미 초기화되었으므로 추가 작업 필요 없음
});
// 새로고침 이벤트 리스너
window.DashboardEventManager.on('refresh', function() {
console.log("서버 상태 새로고침 이벤트 수신");
loadServerData();
});
} else {
console.error("DashboardEventManager가 정의되지 않았습니다. modern-dashboard.js가 먼저 로드되었는지 확인하세요.");
}
});
</script>
@@ -0,0 +1,267 @@
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<div class="dashboard-sms-summary">
<h2 class="section-title">장애통보(SMS)</h2>
<div class="card shadow mb-0">
<div class="sms-table-container">
<table class="sms-table">
<thead>
<tr>
<th width="18%">인스턴스</th>
<th width="8%">유형</th>
<th width="12%">시간</th>
<th width="10%">건수</th>
<th width="20%">오류대상</th>
<th>메시지</th>
</tr>
</thead>
<tbody id="smsTableBody">
<!-- 데이터가 여기에 동적으로 로드됩니다 -->
</tbody>
</table>
</div>
</div>
</div>
<style>
/* SMS 섹션 스타일 */
.dashboard-sms-summary .section-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.dashboard-sms-summary .card {
border-radius: 0.35rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
}
.sms-table-container {
max-height: 220px;
overflow-y: auto;
}
.sms-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.sms-table th {
background-color: #f8f9fc;
font-weight: 600;
text-align: left;
padding: 0.5rem;
border-bottom: 1px solid #e3e6f0;
position: sticky;
top: 0;
z-index: 10;
}
.sms-table td {
padding: 0.5rem;
border-bottom: 1px solid #f0f0f0;
vertical-align: middle;
}
.sms-table tr:last-child td {
border-bottom: none;
}
.sms-table tr:hover {
background-color: rgba(0, 0, 0, 0.02);
}
.error-type-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
}
.error-type-icon.adapter {
background-color: #4e73df;
color: white;
}
.error-type-icon.interface {
background-color: #e74a3b;
color: white;
}
.error-type-icon i {
font-size: 0.8rem;
}
.error-target {
font-family: monospace;
padding: 0.2rem 0.4rem;
background-color: #f8f9fc;
border-radius: 0.2rem;
font-size: 0.8rem;
}
.text-danger {
color: #e74a3b !important;
}
.empty-message {
padding: 1rem;
text-align: center;
color: #6c757d;
font-style: italic;
}
.occurrence-count {
display: inline-block;
padding: 0.2rem 0.5rem;
background-color: #f8f9fa;
border-radius: 0.25rem;
font-weight: 600;
color: #dc3545;
min-width: 2.5rem;
text-align: center;
}
</style>
<script>
$(document).ready(function() {
// SMS 데이터 로드 함수
function loadSmsData() {
$.ajax({
url: 'modern-dashboard-api.do',
type: 'GET',
data: {
type: 'sms-notification'
, serviceType: sessionStorage['serviceType']
},
dataType: 'json',
success: function(response) {
if (response.success) {
renderSmsTable(response.data);
} else {
console.error('SMS 데이터 로드 실패:', response.message);
showEmptySmsTable();
}
},
error: function(xhr, status, error) {
console.error('SMS 데이터 로드 중 오류 발생:', error);
showEmptySmsTable();
}
});
}
// SMS 테이블 렌더링 함수
function renderSmsTable(data) {
var $tableBody = $('#smsTableBody');
$tableBody.empty();
if (data && data.length > 0) {
// 최대 10개 항목만 표시
var displayData = data.slice(0, 10);
$.each(displayData, function(index, item) {
var errorTypeIcon = getErrorTypeIcon(item.errorType);
var timeStr = formatTime(item.dateTime);
// 오류 대상 정보와 스타일 조건부 적용
var errorTarget = '';
if (item.errorType === 'ADAPTER') {
// 어댑터 오류일 경우 - 클릭 불가능
errorTarget = '<span class="adapter-id">' + (item.adapterId || '-') + '</span>';
} else {
// 인터페이스 오류일 경우 - 클릭 가능, data-service-type 속성 추가
errorTarget = '<span class="error-target interface-id" data-service-type="' +
(item.serviceType || '') + '" data-date-time="' +
(item.dateTime || '') + '">' + (item.interfaceId || '-') + '</span>';
}
var row = '<tr>' +
'<td>' + (item.instanceName || '-') + '</td>' +
'<td>' + errorTypeIcon + '</td>' +
'<td class="text-nowrap">' + timeStr + '</td>' +
'<td><span class="occurrence-count">' + (item.count || '0') + '</span></td>' +
'<td>' + errorTarget + '</td>' +
'<td class="text-danger">' + (item.errorContent || '-') + '</td>' +
'</tr>';
$tableBody.append(row);
});
// 인터페이스ID에만 클릭 이벤트 스타일 적용
$('.interface-id').css({
'cursor': 'pointer',
'color': '#007bff',
'text-decoration': 'underline'
});
// 인터페이스ID 클릭 이벤트 처리 - 인터페이스 타입에만 적용
$('.interface-id').on('click', function(e) {
e.preventDefault();
var row = $(this).closest('tr');
var interfaceId = $(this).text().trim();
var instanceName = row.find('td').eq(0).text().trim();
var errorTime = $(this).data('date-time');
var errorMessage = row.find('td').eq(5).text().trim();
var serviceType = $(this).data('service-type');
// 인터페이스 로그 상세 모달 열기
openInterfaceLogDetailModal(interfaceId, errorTime, instanceName, errorMessage, serviceType);
});
} else {
showEmptySmsTable();
}
}
// 에러 타입 아이콘 생성
function getErrorTypeIcon(errorType) {
if (errorType === 'ADAPTER') {
return '<div class="error-type-icon adapter" title="어댑터 오류"><i class="bi bi-plug"></i></div>';
} else {
return '<div class="error-type-icon interface" title="인터페이스 오류"><i class="bi bi-arrow-left-right"></i></div>';
}
}
// 오류 대상 정보 가져오기
function getErrorTarget(item) {
if (item.errorType === 'ADAPTER') {
return item.adapterId || '-';
} else {
return item.interfaceId || '-';
}
}
// 시간 포맷팅 (HH:mm:ss)
function formatTime(dateTimeStr) {
if (!dateTimeStr) return '-';
var parts = dateTimeStr.split(' ');
if (parts.length >= 2) {
var timePart = parts[1];
return timePart.substring(0, 8); // HH:mm:ss 부분만 추출
}
return '-';
}
// 빈 테이블 표시 함수
function showEmptySmsTable() {
var $tableBody = $('#smsTableBody');
$tableBody.empty();
$tableBody.append('<tr><td colspan="5" class="empty-message">장애 통보 내역이 없습니다</td></tr>');
}
// 초기 데이터 로드
loadSmsData();
// DashboardEventManager를 사용하여 새로고침 이벤트 구독
if (window.DashboardEventManager) {
window.DashboardEventManager.on('refresh', function() {
console.log('SMS 데이터 갱신 시작');
loadSmsData()
});
// 초기화 완료 이벤트 로깅
console.log('SMS 컴포넌트 초기화 완료');
}
});
</script>
@@ -0,0 +1,957 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*" %>
<!-- 소켓 상세 정보 모달 -->
<div id="socketDetailModal" class="modal modal-modern">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">회선 상세 정보</h2>
<span class="close-modal">&times;</span>
</div>
<div class="modal-body">
<!-- 탭 메뉴 -->
<div class="socket-detail-tabs">
<button class="tab-btn active" data-tab="institution-visual">
<i class="bi bi-pie-chart"></i> 기관 현황
</button>
<button class="tab-btn" data-tab="institution-status">
<i class="bi bi-building"></i> 기관 상태
</button>
<button class="tab-btn" data-tab="socket-status">
<i class="bi bi-plug"></i> 어댑터 상태
</button>
<button class="tab-btn" data-tab="socket-process">
<i class="bi bi-arrow-left-right"></i> 세션 상태
</button>
</div>
<!-- 탭 컨텐츠 -->
<div class="socket-detail-content">
<!-- 기관 연결 현황 시각화 탭 -->
<div id="institution-visual" class="tab-content active">
<div class="visual-stats-summary">
<div class="stats-card total-institutions">
<div class="stats-icon"><i class="bi bi-building"></i></div>
<div class="stats-details">
<div class="stats-number" id="total-institutions-count">0</div>
<div class="stats-label">총 기관</div>
</div>
</div>
<div class="stats-card normal-institutions">
<div class="stats-icon"><i class="bi bi-check-circle"></i></div>
<div class="stats-details">
<div class="stats-number" id="normal-institutions-count">0</div>
<div class="stats-label">정상</div>
</div>
</div>
<div class="stats-card warning-institutions">
<div class="stats-icon"><i class="bi bi-exclamation-triangle"></i></div>
<div class="stats-details">
<div class="stats-number" id="warning-institutions-count">0</div>
<div class="stats-label">경고</div>
</div>
</div>
<div class="stats-card error-institutions">
<div class="stats-icon"><i class="bi bi-x-circle"></i></div>
<div class="stats-details">
<div class="stats-number" id="error-institutions-count">0</div>
<div class="stats-label">오류</div>
</div>
</div>
<div class="stats-card inactive-institutions">
<div class="stats-icon"><i class="bi bi-dash-circle"></i></div>
<div class="stats-details">
<div class="stats-number" id="inactive-institutions-count">0</div>
<div class="stats-label">미사용</div>
</div>
</div>
</div>
<div class="connection-visual-container">
<div id="institution-grid" class="institution-grid"></div>
</div>
</div>
<!-- 기관 상태 탭 -->
<div id="institution-status" class="tab-content">
<div class="institution-status-container">
<table class="institution-status-table modal-table">
<thead>
<tr>
<th>기관명</th>
<th>담당자명</th>
<th>연락처</th>
<th>정상어댑터</th>
<th>오류어댑터</th>
<th>회선상태</th>
</tr>
</thead>
<tbody>
<!-- 데이터가 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
</div>
<!-- 회선 상태 탭 -->
<div id="socket-status" class="tab-content">
<div class="socket-status-container">
<table class="socket-status-table modal-table">
<thead>
<tr>
<th>어댑터 그룹</th>
<th>인스턴스명</th>
<th>어댑터명</th>
<th>어댑터 설명</th>
<th>어댑터 유형</th>
<th>상태</th>
<th>기동 상태</th>
</tr>
</thead>
<tbody>
<!-- 데이터가 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
</div>
<!-- 회선 연결 상태 탭 -->
<div id="socket-process" class="tab-content">
<div class="socket-process-container">
<table class="socket-process-table modal-table">
<thead>
<tr>
<th>어댑터 그룹</th>
<th>인스턴스명</th>
<th>어댑터명</th>
<th>어댑터 설명</th>
<th>최대 연결</th>
<th>현재 연결</th>
<th>송신 수</th>
<th>수신 수</th>
<th>오류 수</th>
<th>상태</th>
</tr>
</thead>
<tbody>
<!-- 데이터가 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary refresh-socket-data">
<i class="bi bi-arrow-clockwise" style="color: #ffffff;"></i> 새로고침
</button>
<button class="btn btn-secondary close-modal-btn">닫기</button>
</div>
</div>
</div>
<style>
/* 기존 스타일 강화 */
.modal-modern {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.modal-modern .modal-content {
position: relative;
background-color: #fff;
margin: 2% auto;
padding: 0;
width: 95%;
max-width: 1400px;
border: none;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
animation: modalSlideUp 0.3s;
}
@keyframes modalSlideUp {
from {opacity: 0; transform: translateY(30px);}
to {opacity: 1; transform: translateY(0);}
}
@keyframes modalFadeIn {
from {opacity: 0;}
to {opacity: 1;}
}
.modal-modern .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid #e0e0e0;
background-color: #f8f9fa;
border-radius: 16px 16px 0 0;
}
.modal-modern .modal-title {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #333;
}
.modal-modern .close-modal {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.05);
}
.modal-modern .close-modal:hover {
color: #333;
background-color: rgba(0, 0, 0, 0.1);
}
.modal-modern .modal-body {
padding: 1.5rem 2rem;
max-height: 75vh;
overflow-y: auto;
background-color: #f8f9fa;
}
.modal-modern .modal-footer {
padding: 1.2rem 2rem;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: flex-end;
gap: 1rem;
border-radius: 0 0 16px 16px;
background-color: #fff;
}
/* 탭 스타일 향상 */
.socket-detail-tabs {
display: flex;
border-bottom: none;
margin-bottom: 1.5rem;
overflow-x: auto;
background-color: #fff;
border-radius: 8px;
padding: 0.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.tab-btn {
padding: 0.9rem 1.5rem;
background: none;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-size: 1rem;
color: #666;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tab-btn:hover {
color: #333;
background-color: rgba(0, 0, 0, 0.05);
}
.tab-btn.active {
color: white;
background-color: #007bff;
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.2);
}
.tab-content {
display: none;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from {opacity: 0;}
to {opacity: 1;}
}
.tab-content.active {
display: block;
}
/* 테이블 스타일 향상 */
.modal-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background-color: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.modal-table th {
background-color: #007bff;
color: white;
padding: 1.2rem;
text-align: left;
font-weight: 800;
top: 0;
z-index: 10;
}
.modal-table td {
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
}
.modal-table tbody tr {
transition: background-color 0.2s;
}
.modal-table tbody tr:hover {
background-color: rgba(0, 0, 0, 0.03);
}
.modal-table tbody tr:last-child td {
border-bottom: none;
}
/* 버튼 스타일 향상 */
.btn {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-size: inherit;
transition: all 0.25s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0069d9;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
.btn-secondary {
background-color: #f1f5f9;
color: #333;
}
.btn-secondary:hover {
background-color: #e2e8f0;
}
/* 로딩 애니메이션 향상 */
.loading-indicator {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid #007bff;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 통계 카드 스타일 향상 */
.visual-stats-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stats-card {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
padding: 1.2rem;
display: flex;
align-items: center;
gap: 1rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.stats-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.stats-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
}
.stats-details {
flex: 1;
}
.stats-number {
font-size: 1.8rem;
font-weight: 700;
line-height: 1.1;
}
.stats-label {
font-size: 0.9rem;
color: #666;
margin-top: 0.2rem;
}
/* 카드 색상 */
.total-institutions .stats-icon {
background-color: rgba(0, 123, 255, 0.3);
color: #007bff;
}
.normal-institutions .stats-icon {
background-color: rgba(40, 167, 69, 0.3);
color: #28a745;
}
.warning-institutions .stats-icon {
background-color: rgba(255, 193, 7, 0.3);
color: #ffc107;
}
.error-institutions .stats-icon {
background-color: rgba(220, 53, 69, 0.3);
color: #dc3545;
}
.inactive-institutions .stats-icon {
background-color: rgba(108, 117, 125, 0.3);
color: #6c757d;
}
/* 기관 카드 그리드 향상 */
.connection-visual-container {
background-color: #fff;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.institution-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.institution-card {
background-color: #f8f9fa;
border-radius: 12px;
padding: 1.2rem;
transition: all 0.3s ease;
border-left: 5px solid #ccc;
cursor: pointer;
position: relative;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.institution-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.institution-status-indicator {
position: absolute;
top: 15px;
right: 15px;
font-size: 1.2rem;
}
.institution-name {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
color: #333;
padding-right: 2rem;
}
.institution-info {
display: grid;
grid-template-columns: 80px 1fr;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.institution-info-label {
color: #666;
}
.institution-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-top: 1rem;
border-top: 1px solid #e0e0e0;
padding-top: 0.8rem;
}
.institution-stat {
text-align: center;
padding: 0.5rem;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.03);
}
/* 상태 배지 스타일 */
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.4rem 0.8rem;
border-radius: 50px;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
}
.status-badge-normal {
background-color: rgba(40, 167, 69, 0.15);
color: #28a745;
}
.status-badge-warning {
background-color: rgba(255, 193, 7, 0.15);
color: #ffc107;
}
.status-badge-error {
background-color: rgba(220, 53, 69, 0.15);
color: #dc3545;
}
.status-badge-inactive {
background-color: rgba(108, 117, 125, 0.15);
color: #6c757d;
}
/* 반응형 디자인 */
@media (max-width: 1024px) {
.visual-stats-summary {
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
}
.modal-modern .modal-content {
width: 98%;
margin: 1% auto;
}
.modal-modern .modal-body {
padding: 1rem 1.5rem;
}
}
@media (max-width: 768px) {
.socket-detail-tabs {
padding: 0.3rem;
flex-wrap: wrap;
}
.tab-btn {
padding: 0.8rem 1rem;
font-size: 0.9rem;
flex-grow: 1;
}
.visual-stats-summary {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.8rem;
}
.stats-icon {
width: 40px;
height: 40px;
font-size: 1.2rem;
}
.stats-number {
font-size: 1.5rem;
}
.institution-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.modal-modern .modal-header,
.modal-modern .modal-footer {
padding: 1rem 1.5rem;
}
}
@media (max-width: 576px) {
.visual-stats-summary {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.institution-grid {
grid-template-columns: 1fr;
}
.modal-modern .modal-body {
padding: 1rem;
}
}
.institution-card.status-normal {
border-left: 5px solid #98e2a6; /* 파스텔 그린 */
}
.institution-card.status-warning {
border-left: 5px solid #ffe5a3; /* 파스텔 옐로우 */
}
.institution-card.status-error {
border-left: 5px solid #ffb3b3; /* 파스텔 레드 */
}
.institution-card.status-inactive {
border-left: 5px solid #d9d9d9; /* 파스텔 그레이 */
}
</style>
<script>
// 소켓 상세 정보 모달 관련 스크립트
(function() {
// 모달 요소
const modal = document.getElementById('socketDetailModal');
const closeButtons = modal.querySelectorAll('.close-modal, .close-modal-btn');
// 탭 관련 요소
const tabButtons = modal.querySelectorAll('.tab-btn');
const tabContents = modal.querySelectorAll('.tab-content');
// 테이블 요소
const institutionTable = modal.querySelector('.institution-status-table tbody');
const statusTable = modal.querySelector('.socket-status-table tbody');
const processTable = modal.querySelector('.socket-process-table tbody');
// 시각화 요소
const institutionGrid = document.getElementById('institution-grid');
const totalInstitutionsCount = document.getElementById('total-institutions-count');
const normalInstitutionsCount = document.getElementById('normal-institutions-count');
const warningInstitutionsCount = document.getElementById('warning-institutions-count');
const errorInstitutionsCount = document.getElementById('error-institutions-count');
const inactiveInstitutionsCount = document.getElementById('inactive-institutions-count');
// 현재 선택된 서버와 어댑터 그룹
let currentServerName = '';
let currentAdapterGroup = '';
// 모달 닫기 이벤트
closeButtons.forEach(button => {
button.addEventListener('click', () => {
modal.style.display = 'none';
});
});
// 모달 외부 클릭 시 닫기
window.addEventListener('click', (event) => {
if (event.target === modal) {
modal.style.display = 'none';
}
});
// 탭 전환 이벤트
tabButtons.forEach(button => {
button.addEventListener('click', () => {
// 활성 탭 변경
tabButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// 탭 컨텐츠 변경
const tabId = button.getAttribute('data-tab');
tabContents.forEach(content => {
content.classList.remove('active');
if (content.id === tabId) {
content.classList.add('active');
}
});
});
});
// 새로고침 버튼 이벤트
const refreshButton = modal.querySelector('.refresh-socket-data');
refreshButton.addEventListener('click', () => {
loadSocketData(currentServerName, currentAdapterGroup);
});
// 소켓 데이터 로드 함수
function loadSocketData(serverName, adapterGroup) {
// 현재 선택된 서버와 어댑터 그룹 저장
currentServerName = serverName || '';
currentAdapterGroup = adapterGroup || '';
// 로딩 표시
institutionTable.innerHTML = '<tr><td colspan="6"><div class="loading-indicator"><div class="loading-spinner"></div></div></td></tr>';
statusTable.innerHTML = '<tr><td colspan="7"><div class="loading-indicator"><div class="loading-spinner"></div></div></td></tr>';
processTable.innerHTML = '<tr><td colspan="10"><div class="loading-indicator"><div class="loading-spinner"></div></div></td></tr>';
institutionGrid.innerHTML = '<div class="loading-indicator"><div class="loading-spinner"></div></div>';
// API 호출
ModernDashboardApi.getSocketCombined(serverName, adapterGroup, function(data) {
if (data) {
// 기관 상태 데이터 표시
updateInstitutionStatusTable(data.institutionStatus);
// 기관 시각화 업데이트
updateInstitutionVisualization(data.institutionStatus);
// 회선 상태 데이터 표시
updateSocketStatusTable(data.socketStatus);
// 회선 연결 상태 데이터 표시
updateSocketProcessTable(data.socketProcess);
}
}, function(xhr, status, error) {
// 에러 처리
const errorMsg = '<div style="padding: 2rem; text-align: center; color: #dc3545;"><i class="fas fa-exclamation-circle" style="font-size: 3rem;"></i><p style="margin-top: 1rem;">데이터를 불러오는 중 오류가 발생했습니다.</p></div>';
institutionTable.innerHTML = '<tr><td colspan="6">' + errorMsg + '</td></tr>';
statusTable.innerHTML = '<tr><td colspan="7">' + errorMsg + '</td></tr>';
processTable.innerHTML = '<tr><td colspan="10">' + errorMsg + '</td></tr>';
institutionGrid.innerHTML = errorMsg;
console.error('소켓 데이터 로드 실패:', error);
});
}
// 기관 상태 테이블 업데이트
function updateInstitutionStatusTable(data) {
if (!data || data.length === 0) {
institutionTable.innerHTML = '<tr><td colspan="6"><div style="padding: 2rem; text-align: center; color: #6c757d;"><i class="fas fa-info-circle" style="font-size: 2rem;"></i><p style="margin-top: 1rem;">표시할 데이터가 없습니다.</p></div></td></tr>';
return;
}
let html = '';
data.forEach(institution => {
let statusClass = '';
let statusText = '';
// 상태에 따른 텍스트 및 클래스 설정
if (institution.lineStatus === 'UNDEFINED') {
statusText = '미사용';
statusClass = 'status-badge status-badge-inactive';
} else if (institution.lineStatus === 'WARN') {
statusText = '경고';
statusClass = 'status-badge status-badge-warning';
} else if (institution.lineStatus === 'ERROR') {
statusText = '오류';
statusClass = 'status-badge status-badge-error';
} else {
statusText = '정상';
statusClass = 'status-badge status-badge-normal';
}
html += '<tr>' +
'<td>' + (institution.institutionName || '-') + '</td>' +
'<td>' + (institution.contactPerson || '-') + '</td>' +
'<td>' + (institution.contactNumber || '-') + '</td>' +
'<td>' + (institution.normalAdapterCount || '0') + '</td>' +
'<td>' + (institution.errorAdapterCount || '0') + '</td>' +
'<td><span class="' + statusClass + '">' + statusText + '</span></td>' +
'</tr>';
});
institutionTable.innerHTML = html;
}
// 기관 시각화 업데이트
function updateInstitutionVisualization(data) {
if (!data || data.length === 0) {
institutionGrid.innerHTML = '<div style="padding: 2rem; text-align: center; color: #6c757d;"><i class="fas fa-info-circle" style="font-size: 2rem;"></i><p style="margin-top: 1rem;">표시할 데이터가 없습니다.</p></div>';
updateStatsCounts(0, 0, 0, 0, 0);
return;
}
let html = '';
let normalCount = 0;
let warningCount = 0;
let errorCount = 0;
let inactiveCount = 0;
data.forEach(institution => {
let statusClass = 'status-normal';
let statusIcon = '<i class="bi bi-check-circle" style="color: #28a745;"></i>';
let statusName = '정상';
// 상태에 따른 시각화 설정
if (institution.lineStatus === 'UNDEFINED') {
statusClass = 'status-inactive';
statusIcon = '<i class="bi bi-dash-circle" style="color: #6c757d;"></i>';
statusName = '미사용';
inactiveCount++;
} else if (institution.lineStatus === 'WARN') {
statusClass = 'status-warning';
statusIcon = '<i class="bi bi-exclamation-triangle" style="color: #ffc107;"></i>';
statusName = '경고';
warningCount++;
} else if (institution.lineStatus === 'ERROR') {
statusClass = 'status-error';
statusIcon = '<i class="bi bi-x-circle" style="color: #dc3545;"></i>';
statusName = '오류';
errorCount++;
} else {
normalCount++;
}
html += '<div class="institution-card ' + statusClass + '" data-code="' + institution.institutionCode + '">' +
'<div class="institution-status-indicator">' + statusIcon + '</div>' +
'<div class="institution-name">' + (institution.institutionName || '미지정 기관') + '</div>' +
'<div class="institution-info">' +
'<span class="institution-info-label">담당자:</span>' +
'<span>' + (institution.contactPerson || '-') + '</span>' +
'</div>' +
'<div class="institution-info">' +
'<span class="institution-info-label">연락처:</span>' +
'<span>' + (institution.contactNumber || '-') + '</span>' +
'</div>' +
'<div class="institution-info">' +
'<span class="institution-info-label">상태:</span>' +
'<span>' + statusName + '</span>' +
'</div>' +
'<div class="institution-stats">' +
'<div class="institution-stat">' +
'<div class="stat-number normal">' + (institution.normalAdapterCount || '0') + '</div>' +
'<div class="stat-label">정상 어댑터</div>' +
'</div>' +
'<div class="institution-stat">' +
'<div class="stat-number error">' + (institution.errorAdapterCount || '0') + '</div>' +
'<div class="stat-label">오류 어댑터</div>' +
'</div>' +
'</div>' +
'</div>';
});
institutionGrid.innerHTML = html;
updateStatsCounts(data.length, normalCount, warningCount, errorCount, inactiveCount);
// 기관 카드 클릭 이벤트 - 해당 기관 정보로 필터링
const institutionCards = institutionGrid.querySelectorAll('.institution-card');
institutionCards.forEach(card => {
card.addEventListener('click', () => {
const institutionCode = card.getAttribute('data-code');
// 여기에 기관 코드로 필터링하는 로직 추가
console.log('기관 선택:', institutionCode);
// 기관에 해당하는 어댑터로 필터링하는 탭으로 이동
const socketStatusTab = document.querySelector('.tab-btn[data-tab="socket-status"]');
socketStatusTab.click();
});
});
}
// 통계 수치 업데이트
function updateStatsCounts(total, normal, warning, error, inactive) {
totalInstitutionsCount.textContent = total;
normalInstitutionsCount.textContent = normal;
warningInstitutionsCount.textContent = warning;
errorInstitutionsCount.textContent = error;
inactiveInstitutionsCount.textContent = inactive;
}
// 회선 상태 테이블 업데이트
function updateSocketStatusTable(data) {
if (!data || !data.rows || data.rows.length === 0) {
statusTable.innerHTML = '<tr><td colspan="7"><div style="padding: 2rem; text-align: center; color: #6c757d;"><i class="fas fa-info-circle" style="font-size: 2rem;"></i><p style="margin-top: 1rem;">표시할 데이터가 없습니다.</p></div></td></tr>';
return;
}
let html = '';
data.rows.forEach(row => {
let statusClass = row.status === 'true' ? 'status-badge status-badge-normal' : 'status-badge status-badge-error';
const statusName = row.status === 'true' ? '정상' : '오류';
html += '<tr>' +
'<td>' + (row.adptrBzwkGroupName || '-') + '</td>' +
'<td>' + (row.eaiSevrInstncName || '-') + '</td>' +
'<td>' + (row.adptrNickName || '-') + '</td>' +
'<td>' + (row.adptrBzwkDesc || '-') + '</td>' +
'<td>' + (row.adptrType || '-') + '</td>' +
'<td><span class="' + statusClass + '">' + statusName + '</span></td>' +
'<td>' + (row.starting || '-') + '</td>' +
'</tr>';
});
statusTable.innerHTML = html;
}
// 회선 연결 상태 테이블 업데이트
function updateSocketProcessTable(data) {
if (!data || !data.rows || data.rows.length === 0) {
processTable.innerHTML = '<tr><td colspan="10"><div style="padding: 2rem; text-align: center; color: #6c757d;"><i class="fas fa-info-circle" style="font-size: 2rem;"></i><p style="margin-top: 1rem;">표시할 데이터가 없습니다.</p></div></td></tr>';
return;
}
let html = '';
data.rows.forEach(row => {
let statusClass = row.status === 'true' ? 'status-badge status-badge-normal' : 'status-badge status-badge-error';
const statusName = row.status === 'true' ? '정상' : '오류';
html += '<tr>' +
'<td>' + (row.adptrBzwkGroupName || '-') + '</td>' +
'<td>' + (row.eaiSevrInstncName || '-') + '</td>' +
'<td>' + (row.adptrBzwkName || '-') + '</td>' +
'<td>' + (row.adptrBzwkDesc || '-') + '</td>' +
'<td>' + (row.maxConnection || '0') + '</td>' +
'<td>' + (row.currentConnection || '0') + '</td>' +
'<td>' + (row.sendCount || '0') + '</td>' +
'<td>' + (row.recvCount || '0') + '</td>' +
'<td>' + (row.errorCount || '0') + '</td>' +
'<td><span class="' + statusClass + '">' + statusName + '</span></td>' +
'</tr>';
});
processTable.innerHTML = html;
}
// 모달 열기 함수 (전역 스코프에 노출)
window.openSocketDetailModal = function(serverName, adapterGroup) {
// 모달 표시
modal.style.display = 'block';
// 데이터 로드
loadSocketData(serverName, adapterGroup);
};
})();
</script>
@@ -0,0 +1,699 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<div class="dashboard-summary">
<h2 class="section-title">실시간 트랜잭션 현황</h2>
<div class="card">
<div class="chart-controls">
<div class="btn-group" style="display: none">
<button class="btn btn-chart-type" data-type="area">영역</button>
<button class="btn btn-chart-type active" data-type="line">선형</button>
<button class="btn btn-chart-type" data-type="bar">막대</button>
</div>
<div class="btn-group ml-2">
<button class="btn btn-time-range" data-range="5m">5분</button>
<button class="btn btn-time-range active" data-range="10m">10분</button>
<button class="btn btn-time-range" data-range="30m">30분</button>
<button class="btn btn-time-range" data-range="1h">1시간</button>
</div>
</div>
<div class="chart-container">
<div id="transaction-chart" class="chart-content">
<i class="bi bi-bar-chart-line"></i>
<span>트랜잭션 데이터를 로딩 중입니다...</span>
</div>
</div>
</div>
</div>
<!-- ApexCharts 라이브러리 로드 -->
<script src="<%=request.getContextPath()%>/addon/apexcharts/apexcharts.js"></script>
<!-- 트랜잭션 차트 관련 스크립트 -->
<script>
// 트랜잭션 시계열 데이터 관리 객체
const TransactionTimeSeriesData = {
// 서비스별 시계열 데이터를 저장할 맵
serviceData: {},
// 최대 데이터 포인트 수 (60분 간격, 10초마다 갱신 = 360개 포인트)
maxDataPoints: 360,
// 초기화
init: function() {
// 서비스 목록 초기화
this.initServiceTypes();
},
// 서비스 유형 초기화
initServiceTypes: function() {
// 서버 데이터 배열에서 서비스 타입 추출
if (window.serverDataArray) {
const uniqueTypes = new Set();
serverDataArray.forEach(server => {
uniqueTypes.add(server.type);
});
// 각 고유 서비스 타입에 대해 시계열 데이터 초기화
uniqueTypes.forEach(serviceType => {
if (!this.serviceData[serviceType]) {
this.serviceData[serviceType] = {
timestamps: [],
success: [],
fail: [],
timeout: [],
syserror: []
};
}
});
} else {
// 기본 서비스 타입 (기본값)
const defaultTypes = ['EAI', 'FEP', 'MCI', 'MCU', 'BAP'];
defaultTypes.forEach(serviceType => {
this.serviceData[serviceType] = {
timestamps: [],
success: [],
fail: [],
timeout: [],
syserror: []
};
});
}
// 합계 데이터를 위한 TOTAL 서비스 추가
this.serviceData['TOTAL'] = {
timestamps: [],
success: [],
fail: [],
timeout: [],
syserror: []
};
},
// 타임스탬프를 Date 객체로 변환
ensureDateObject: function(timestamp) {
if (timestamp instanceof Date) {
return timestamp;
} else if (typeof timestamp === 'number') {
return new Date(timestamp);
} else if (typeof timestamp === 'string') {
return new Date(timestamp);
}
return new Date(); // 기본값으로 현재 시간 반환
},
// 트랜잭션 데이터 업데이트
updateData: function(transactions) {
if (!transactions || !transactions.length) return;
const timestamp = new Date();
// 각 서비스별 데이터 업데이트
transactions.forEach(transaction => {
const serviceType = transaction.type;
// 해당 서비스의 시계열 데이터가 없으면 초기화
if (!this.serviceData[serviceType]) {
this.serviceData[serviceType] = {
timestamps: [],
success: [],
fail: [],
timeout: [],
syserror: []
};
}
const data = this.serviceData[serviceType];
// 타임스탬프 추가
data.timestamps.push(timestamp);
// 데이터 추가
data.success.push(transaction.success || 0);
data.fail.push(transaction.fail || 0);
data.timeout.push(transaction.timeout || 0);
data.syserror.push(transaction.syserror || 0);
// 최대 데이터 포인트 수 제한
if (data.timestamps.length > this.maxDataPoints) {
data.timestamps.shift();
data.success.shift();
data.fail.shift();
data.timeout.shift();
data.syserror.shift();
}
});
},
// 오래된 데이터 정리 (60분 이상 경과된 데이터)
cleanupData: function() {
const cutoffTime = new Date(new Date() - 60 * 60 * 1000); // 60분 전
for (const serviceType in this.serviceData) {
const data = this.serviceData[serviceType];
// 각 데이터 시리즈에서 60분 이전 데이터 제거
let i = 0;
while (i < data.timestamps.length && this.ensureDateObject(data.timestamps[i]) < cutoffTime) {
i++;
}
if (i > 0) {
data.timestamps = data.timestamps.slice(i);
data.success = data.success.slice(i);
data.fail = data.fail.slice(i);
data.timeout = data.timeout.slice(i);
data.syserror = data.syserror.slice(i);
}
}
},
// 특정 서비스의 시계열 데이터 가져오기
getServiceData: function(serviceType) {
return this.serviceData[serviceType] || {
timestamps: [],
success: [],
fail: [],
timeout: [],
syserror: []
};
},
};
// 트랜잭션 차트 관리 객체
const TransactionChart = {
chart: null,
chartType: 'line', // 기본값: 영역 차트
timeRange: '10m', // 기본값: 5분
// 초기화
init: function(containerId) {
// ApexCharts 로드 확인
if (typeof ApexCharts === 'undefined') {
console.error('ApexCharts 라이브러리가 로드되지 않았습니다.');
return;
}
const container = document.getElementById(containerId);
if (!container) {
console.error('차트 컨테이너를 찾을 수 없습니다: ' + containerId);
return;
}
// 기존 차트 제거
if (this.chart) {
this.chart.destroy();
}
// 새 차트 인스턴스 생성
const options = this.getChartOptions();
this.chart = new ApexCharts(container, options);
this.chart.render();
// 브라우저 창 크기 변경 시 차트 크기 조정
window.addEventListener('resize', () => {
if (this.chart) {
this.chart.updateOptions({
chart: {
width: '100%'
}
});
}
});
},
// 차트 옵션 가져오기
getChartOptions: function() {
return {
chart: {
type: this.chartType,
height: 180,
toolbar: {
show: false
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
animateGradually: {
enabled: true,
delay: 150
},
dynamicAnimation: {
enabled: true,
speed: 350
}
},
fontFamily: 'Noto Sans KR, sans-serif',
zoom: {
enabled: false
}
},
dataLabels: {
enabled: false
},
stroke: {
curve: 'smooth',
width: 2
},
series: [],
colors: ['#4CAF50', '#F44336', '#2196F3', '#FF5722', '#9C27B0', '#FF9800'],
fill: {
type: this.chartType === 'area' ? 'gradient' : 'solid',
gradient: {
shade: 'light',
type: 'vertical',
shadeIntensity: 0.3,
opacityFrom: 0.7,
opacityTo: 0.2,
stops: [0, 90, 100]
}
},
grid: {
borderColor: '#e0e0e0',
strokeDashArray: 3,
xaxis: {
lines: {
show: false
}
}
},
tooltip: {
shared: true,
intersect: false,
y: {
formatter: function(value) {
return value.toLocaleString() + ' 건';
}
},
x: {
formatter: function(value) {
const date = new Date(value);
return date.toLocaleTimeString();
}
}
},
xaxis: {
type: 'datetime',
labels: {
datetimeUTC: false,
format: 'HH:mm'
},
tooltip: {
enabled: false
},
tickAmount: 6
},
yaxis: {
title: {
text: '처리 건수',
style: {
fontWeight: 600
}
},
labels: {
formatter: function(value) {
return value.toLocaleString();
}
},
forceNiceScale: true,
min: undefined,
max: undefined
},
legend: {
position: 'top',
horizontalAlign: 'right',
floating: true,
offsetY: -25,
offsetX: -5,
showForSingleSeries: true,
onItemClick: {
toggleDataSeries: true
},
onItemHover: {
highlightDataSeries: true
},
markers: {
width: 12,
height: 12,
strokeWidth: 0,
radius: 12,
offsetX: -5
},
itemMargin: {
horizontal: 10,
vertical: 5
}
},
states: {
hover: {
filter: {
type: 'lighten',
value: 0.05
}
},
active: {
filter: {
type: 'darken',
value: 0.1
}
}
},
responsive: [
{
breakpoint: 768,
options: {
legend: {
position: 'bottom',
horizontalAlign: 'center',
floating: false,
offsetY: 0,
offsetX: 0
}
}
}
]
};
},
// 서비스 타입에 따른 색상 맵
colorMap: {
'EAI': { success: '#4CAF50', error: '#F44336' },
'FEP': { success: '#2196F3', error: '#FF5722' },
'MCI': { success: '#9C27B0', error: '#FF9800' },
'MCU': { success: '#3F51B5', error: '#FFC107' },
'BAP': { success: '#00BCD4', error: '#795548' },
'TOTAL': { success: '#607D8B', error: '#9E9E9E' }
},
// 서비스 및 데이터 타입에 따른 색상 반환
getServiceColor: function(serviceType, dataType) {
if (this.colorMap[serviceType] && this.colorMap[serviceType][dataType]) {
return this.colorMap[serviceType][dataType];
}
// 기본 색상
return dataType === 'success' ? '#66BB6A' : '#EF5350';
},
// 차트 타입 설정
setChartType: function(type) {
this.chartType = type;
this.updateChart();
},
// 시간 범위 설정
setTimeRange: function(range) {
this.timeRange = range;
this.updateChart();
},
// 차트 업데이트
updateChart: function() {
if (!this.chart) return;
const series = [];
// 각 서비스에 대한 시리즈 생성 (TOTAL 제외)
for (const serviceType in TransactionTimeSeriesData.serviceData) {
if (serviceType === 'TOTAL') continue;
const serviceData = TransactionTimeSeriesData.serviceData[serviceType];
if (!serviceData.timestamps.length) continue;
// 성공 시리즈
series.push({
name: serviceType + ' 성공',
type: this.chartType,
data: serviceData.timestamps.map((time, index) => ({
x: TransactionTimeSeriesData.ensureDateObject(time).getTime(),
y: serviceData.success[index]
})),
color: this.getServiceColor(serviceType, 'success')
});
// 에러 시리즈 (실패 + 타임아웃 + 시스템 에러)
series.push({
name: serviceType + ' 에러',
type: this.chartType,
data: serviceData.timestamps.map((time, index) => ({
x: TransactionTimeSeriesData.ensureDateObject(time).getTime(),
y: serviceData.fail[index] + serviceData.timeout[index] + serviceData.syserror[index]
})),
color: this.getServiceColor(serviceType, 'error')
});
}
// 시간 범위에 따른 X축 범위 설정
const rangeMinutes = {
'5m': 5,
'10m': 10,
'30m': 30,
'1h': 60
}[this.timeRange] || 10;
const startTime = new Date(new Date() - rangeMinutes * 60 * 1000);
const endTime = new Date();
// 차트 옵션 업데이트 - 전체 다시 그리기 방지
this.chart.updateOptions({
chart: {
type: this.chartType
},
fill: {
type: this.chartType === 'area' ? 'gradient' : 'solid'
},
xaxis: {
min: startTime.getTime(),
max: endTime.getTime() + 1000
}
}, false, false); // 차트 다시 그리지 않고 업데이트
// 시리즈 데이터만 업데이트
this.chart.updateSeries(series, false);
},
// 모의 데이터로 차트 업데이트 (테스트용)
updateWithMockData: function() {
const mockData = TransactionTimeSeriesData.generateMockData();
TransactionTimeSeriesData.updateData(mockData);
this.updateChart();
}
};
// 문서 로드 완료 시 초기화
$(document).ready(function() {
// 시계열 데이터 관리 객체 초기화
TransactionTimeSeriesData.init();
// 차트 초기화
TransactionChart.init('transaction-chart');
// 차트 컨트롤 이벤트 설정
setupChartControls();
// 초기 데이터 로드
loadInitialChartData();
// 10초마다 오래된 데이터 정리
setInterval(() => TransactionTimeSeriesData.cleanupData(), 10000);
// DashboardEventManager를 사용하여 새로고침 이벤트 구독
if (window.DashboardEventManager) {
window.DashboardEventManager.on('refresh', function() {
// 트랜잭션 데이터 추가 로드
loadSummaryTransactionData();
});
}
});
// 차트 컨트롤 이벤트 설정
function setupChartControls() {
// 차트 타입 변경
$('.btn-chart-type').click(function() {
// 기존 활성화 버튼 비활성화
$('.btn-chart-type').removeClass('active');
// 클릭한 버튼 활성화
$(this).addClass('active');
// 차트 타입 변경
const chartType = $(this).data('type');
TransactionChart.setChartType(chartType);
});
// 시간 범위 변경
$('.btn-time-range').click(function() {
// 기존 활성화 버튼 비활성화
$('.btn-time-range').removeClass('active');
// 클릭한 버튼 활성화
$(this).addClass('active');
// 시간 범위 변경
const timeRange = $(this).data('range');
TransactionChart.setTimeRange(timeRange);
});
}
// 초기 차트 데이터 로드
function loadInitialChartData() {
// 시계열 데이터 API 호출
ModernDashboardApi.getTransactionTimeSeries(null, 10, function(data) {
if (data && data.timeSeriesData) {
// 각 서비스별 시계열 데이터 처리
for (var serviceType in data.timeSeriesData) {
var serviceData = data.timeSeriesData[serviceType];
if (!serviceData.timestamps || serviceData.timestamps.length === 0) continue;
// 기존 데이터 초기화
TransactionTimeSeriesData.serviceData[serviceType] = {
timestamps: [],
success: [],
fail: [],
timeout: [],
syserror: []
};
// API 응답 데이터 복사 및 타임스탬프 변환
TransactionTimeSeriesData.serviceData[serviceType].timestamps = serviceData.timestamps.map(time =>
TransactionTimeSeriesData.ensureDateObject(time)
);
TransactionTimeSeriesData.serviceData[serviceType].success = serviceData.success.slice();
TransactionTimeSeriesData.serviceData[serviceType].fail = serviceData.fail.slice();
TransactionTimeSeriesData.serviceData[serviceType].timeout = serviceData.timeout.slice();
TransactionTimeSeriesData.serviceData[serviceType].syserror = serviceData.syserror.slice();
}
// 차트 업데이트
TransactionChart.updateChart();
} else {
// API 응답이 없는 경우 모의 데이터로 테스트
TransactionChart.updateWithMockData();
}
});
}
// 트랜잭션 데이터 로드 및 차트 업데이트 (함수명 변경)
function loadSummaryTransactionData() {
// 현재 선택된 시간 범위 가져오기
var activeRange = $('.btn-time-range.active').data('range') || '10m';
var minutes = {
'5m': 5,
'10m': 10,
'30m': 30,
'1h': 60
}[activeRange] || 10;
ModernDashboardApi.getTransactionTimeSeries(null, minutes, function(data) {
if (data && data.timeSeriesData) {
// 각 서비스별 시계열 데이터 처리
for (var serviceType in data.timeSeriesData) {
var serviceData = data.timeSeriesData[serviceType];
if (!serviceData.timestamps || serviceData.timestamps.length === 0) continue;
// 기존 데이터 초기화
TransactionTimeSeriesData.serviceData[serviceType] = {
timestamps: [],
success: [],
fail: [],
timeout: [],
syserror: []
};
// API 응답 데이터 복사 및 타임스탬프 변환
TransactionTimeSeriesData.serviceData[serviceType].timestamps = serviceData.timestamps.map(time =>
TransactionTimeSeriesData.ensureDateObject(time)
);
TransactionTimeSeriesData.serviceData[serviceType].success = serviceData.success.slice();
TransactionTimeSeriesData.serviceData[serviceType].fail = serviceData.fail.slice();
TransactionTimeSeriesData.serviceData[serviceType].timeout = serviceData.timeout.slice();
TransactionTimeSeriesData.serviceData[serviceType].syserror = serviceData.syserror.slice();
}
// 차트 업데이트 - 부드러운 업데이트
TransactionChart.updateChart();
}
});
}
</script>
<!-- 추가 CSS 스타일 -->
<style>
.chart-controls {
display: flex;
justify-content: flex-end;
}
.chart-controls .btn-group {
display: flex;
}
.chart-controls .btn {
padding: 4px 8px;
font-size: 0.75rem;
border: 1px solid #e0e0e0;
background-color: #fff;
color: #666;
transition: all 0.2s ease;
}
.chart-controls .btn:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.chart-controls .btn:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.chart-controls .btn.active {
background-color: #2196F3;
color: white;
border-color: #2196F3;
}
.chart-controls .ml-2 {
margin-left: 0.5rem;
}
.chart-content {
width: 100%;
height: 100%;
min-height: 200px;
display: flex;
justify-content: center;
align-items: center;
color: var(--color-gray-400);
}
.card {
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
background-color: #fff;
padding: 16px;
margin-bottom: 0px;
}
/* 범례 스타일 개선 */
.apexcharts-legend-series {
margin: 3px 8px !important;
cursor: pointer !important;
}
.apexcharts-legend-text {
font-size: 0.85rem !important;
color: #333 !important;
font-weight: 500 !important;
}
.apexcharts-legend-marker {
margin-right: 5px !important;
}
.apexcharts-legend-series.apexcharts-inactive-legend {
opacity: 0.5 !important;
}
</style>
@@ -0,0 +1,834 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*" %>
<%@ page import="com.eactive.eai.rms.common.datasource.*" %>
<%@ page import="java.util.stream.Collectors" %>
<div class="dashboard-transactions">
<div class="dashboard-header">
<div class="header-title-container">
<h2 class="section-title">거래처리 집계(일누적)</h2>
</div>
</div>
<div class="card shadow mb-0" style="overflow-y:auto;">
<div class="transaction-filters">
<div class="filter-group">
<button class="tran-filter-btn active" data-type="all">전체</button>
<%
// serverTypeMap 변수를 가져오기
Map<String, List<DataSourceType>> serverTypeMap =
(Map<String, List<DataSourceType>>)request.getAttribute("serverTypeMap");
if (serverTypeMap == null) {
// application 스코프에서 가져오기 시도
serverTypeMap = (Map<String, List<DataSourceType>>)application.getAttribute("serverTypeMap");
}
Map<String, Integer> priorityMap = new HashMap<>();
priorityMap.put("MCI", 0);
priorityMap.put("EAI", 1);
priorityMap.put("FEP", 2);
priorityMap.put("CGW", 3);
priorityMap.put("FGW", 4);
priorityMap.put("API", 5);
priorityMap.put("APIGW", 6);
priorityMap.put("ONL", 7);
Map<String,List<DataSourceType>> sortedMap = serverTypeMap.entrySet().stream()
.sorted(Comparator.comparingInt(e -> priorityMap.getOrDefault(e.getKey(), Integer.MAX_VALUE)))
.collect(Collectors.toMap(
Map.Entry::getKey
, Map.Entry::getValue
,(e1, e2) -> e1
, LinkedHashMap::new
));
if (sortedMap != null) {
// 서버 타입별 필터 버튼 생성
for (Map.Entry<String, List<DataSourceType>> entry : sortedMap.entrySet()) {
String serverType = entry.getKey();
List<DataSourceType> servers = entry.getValue();
// BAT 서버는 제외
if (DataSourceTypeManager.BAT.equals(serverType) || DataSourceTypeManager.BAP.equals(serverType)) {
continue;
}
String serverTypeText = servers.size() > 0 ? servers.get(0).getText() : serverType;
if( !serverTypeText.endsWith("GW") ) {
%>
<button class="tran-filter-btn" data-type="<%= serverType.toLowerCase() %>"><%= serverTypeText %></button>
<% }
}
} %>
</div>
</div>
<div class="transaction-table-container">
<table class="transaction-table">
<thead>
<tr>
<th style="width: 1%"></th>
<th>업무그룹</th>
<th>정상처리</th>
<th>업무에러</th>
<th>타임아웃</th>
<th>통신에러</th>
</tr>
</thead>
<tbody id="transaction-table-body">
<!-- 거래 처리 데이터가 동적으로 추가됩니다 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- 거래 처리 행 템플릿 -->
<template id="transaction-row-template">
<tr>
<td>
<span class="instance-status status-normal"></span>
</td>
<td class="group-name">업무그룹</td>
<td>
<div class="metric">
<div class="metric-value">
<span class="clickable transaction-count">0</span>
<span class="metric-change transaction-change" style="visibility: hidden;"><i class="bi bi-arrow-up-short"></i>0</span>
</div>
</div>
</td>
<td>
<div class="metric">
<div class="metric-value">
<span class="clickable error-count">0</span>
<span class="metric-change error-change" style="visibility: hidden;"><i class="bi bi-arrow-up-short"></i>0</span>
</div>
</div>
</td>
<td>
<div class="metric">
<div class="metric-value">
<span class="clickable timeout-count">0</span>
<span class="metric-change timeout-change" style="visibility: hidden;"><i class="bi bi-arrow-up-short"></i>0</span>
</div>
</div>
</td>
<td>
<div class="metric">
<div class="metric-value">
<span class="clickable syserror-count">0</span>
<span class="metric-change syserror-change" style="visibility: hidden;"><i class="bi bi-arrow-up-short"></i>0</span>
</div>
</div>
</td>
</tr>
</template>
<script>
// 데이터 로딩 상태 플래그
var isTransactionDataLoading = false;
// 이전 데이터 저장 객체
var previousTransactionData = {};
// 숫자 형식화 함수 (천 단위 콤마 추가)
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// 기존 행 업데이트 함수 수정
function updateExistingRow(row, transactionData) {
// 상태 업데이트
var statusElement = row.querySelector('.instance-status');
statusElement.className = 'instance-status';
// 정상 처리와 오류 비율에 따라 색상 계산
var totalCount = (transactionData.count || 0);
var errorCount = (transactionData.error || 0) + (transactionData.timeout || 0) + (transactionData.syserror || 0);
var totalTransactions = totalCount + errorCount;
// 색상 설정
if (totalTransactions > 0) {
var successRatio = totalCount / totalTransactions;
var statusColor = calculateStatusColor(successRatio);
statusElement.style.backgroundColor = statusColor;
} else {
// 거래가 없는 경우 회색으로 표시
statusElement.style.backgroundColor = '#9e9e9e';
}
// 이전 데이터 가져오기
var prevData = previousTransactionData[transactionData.name] || {
count: 0,
error: 0,
timeout: 0,
syserror: 0
};
// 정상처리 건수 업데이트
var transactionCount = row.querySelector('.transaction-count');
transactionCount.textContent = formatNumber(transactionData.count || 0);
// 증가량 표시
var transactionChange = row.querySelector('.transaction-change');
var countDiff = (transactionData.count || 0) - prevData.count;
if (countDiff > 0) {
transactionChange.innerHTML = '<i class="bi bi-arrow-up-short"></i>' + formatNumber(countDiff);
transactionChange.style.visibility = 'visible';
// 2초 후 사라지게 설정
setTimeout(function() {
transactionChange.style.visibility = 'hidden';
}, 2000);
} else {
transactionChange.style.visibility = 'hidden';
}
// 업무에러 업데이트
var errorCount = row.querySelector('.error-count');
errorCount.textContent = formatNumber(transactionData.error || 0);
// 증가량 표시
var errorChange = row.querySelector('.error-change');
var errorDiff = (transactionData.error || 0) - prevData.error;
if (errorDiff > 0) {
errorChange.innerHTML = '<i class="bi bi-arrow-up-short"></i>' + formatNumber(errorDiff);
errorChange.style.visibility = 'visible';
// 2초 후 사라지게 설정
setTimeout(function() {
errorChange.style.visibility = 'hidden';
}, 2000);
} else {
errorChange.style.visibility = 'hidden';
}
// 타임아웃 업데이트
var timeoutCount = row.querySelector('.timeout-count');
timeoutCount.textContent = formatNumber(transactionData.timeout || 0);
// 증가량 표시
var timeoutChange = row.querySelector('.timeout-change');
var timeoutDiff = (transactionData.timeout || 0) - prevData.timeout;
if (timeoutDiff > 0) {
timeoutChange.innerHTML = '<i class="bi bi-arrow-up-short"></i>' + formatNumber(timeoutDiff);
timeoutChange.style.visibility = 'visible';
// 2초 후 사라지게 설정
setTimeout(function() {
timeoutChange.style.visibility = 'hidden';
}, 2000);
} else {
timeoutChange.style.visibility = 'hidden';
}
// 통신에러 업데이트
var syserrorCount = row.querySelector('.syserror-count');
syserrorCount.textContent = formatNumber(transactionData.syserror || 0);
// 증가량 표시
var syserrorChange = row.querySelector('.syserror-change');
var syserrorDiff = (transactionData.syserror || 0) - prevData.syserror;
if (syserrorDiff > 0) {
syserrorChange.innerHTML = '<i class="bi bi-arrow-up-short"></i>' + formatNumber(syserrorDiff);
syserrorChange.style.visibility = 'visible';
// 2초 후 사라지게 설정
setTimeout(function() {
syserrorChange.style.visibility = 'hidden';
}, 2000);
} else {
syserrorChange.style.visibility = 'hidden';
}
// 현재 데이터를 이전 데이터로 저장
previousTransactionData[transactionData.name] = {
count: transactionData.count || 0,
error: transactionData.error || 0,
timeout: transactionData.timeout || 0,
syserror: transactionData.syserror || 0
};
}
// 거래 행 생성 함수 수정
function createTransactionRow(transactionData) {
var template = document.getElementById('transaction-row-template');
var clone = document.importNode(template.content, true);
// 업무그룹 이름 설정
clone.querySelector('.group-name').textContent = transactionData.name;
// 정상 처리와 오류 비율에 따라 색상 계산
var totalCount = (transactionData.count || 0);
var errorCount = (transactionData.error || 0) + (transactionData.timeout || 0) + (transactionData.syserror || 0);
var totalTransactions = totalCount + errorCount;
// 상태 설정
var statusElement = clone.querySelector('.instance-status');
// 색상 설정
if (totalTransactions > 0) {
var successRatio = totalCount / totalTransactions;
var statusColor = calculateStatusColor(successRatio);
statusElement.style.backgroundColor = statusColor;
} else {
// 거래가 없는 경우 회색으로 표시
statusElement.style.backgroundColor = '#9e9e9e';
}
// 정상처리 건수 설정
clone.querySelector('.transaction-count').textContent = formatNumber(transactionData.count || 0);
// 업무에러 설정
clone.querySelector('.error-count').textContent = formatNumber(transactionData.error || 0);
// 타임아웃 설정
clone.querySelector('.timeout-count').textContent = formatNumber(transactionData.timeout || 0);
// 통신에러 설정
clone.querySelector('.syserror-count').textContent = formatNumber(transactionData.syserror || 0);
// 현재 데이터를 이전 데이터로 저장
previousTransactionData[transactionData.name] = {
count: transactionData.count || 0,
error: transactionData.error || 0,
timeout: transactionData.timeout || 0,
syserror: transactionData.syserror || 0
};
return clone;
}
// 거래 건수 테이블 업데이트 함수
function updateTransactionTable(data) {
if (!data || !data.transactions) return;
var tableBody = document.getElementById('transaction-table-body');
// 합계 값 초기화
var totalCount = 0;
var totalError = 0;
var totalTimeout = 0;
var totalSyserror = 0;
// 현재 테이블에 있는 행들 저장
var currentRows = {}; // 현재 행들을 저장할 객체
// 현재 테이블의 상태를 저장
Array.from(tableBody.querySelectorAll('tr:not(.total-row)')).forEach(function(row) {
var transactionType = row.querySelector('.group-name').textContent;
currentRows[transactionType] = row;
});
// 합계 행 제거 (나중에 다시 추가)
var totalRow = tableBody.querySelector('.total-row');
if (totalRow) {
tableBody.removeChild(totalRow);
}
// 거래 데이터 행 업데이트 또는 추가
data.transactions.forEach(function(transaction) {
// TOTAL 항목은 표에 표시하지 않고 마지막에 별도로 처리
// APIGW는 표시하고, CGW, FGW는 제외
if (transaction.type === 'TOTAL' || (transaction.type.endsWith("GW") && transaction.type !== 'APIGW') ) {
return;
}
// 각 거래별 데이터 설정
var transactionData = {
name: transaction.type,
status: 'normal',
count: transaction.success || 0,
error: transaction.fail || 0,
timeout: transaction.timeout || 0,
syserror: transaction.syserror || 0
};
// 상태 설정: 에러가 많으면 경고 또는 오류 상태로 표시
if ((transaction.fail + transaction.timeout + transaction.syserror) > 50) {
transactionData.status = 'error';
} else if ((transaction.fail + transaction.timeout + transaction.syserror) > 10) {
transactionData.status = 'warning';
}
// 이미 존재하는 행인지 확인하여 업데이트 또는 새로 생성
if (currentRows[transaction.type]) {
updateExistingRow(currentRows[transaction.type], transactionData);
} else {
// 새 행 생성 및 추가
var newRow = createTransactionRow(transactionData);
tableBody.appendChild(newRow);
}
// 합계 계산
totalCount += transactionData.count;
totalError += transactionData.error;
totalTimeout += transactionData.timeout;
totalSyserror += transactionData.syserror;
});
// 사라진 행 제거 (현재 데이터에 없는 행)
var remainingTypes = data.transactions
.filter(t => t.type !== 'TOTAL')
.map(t => t.type);
for (var type in currentRows) {
if (!remainingTypes.includes(type)) {
tableBody.removeChild(currentRows[type]);
}
}
// 합계 행 생성 및 추가
totalRow = document.createElement('tr');
totalRow.className = 'total-row';
// 상태 아이콘 셀
var statusCell = document.createElement('td');
totalRow.appendChild(statusCell);
// 업무그룹 셀
var nameCell = document.createElement('td');
nameCell.className = 'group-name';
nameCell.textContent = '합계';
totalRow.appendChild(nameCell);
// 정상처리 셀
var countCell = document.createElement('td');
var countMetric = document.createElement('div');
countMetric.className = 'metric';
var countValue = document.createElement('div');
countValue.className = 'metric-value';
var countSpan = document.createElement('span');
countSpan.className = 'transaction-count';
countSpan.textContent = formatNumber(totalCount);
// 가짜 metric-change span 추가 (정상처리)
var countChangeSpan = document.createElement('span');
countChangeSpan.className = 'metric-change transaction-change';
countChangeSpan.style.visibility = 'hidden';
countChangeSpan.innerHTML = '<i class="bi bi-arrow-up-short"></i>0';
countValue.appendChild(countSpan);
countValue.appendChild(countChangeSpan); // 가짜 span 추가
countMetric.appendChild(countValue);
countCell.appendChild(countMetric);
totalRow.appendChild(countCell);
// 업무에러 셀
var errorCell = document.createElement('td');
var errorMetric = document.createElement('div');
errorMetric.className = 'metric';
var errorValue = document.createElement('div');
errorValue.className = 'metric-value';
var errorSpan = document.createElement('span');
errorSpan.className = 'error-count';
errorSpan.textContent = formatNumber(totalError);
// 가짜 metric-change span 추가 (업무에러)
var errorChangeSpan = document.createElement('span');
errorChangeSpan.className = 'metric-change error-change';
errorChangeSpan.style.visibility = 'hidden';
errorChangeSpan.innerHTML = '<i class="bi bi-arrow-up-short"></i>0';
errorValue.appendChild(errorSpan);
errorValue.appendChild(errorChangeSpan); // 가짜 span 추가
errorMetric.appendChild(errorValue);
errorCell.appendChild(errorMetric);
totalRow.appendChild(errorCell);
// 타임아웃 셀
var timeoutCell = document.createElement('td');
var timeoutMetric = document.createElement('div');
timeoutMetric.className = 'metric';
var timeoutValue = document.createElement('div');
timeoutValue.className = 'metric-value';
var timeoutSpan = document.createElement('span');
timeoutSpan.className = 'timeout-count';
timeoutSpan.textContent = formatNumber(totalTimeout);
// 가짜 metric-change span 추가 (타임아웃)
var timeoutChangeSpan = document.createElement('span');
timeoutChangeSpan.className = 'metric-change timeout-change';
timeoutChangeSpan.style.visibility = 'hidden';
timeoutChangeSpan.innerHTML = '<i class="bi bi-arrow-up-short"></i>0';
timeoutValue.appendChild(timeoutSpan);
timeoutValue.appendChild(timeoutChangeSpan); // 가짜 span 추가
timeoutMetric.appendChild(timeoutValue);
timeoutCell.appendChild(timeoutMetric);
totalRow.appendChild(timeoutCell);
// 통신에러 셀
var syserrorCell = document.createElement('td');
var syserrorMetric = document.createElement('div');
syserrorMetric.className = 'metric';
var syserrorValue = document.createElement('div');
syserrorValue.className = 'metric-value';
var syserrorSpan = document.createElement('span');
syserrorSpan.className = 'syserror-count';
syserrorSpan.textContent = formatNumber(totalSyserror);
// 가짜 metric-change span 추가 (통신에러)
var syserrorChangeSpan = document.createElement('span');
syserrorChangeSpan.className = 'metric-change syserror-change';
syserrorChangeSpan.style.visibility = 'hidden';
syserrorChangeSpan.innerHTML = '<i class="bi bi-arrow-up-short"></i>0';
syserrorValue.appendChild(syserrorSpan);
syserrorValue.appendChild(syserrorChangeSpan); // 가짜 span 추가
syserrorMetric.appendChild(syserrorValue);
syserrorCell.appendChild(syserrorMetric);
totalRow.appendChild(syserrorCell);
// 테이블에 합계 행 추가
tableBody.appendChild(totalRow);
}
// 트랜잭션 데이터 로드 함수
function loadTransactionData(type) {
// 이미 로딩 중이면 중복 요청 방지
if (isTransactionDataLoading) {
return;
}
// 로딩 상태 플래그 설정
isTransactionDataLoading = true;
// 전역 로딩 이벤트 발생 (DashboardEventManager 사용)
if (window.DashboardEventManager) {
window.DashboardEventManager.trigger('loading-start');
}
// 필터 조건을 파라미터로 API 호출
ModernDashboardApi.getTransactionInfo(type, function(data) {
// 데이터 처리 완료 후 UI 업데이트
// 테이블 업데이트
updateTransactionTable(data);
// 전역 로딩 완료 이벤트 발생
if (window.DashboardEventManager) {
window.DashboardEventManager.trigger('loading-end');
}
// 로딩 상태 플래그 해제
isTransactionDataLoading = false;
});
}
// 트랜잭션 필터 버튼 클릭 이벤트
$('.tran-filter-btn').click(function() {
// 모든 버튼 비활성화
$('.tran-filter-btn').removeClass('active');
// 클릭한 버튼 활성화
$(this).addClass('active');
// 선택한 타입에 따라 트랜잭션 데이터 필터링 로직 추가
var type = $(this).data('type');
loadTransactionData(type);
});
// 실제 서비스에서 표시할 거래 유형 목록 (정렬 순서 포함)
var preferredTransactionOrder = ['MCI', 'EAI', 'FEP', 'MCU', 'APIGW'];
// refreshDashboardData 함수가 호출될 때 거래 처리 테이블이 초기화되지 않도록
// modern-dashboard.js 파일의 refreshDashboardData 함수를 확장합니다.
$(document).ready(function() {
// DashboardEventManager를 사용하여 새로고침 이벤트 구독
if (window.DashboardEventManager) {
window.DashboardEventManager.on('refresh', function() {
// 트랜잭션 정보 조회 - 현재 선택된 필터 사용
var activeType = $('.tran-filter-btn.active').data('type') || 'all';
console.log('거래처리 집계 데이터 갱신 시작: ' + activeType);
loadTransactionData(activeType);
});
// 초기화 완료 이벤트 로깅
console.log('거래처리 집계 컴포넌트 초기화 완료');
}
// 페이지 로드 시 데이터 로드
var activeType = $('.tran-filter-btn.active').data('type') || 'all';
loadTransactionData(activeType);
});
// 성공 비율에 따른 색상 계산 함수
function calculateStatusColor(successRatio) {
// 성공 비율이 0.5(50%)일 때 주황색(#ff9800)
// 성공 비율이 1.0(100%)에 가까울수록 초록색(#4caf50)
// 성공 비율이 0.0(0%)에 가까울수록 빨간색(#e53935)
// 정확히 50%에 가까울 때는 주황색
if (Math.abs(successRatio - 0.5) < 0.05) {
return '#ff9800'; // 주황색 (50:50 비율 근처)
} else if (successRatio > 0.5) {
// 50% ~ 100% 사이: 주황색에서 초록색으로 매우 급격한 변화
// 55% 이상이면 거의 바로 초록색으로 변화
var greenRatio;
if (successRatio > 0.65) {
// 65% 이상이면 완전한 초록색
greenRatio = 1;
} else {
// 50%~65% 사이에서 급격하게 변화
greenRatio = (successRatio - 0.5) * 6.67; // 0.5에서 0.65 사이를 0에서 1로 매핑
}
// 주황색(#ff9800)에서 초록색(#4caf50)으로 보간
var r = Math.round(255 - (255 - 76) * greenRatio);
var g = Math.round(152 + (175 - 152) * greenRatio);
var b = Math.round(0 + (80 - 0) * greenRatio);
return 'rgb(' + r + ',' + g + ',' + b + ')';
} else {
// 0% ~ 50% 사이: 빨간색에서 주황색으로 매우 급격한 변화
// 45% 이하면 거의 바로 빨간색으로 변화
var orangeRatio;
if (successRatio < 0.35) {
// 35% 이하면 완전한 빨간색
orangeRatio = 0;
} else {
// 35%~50% 사이에서 급격하게 변화
orangeRatio = (successRatio - 0.35) * 6.67; // 0.35에서 0.5 사이를 0에서 1로 매핑
}
// 빨간색(#e53935)에서 주황색(#ff9800)으로 보간
var r = Math.round(229 + (255 - 229) * orangeRatio);
var g = Math.round(57 + (152 - 57) * orangeRatio);
var b = Math.round(53 + (0 - 53) * orangeRatio);
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
}
</script>
<style>
/* 트랜잭션 섹션 */
.dashboard-transactions .section-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
flex-shrink: 0;
}
.dashboard-transactions .card {
border-radius: 0.35rem;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
overflow: hidden;
height: 242px;
}
.transaction-filters {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid #e3e6f0;
}
.filter-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.filter-btn:hover {
background-color: #e9ecef;
color: #495057;
}
.filter-btn.active {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.transaction-table-container {
max-height: 300px;
overflow-y: auto;
position: relative;
}
.transaction-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
table-layout: fixed;
}
.transaction-table th {
background-color: #f8f9fc;
font-weight: 600;
text-align: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid #e3e6f0;
position: sticky;
top: 0;
z-index: 10;
}
.transaction-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #f0f0f0;
vertical-align: middle;
}
.transaction-table td:nth-child(1) {
text-align: left;
}
.transaction-table td:nth-child(2) {
text-align: center;
}
.transaction-table td:nth-child(n+3) {
text-align: center;
}
.transaction-table tr:last-child td {
border-bottom: none;
}
.transaction-table tr:hover {
background-color: rgba(0, 0, 0, 0.02);
}
.transaction-table tr.total-row {
background-color: rgba(200, 200, 200, 0.2);
font-weight: 600;
}
.transaction-table tr.total-row td {
border-bottom: 2px solid #9e9e9e;
}
.transaction-table tr.total-row .group-name {
color: #616161;
}
/* 인스턴스 상태 표시 */
.instance-status {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
}
.status-normal {
background-color: #4caf50;
}
.status-warning {
background-color: #ff9800;
}
.status-error {
background-color: #e53935;
}
/* 메트릭 스타일 */
.metric {
display: flex;
flex-direction: column;
position: relative;
min-height: 24px;
}
.metric-value {
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.clickable {
cursor: pointer;
font-weight: 500;
}
.transaction-count {
color: #4caf50;
font-weight: 600;
}
.error-count {
color: #e53935;
font-weight: 600;
}
.timeout-count {
color: #ff9800;
font-weight: 600;
}
.syserror-count {
color: #e53935;
font-weight: 600;
}
.metric-change {
font-size: 0.75rem;
color: #4caf50;
font-weight: 500;
display: inline-flex;
align-items: center;
transition: opacity 0.3s ease;
position: relative;
margin-left: 2px;
white-space: nowrap;
}
.error-change {
color: #e53935;
}
.timeout-change {
color: #ff9800;
}
.syserror-change {
color: #e53935;
}
.metric-change i {
font-size: 1rem;
margin-right: 0;
}
/* 그룹 이름 스타일 */
.group-name {
text-align: center;
}
/* 대시보드 헤더 스타일 */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
/* 제목과 버튼을 한 줄에 나란히 표시하기 위한 컨테이너 */
.header-title-container {
display: flex;
align-items: center;
}
/* 트랜지션 효과 제거 */
#transaction-table-body {
/* transition: opacity 0.2s ease; */
}
</style>
@@ -0,0 +1,275 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.List"%>
<%@ page import="java.util.ArrayList"%>
<%@ page import="java.util.HashMap"%>
<%@ page import="java.util.Map"%>
<%@ page import="com.eactive.eai.rms.common.context.MonitoringContext"%>
<%@ page import="com.eactive.eai.rms.common.util.CommonUtil"%>
<%@ page import="com.eactive.eai.rms.common.datasource.DataSourceType"%>
<%@ page import="com.eactive.eai.rms.common.datasource.DataSourceTypeManager"%>
<%@ page import="com.eactive.eai.common.util.SystemUtil"%>
<%@ page import="javax.persistence.criteria.CriteriaBuilder" %>
<%@ page import="java.util.Comparator" %>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
request.setCharacterEncoding("utf-8");
MonitoringContext monitoringContext = (MonitoringContext)CommonUtil.getBean(request, "monitoringContext");
String NEW_TITLE = (String) session.getAttribute(MonitoringContext.NEW_DASHBOARD_TITLE);
String fepBatchUrl = monitoringContext.getStringProperty("FEP_BATCH_SERVER", "");
String eaiBatchUrl = monitoringContext.getStringProperty("EAI_BATCH_SERVER", "");
// 온라인 데이터 소스 타입 목록 가져오기
List<DataSourceType> srvList = DataSourceTypeManager.getOnlineDataSourceTypes();
// GW 추가
if( DataSourceTypeManager.getDataSourceType("FGW") != null ){
srvList.add(DataSourceTypeManager.getDataSourceType("FGW"));
}
if( DataSourceTypeManager.getDataSourceType("CGW") != null ){
srvList.add(DataSourceTypeManager.getDataSourceType("CGW"));
}
int srvCnt = srvList.size();
// 서버 타입별 목록을 저장할 맵 생성
Map<String, List<DataSourceType>> serverTypeMap = new HashMap<>();
// 서버타입 맵을 application 스코프에 저장 (모든 컴포넌트에서 접근 가능)
application.setAttribute("serverTypeMap", serverTypeMap);
// 서버 타입별로 분류
for (DataSourceType server : srvList) {
String serverName = server.getName();
// 서버 타입에 해당하는 리스트가 없으면 생성
if (!serverTypeMap.containsKey(serverName)) {
serverTypeMap.put(serverName, new ArrayList<DataSourceType>());
}
// 서버를 해당 타입 리스트에 추가
serverTypeMap.get(serverName).add(server);
}
// BAP(일괄전송) 서버 타입 추가
DataSourceType bapServer = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.BAP);
if (bapServer != null && !serverTypeMap.containsKey(DataSourceTypeManager.BAP)) {
List<DataSourceType> bapList = new ArrayList<>();
bapList.add(bapServer);
serverTypeMap.put(DataSourceTypeManager.BAP, bapList);
}
// 서버 데이터 생성 (JavaScript 용)
StringBuilder serverDataJS = new StringBuilder();
serverDataJS.append("var serverDataArray = [");
boolean first = true;
for (Map.Entry<String, List<DataSourceType>> entry : serverTypeMap.entrySet()) {
String serverType = entry.getKey();
List<DataSourceType> servers = entry.getValue();
// BAT 서버는 제외
if (DataSourceTypeManager.BAT.equals(serverType)) {
continue;
}
for (DataSourceType server : servers) {
if (!first) {
serverDataJS.append(",");
}
String name = server.getName();
String text = server.getText();
serverDataJS.append("{");
serverDataJS.append("type:'").append(serverType).append("',");
serverDataJS.append("name:'").append(name).append("',");
serverDataJS.append("text:'").append(text).append("'");
serverDataJS.append("}");
first = false;
}
}
serverDataJS.append("];");
%>
<!DOCTYPE html>
<html lang="ko">
<head>
<title>eLink 모니터링 대시보드</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<!-- 폰트 -->
<jsp:include page="/jsp/common/include/fonts.jsp"/>
<!-- 아이콘 -->
<link href="<%=request.getContextPath()%>/addon/bootstrap-icons-1.11.3/bootstrap-icons.min.css" rel="stylesheet" />
<!-- 커스텀 CSS -->
<link href="<%=request.getContextPath()%>/dashboard-modern/css/main.css" rel="stylesheet" />
<!-- jQuery -->
<script src="<%=request.getContextPath()%>/js/jquery-3.7.1.min.js"></script>
<!-- 커스텀 JavaScript -->
<script src="<%=request.getContextPath()%>/dashboard-modern/js/modern-dashboard-api.js"></script>
<script src="<%=request.getContextPath()%>/dashboard-modern/js/modern-dashboard.js"></script>
<!-- API 초기화 (가장 먼저 실행) -->
<script>
// 페이지 로드 전 API 초기화
ModernDashboardApi.init('<%=request.getContextPath()%>');
</script>
<style>
/* 1번 줄 (4:3:1 비율) - 서버 상태, 배치 처리 상태, 피크 처리 성능 */
.first-row {
grid-template-columns: 4fr 3fr 1fr;
}
/* 2번 줄 (1:1 비율) - 거래 처리 집계, 실시간 트랜잭션 현황 */
.second-row {
grid-template-columns: 1fr 1fr;
}
/* 3번 줄 (7:3 비율) - 어댑터 상태, 장애통보(SMS) */
.third-row {
grid-template-columns: 6fr 4fr;
}
/* 컴팩트한 대시보드를 위한 추가 스타일 */
.container {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
/* 카드 내부 여백 줄임 */
.card {
padding: 0.75rem;
}
/* 테이블 행 높이 줄임 */
table tr td, table tr th {
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
/* 반응형 디자인 */
@media (max-width: 1200px) {
.first-row {
grid-template-columns: 1fr;
}
.second-row {
grid-template-columns: 1fr;
}
.third-row {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<!-- 헤더 영역 -->
<jsp:include page="components/dashboard-header.jsp" />
<!-- 1번 줄 (4:3:1 비율) - 서버 상태, 배치 처리 상태, 피크 처리 성능 -->
<div class="dashboard-row first-row">
<!-- 서버 상태 섹션 (4/8 비율) -->
<div class="server-container">
<jsp:include page="components/dashboard-servers.jsp">
<jsp:param name="serverTypeMap" value="<%= serverTypeMap %>" />
</jsp:include>
</div>
<!-- 배치 처리 상태 섹션 (3/8 비율) -->
<div class="batch-container">
<jsp:include page="components/dashboard-batch.jsp" />
</div>
<!-- 피크 처리 성능 섹션 (1/8 비율) -->
<div class="peak-container">
<jsp:include page="components/dashboard-peak.jsp" />
</div>
</div>
<!-- 2번 줄 (1:1 비율) - 거래 처리 집계, 실시간 트랜잭션 현황 -->
<div class="dashboard-row second-row">
<!-- 거래 처리 집계 섹션 (좌측) -->
<div class="transaction-container" style="overflow-y:auto;">
<jsp:include page="components/dashboard-transactions.jsp">
<jsp:param name="serverTypeMap" value="<%= serverTypeMap %>" />
</jsp:include>
</div>
<!-- 실시간 트랜잭션 현황 섹션 (우측) -->
<div class="transaction-overview-container">
<jsp:include page="components/dashboard-tran-chart.jsp" />
</div>
</div>
<!-- 3번 줄 (8:2 비율) - 어댑터 상태, 장애통보(SMS) -->
<div class="dashboard-row third-row">
<!-- 어댑터 섹션 (8/10 비율) -->
<div class="adapters-container">
<jsp:include page="components/dashboard-adapter.jsp">
<jsp:param name="serverTypeMap" value="<%= serverTypeMap %>" />
</jsp:include>
</div>
<!-- 장애통보(SMS) 섹션 (2/10 비율) -->
<div class="sms-container">
<jsp:include page="components/dashboard-sms.jsp" />
</div>
</div>
<!-- 인터페이스 로그 조회 모달 -->
<jsp:include page="components/dashboard-interface-detail.jsp" />
<!-- 푸터 영역 -->
<jsp:include page="components/dashboard-footer.jsp" />
</div>
<script type="text/javascript">
// 서버 데이터 초기화
<%= serverDataJS.toString() %>
// 자동 새로고침 타이머 변수 (window 객체에 할당)
window.refreshTimer = null;
window.refreshInterval = 5; // 기본값 10초
// 숫자 포맷 유틸리티 함수
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// 서버 데이터 설정 및 대시보드 초기화
$(document).ready(function() {
// 대시보드 초기화
initDashboard('<%=request.getContextPath()%>');
// 현재 시간 표시 및 업데이트
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
// 초기 데이터 로드
refreshDashboardData();
// 로컬 스토리지에서 저장된 새로고침 간격 불러오기
if (localStorage.getItem('dashboardRefreshInterval')) {
window.refreshInterval = parseInt(localStorage.getItem('dashboardRefreshInterval'));
// 저장된 간격에 해당하는 버튼 활성화
$('.refresh-btn').removeClass('active');
$('.refresh-btn[data-interval="' + window.refreshInterval + '"]').addClass('active');
}
// modern-dashboard.js의 함수 호출
window.startRefreshTimer();
window.updateRefreshStatus();
});
</script>
</body>
</html>
@@ -233,4 +233,16 @@ public class AdapterGroupService extends AbstractDataService<AdapterGroup, Strin
return query.fetch();
}
public List<AdapterGroup> findByAdptrinsticodeNotEmpty() {
QAdapterGroup qAdapterGroup = QAdapterGroup.adapterGroup;
JPAQuery<AdapterGroup> query = getJPAQueryFactory()
.selectFrom(qAdapterGroup)
.where(qAdapterGroup.adptrinsticode.isNotNull()
.and(qAdapterGroup.adptrinsticode.ne("")))
.orderBy(qAdapterGroup.adptrbzwkgroupname.asc());
return query.fetch();
}
}
@@ -1,12 +1,10 @@
package com.eactive.eai.rms.onl.dashboard.controller;
import java.util.HashMap;
import java.util.Vector;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.onl.dashboard.service.DashboardErrorService;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -15,11 +13,11 @@ import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.onl.dashboard.service.DashboardErrorService;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Vector;
@Controller
public class DashboardErrorController {
@@ -1,29 +1,5 @@
package com.eactive.eai.rms.onl.dashboard.controller;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.View;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
@@ -34,11 +10,23 @@ import com.eactive.eai.rms.onl.dashboard.dao.DashboardDAO;
import com.eactive.eai.rms.onl.dashboard.service.Dashboard2RepositoryService;
import com.eactive.eai.rms.onl.dashboard.service.DashboardService;
import com.eactive.eai.rms.onl.tracking.vo.MemStaticsVo;
import com.eactive.eai.rms.onl.vo.AdaptersVO;
import com.eactive.eai.rms.onl.vo.EAIEngineStatusVo;
import com.eactive.eai.rms.onl.vo.HostStatusVo;
import com.eactive.eai.rms.onl.vo.RealTimeAdaptersVO;
import com.eactive.eai.rms.onl.vo.SmsVO;
import com.eactive.eai.rms.onl.vo.*;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.View;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
@Controller
@@ -1,12 +1,11 @@
package com.eactive.eai.rms.onl.dashboard.dao;
import java.util.HashMap;
import java.util.Vector;
import org.springframework.stereotype.Repository;
import com.eactive.eai.rms.common.base.SqlMapClientTemplateDao;
import com.ibatis.sqlmap.client.event.RowHandler;
import org.springframework.stereotype.Repository;
import java.util.HashMap;
import java.util.Vector;
@Repository("dashboardDao")
public class DashboardDaoImpl extends SqlMapClientTemplateDao
@@ -1,14 +1,5 @@
package com.eactive.eai.rms.onl.dashboard.dao;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.eactive.eai.rms.common.base.SqlMapClientTemplateDao;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
@@ -16,6 +7,14 @@ import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.SpecifiedDataSourceExecutor;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.ibatis.sqlmap.client.event.RowHandler;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;
@Repository("dashboardError")
@SuppressWarnings("unchecked")
@@ -0,0 +1,78 @@
package com.eactive.eai.rms.onl.dashboard.modern.backup;
import com.eactive.eai.rms.onl.dashboard.modern.service.ThroughputCalculationService;
import com.eactive.eai.rms.onl.tracking.vo.DbStaticsVo;
import com.eactive.eai.rms.onl.tracking.vo.MemStaticsVo;
import com.eactive.eai.rms.onl.vo.MonitorDataVo;
import com.eactive.eai.rms.onl.vo.SmsVO;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Getter
@Setter
public class BackupContentVO implements Serializable {
private String backupTimestamp;
private RepositoryFields repositoryFields;
private DashboardRepository2Fields dashboardRepository2Fields;
// private ThroughputFields throughputFields; 백업 필요 없는 저장소
public BackupContentVO() {
this.backupTimestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
}
@Getter
@Setter
@NoArgsConstructor
public static class RepositoryFields implements Serializable {
private Map<String, MonitorDataVo> repository;
private ConcurrentHashMap<String, MonitorDataVo> repositoryNoExpire;
}
@Getter
@Setter
@NoArgsConstructor
public static class DashboardRepository2Fields implements Serializable {
private Map<String, Map<String, String>> repoEAISevrDstcd;
private Map<String, MemStaticsVo> repoMemTransaction;
private Map<String, DbStaticsVo> repository;
private Map<String, List<String>> repoHostNames;
private Map<String, List<String>> repoInstNames;
private Map<String, Map<String, List<String>>> repoDataSourceInstNames;
private Map<String, String> repoHostAlive;
private List<SmsVO> repoSms;
private Map<String, String> repoPeakTps;
private Map<String, String> repoPeakTrade;
private int[] procBatch = new int[6];
private int[] procEaiBatch = new int[6];
private Map<String, Map<String, String>> itsmDashTranInfo;
private Map<String, Map<String, String>> itsmDashPeakdayInfo;
private Map<String, List> itsmDashDayTranList;
private Map<String, List> itsmDashHourTranList;
private Map<String, Map<String, String>> itsmDashBizErrorMap;
private Map<String, Map<String, String>> itsmDashTimeoutErrorMap;
private Map<String, Map<String, String>> itsmDashConnectErrorMap;
private Map<String, Map<String, String>> itsmDashServerInstTotalProcMap;
}
@Getter
@Setter
@NoArgsConstructor
public static class ThroughputFields {
private Map<String, ThroughputCalculationService.TimeSeriesData> timeSeriesMap;
private Map<String, ThroughputCalculationService.ServerResourceData> serverResourceMap;
private Map<String, Map<String, Integer>> lastAccumulatedValuesMap;
}
}
@@ -0,0 +1,28 @@
package com.eactive.eai.rms.onl.dashboard.modern.backup;
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Slf4j
@Controller
@RequiredArgsConstructor
public class BackupController implements InterceptorSkipController {
private final RepositoryBackupService repositoryBackupService;
@GetMapping("/removeBackupFile.do")
public ResponseEntity<String> removeBackupFile() {
try {
repositoryBackupService.removeBackupFile();
return ResponseEntity.ok("OK");
} catch (Exception e) {
log.error("FILE REMOVE FAIL", e);
return ResponseEntity.internalServerError().body(e.getMessage());
}
}
}
@@ -0,0 +1,17 @@
package com.eactive.eai.rms.onl.dashboard.modern.backup;
import com.fasterxml.jackson.annotation.JsonIgnore;
public abstract class MemStaticsVoMixIn {
@JsonIgnore
public abstract int getProcTotal();
@JsonIgnore
public abstract int getErrorTotal();
@JsonIgnore
public abstract int getSysErrorTotal();
@JsonIgnore
public abstract int getTimeoutTotal();
}
@@ -0,0 +1,13 @@
package com.eactive.eai.rms.onl.dashboard.modern.backup;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(
use = JsonTypeInfo.Id.CLASS,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "category",
visible = true
)
public abstract class MonitorDataVoMixIn {
}
@@ -0,0 +1,9 @@
package com.eactive.eai.rms.onl.dashboard.modern.backup;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public abstract class MonitorVOMixin {
@JsonCreator
public MonitorVOMixin(@JsonProperty("eaiServiceCode") String eaiServiceCode) {}
}
@@ -0,0 +1,177 @@
package com.eactive.eai.rms.onl.dashboard.modern.backup;
import com.eactive.eai.common.monitor.MonitorVO;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.onl.dashboard.modern.backup.mapper.Dashboard2RepositoryVoMapper;
import com.eactive.eai.rms.onl.dashboard.modern.backup.mapper.RepositoryVoMapper;
import com.eactive.eai.rms.onl.dashboard.modern.backup.mapper.ThroughputVoMapper;
import com.eactive.eai.rms.onl.dashboard.repository.Dashboard2Repository;
import com.eactive.eai.rms.onl.server.repository.Repository;
import com.eactive.eai.rms.onl.tracking.vo.MemStaticsVo;
import com.eactive.eai.rms.onl.vo.MonitorDataVo;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@RequiredArgsConstructor
public class RepositoryBackupService implements ApplicationListener<ContextRefreshedEvent> {
@Value("${inst.Type:ems}")
private String instType;
@Value("${inst.Name:ems-0}")
private String instName;
private static final DateTimeFormatter BACKUP_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private final Repository repository;
private final Dashboard2Repository dashboard2Repository;
// private final ThroughputCalculationService throughputCalculationService;
@Autowired
@Qualifier("monitoringContext")
private final MonitoringContext monitoringContext;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ObjectMapper objectMapper =
new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.addMixIn(MonitorDataVo.class, MonitorDataVoMixIn.class)
.addMixIn(MonitorVO.class, MonitorVOMixin.class)
.addMixIn(MemStaticsVo.class, MemStaticsVoMixIn.class);
private final RepositoryVoMapper rMapper = new RepositoryVoMapper();
private final Dashboard2RepositoryVoMapper d2rMapper = new Dashboard2RepositoryVoMapper();
private final ThroughputVoMapper tMapper = new ThroughputVoMapper();
private boolean readFlag = false;
private boolean backupFlag = true;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
String repositoryBackupEnable = monitoringContext.getProperty("repository.backup.enable", "false");
backupFlag = BooleanUtils.toBoolean(repositoryBackupEnable);
if (!backupFlag) {
return;
}
try {
// 백업 디렉토리와 경로 생성
String backupDirectory = monitoringContext.getProperty("repository.backup.path", "/log/elink/");
final Path backupFilePath = Paths.get(backupDirectory, instType, instName, "repository", "backup.json");
// 파일이 존재하고 아직 읽지 않은 경우에 진행
synchronized (this) {
if (Files.exists(backupFilePath) && !readFlag) {
String json = new String(Files.readAllBytes(backupFilePath), StandardCharsets.UTF_8);
if (StringUtils.isNotBlank(json)) {
BackupContentVO backupContentVO = objectMapper.readValue(json, BackupContentVO.class);
LocalDateTime fileTime = LocalDateTime.parse(backupContentVO.getBackupTimestamp(), BACKUP_TIMESTAMP_FORMATTER);
// 백업 파일의 날짜가 오늘보다 이전이면 삭제, 그렇지 않으면 복원 수행
if (fileTime.toLocalDate().isBefore(LocalDate.now())) {
synchronized (this) {
Files.delete(backupFilePath);
}
} else {
rMapper.setVoValuesToObject(backupContentVO.getRepositoryFields(), repository);
d2rMapper.setVoValuesToObject(backupContentVO.getDashboardRepository2Fields(), dashboard2Repository);
// tMapper.setVoValuesToObject(backupContentVO.getThroughputFields(), throughputCalculationService);
readFlag = true;
}
}
}
}
// 10분마다 BackupContentVO 생성해서 저장함
scheduler.scheduleAtFixedRate(() -> {
if (!backupFlag)
return;
log.info("Repository Data Backup");
try {
// VO
BackupContentVO backupContentVO = new BackupContentVO();
backupContentVO.setRepositoryFields(rMapper.toVo(repository));
backupContentVO.setDashboardRepository2Fields(d2rMapper.toVo(dashboard2Repository));
// backupContentVO.setThroughputFields(tMapper.toVo(throughputCalculationService));
synchronized (this) {
// File Write
if (!Files.exists(backupFilePath)) {
Files.createDirectories(backupFilePath.getParent());
Files.createFile(backupFilePath);
}
Files.write(backupFilePath, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(backupContentVO));
}
} catch (Exception e) {
log.error("Repository Backup 데이터 생성 중 오류가 발생했습니다.", e);
}
}, 3, 10, TimeUnit.MINUTES);
} catch (Exception e) {
log.error("Repository Backup 시작 설정 중 오류가 발생했습니다.", e);
}
}
@PreDestroy
public void destroy() {
this.backupFlag = false;
}
// 매일 0시에 실행
@Scheduled(cron = "0 0 0 * * ?")
private void dailyCleanUp() {
removeBackupFile();
}
public synchronized void removeBackupFile() {
String backupFilePathStr = monitoringContext.getProperty("repository.backup.path", "/log/elink/repository/");
backupFilePathStr = backupFilePathStr.endsWith("/") ? backupFilePathStr : backupFilePathStr + "/";
backupFilePathStr += instType + "/" + instName + "/backup.json";
final Path backupFilePath = Paths.get(backupFilePathStr);
if (Files.exists(backupFilePath)) {
try {
Files.delete(backupFilePath);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
@@ -0,0 +1,31 @@
package com.eactive.eai.rms.onl.dashboard.modern.backup.mapper;
import java.lang.reflect.Field;
public interface BackupMapper<VO, OBJ> {
VO toVo(OBJ object) throws Exception;
void setVoValuesToObject(VO vo, OBJ object) throws Exception;
default <T> T getReflection(Class<?> clazz, String fieldName, Object instance) {
try {
Field declaredField = clazz.getDeclaredField(fieldName);
declaredField.setAccessible(true);
return (T) declaredField.get(instance);
} catch (Exception e) {
throw new RuntimeException("Error occurred during field access", e);
}
}
default void setReflection(Class<?> clazz, String fieldName, Object instance, Object value) {
try {
Field declaredField = clazz.getDeclaredField(fieldName);
declaredField.setAccessible(true);
declaredField.set(instance, value);
} catch (Exception e) {
throw new RuntimeException("Error occurred during field access", e);
}
}
}
@@ -0,0 +1,90 @@
package com.eactive.eai.rms.onl.dashboard.modern.backup.mapper;
import com.eactive.eai.rms.common.util.TypeCastUtils;
import com.eactive.eai.rms.onl.dashboard.modern.backup.BackupContentVO;
import com.eactive.eai.rms.onl.dashboard.repository.Dashboard2Repository;
import org.apache.commons.collections.keyvalue.MultiKey;
import org.apache.commons.collections.map.MultiKeyMap;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class Dashboard2RepositoryVoMapper implements BackupMapper<BackupContentVO.DashboardRepository2Fields, Dashboard2Repository> {
@Override
public BackupContentVO.DashboardRepository2Fields toVo(Dashboard2Repository dashboard2Repository) throws Exception {
BackupContentVO.DashboardRepository2Fields dashboardRepository2Fields = new BackupContentVO.DashboardRepository2Fields();
for (Field field : BackupContentVO.DashboardRepository2Fields.class.getDeclaredFields()) {
Method method = Arrays.stream(BackupContentVO.DashboardRepository2Fields.class.getDeclaredMethods()).filter(m -> m.getName().equalsIgnoreCase("set" + field.getName())).findAny().orElse(null);
if (method != null) {
if ("repoMemTransaction".equalsIgnoreCase(field.getName()) || "repository".equalsIgnoreCase(field.getName())) {
method.invoke(dashboardRepository2Fields, multiKeyMapToNormalMap(getReflection(Dashboard2Repository.class, field.getName(), dashboard2Repository)));
} else {
method.invoke(dashboardRepository2Fields, TypeCastUtils.castOrElse(getReflection(Dashboard2Repository.class, field.getName(), dashboard2Repository), field.getType(), null));
}
}
}
return dashboardRepository2Fields;
}
@Override
public void setVoValuesToObject(BackupContentVO.DashboardRepository2Fields vo, Dashboard2Repository object) throws Exception {
for (Field field : BackupContentVO.DashboardRepository2Fields.class.getDeclaredFields()) {
Method method = Arrays.stream(BackupContentVO.DashboardRepository2Fields.class.getDeclaredMethods()).filter(m -> m.getName().equalsIgnoreCase("get" + field.getName())).findAny().orElse(null);
if (method != null) {
if ("repoMemTransaction".equalsIgnoreCase(field.getName()) || "repository".equalsIgnoreCase(field.getName())) {
setReflection(Dashboard2Repository.class, field.getName(), object, normalMapToMultiKeyMap(TypeCastUtils.castOrElse(method.invoke(vo), Map.class, null)));
} else {
setReflection(Dashboard2Repository.class, field.getName(), object, TypeCastUtils.castOrElse(method.invoke(vo), field.getType(), null));
}
}
}
}
private <V> Map<String, V> multiKeyMapToNormalMap(MultiKeyMap multiKeyMap) {
if (multiKeyMap == null)
return null;
Map<String, V> normalMap = new HashMap<>();
for (Object key : multiKeyMap.keySet()) {
// MultiKey에서 배열 가져오기
MultiKey multiKey = (MultiKey) key;
Object[] keys = multiKey.getKeys();
// 키를 "_" join하여 하나의 문자열로 합성
String combinedKey = Arrays.stream(keys)
.map(Object::toString)
.collect(Collectors.joining("_"));
// 합성된 , -> normalMap
normalMap.put(combinedKey, (V) multiKeyMap.get(key));
}
return normalMap;
}
public static <V> MultiKeyMap normalMapToMultiKeyMap(Map<String, V> normalMap) {
if (normalMap == null)
return null;
MultiKeyMap multiKeyMap = new MultiKeyMap();
for (Map.Entry<String, V> entry : normalMap.entrySet()) {
String combinedKey = entry.getKey();
V value = entry.getValue();
// "_" 분리하여 subKeys 배열을 얻음
String[] subKeys = combinedKey.split("_");
multiKeyMap.put(new MultiKey(subKeys), value);
}
return multiKeyMap;
}
}
@@ -0,0 +1,79 @@
package com.eactive.eai.rms.onl.dashboard.modern.backup.mapper;
import com.eactive.eai.rms.onl.dashboard.modern.backup.BackupContentVO;
import com.eactive.eai.rms.onl.server.repository.ExpiringMap;
import com.eactive.eai.rms.onl.server.repository.Repository;
import com.eactive.eai.rms.onl.vo.MonitorDataVo;
import org.apache.commons.collections.keyvalue.MultiKey;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public class RepositoryVoMapper implements BackupMapper<BackupContentVO.RepositoryFields, Repository> {
@Override
public BackupContentVO.RepositoryFields toVo(Repository repository) throws Exception {
BackupContentVO.RepositoryFields result = new BackupContentVO.RepositoryFields();
Field fRepository = Repository.class.getDeclaredField("repository");
fRepository.setAccessible(true);
ExpiringMap<MultiKey, MonitorDataVo> vRepository = (ExpiringMap<MultiKey, MonitorDataVo>) fRepository.get(repository);
Map<String, MonitorDataVo> v2Repository = multiKeyToNormalMap(vRepository);
result.setRepository(v2Repository);
Field fRepositoryNoExpire = Repository.class.getDeclaredField("repositoryNoExpire");
fRepositoryNoExpire.setAccessible(true);
ConcurrentHashMap<MultiKey, MonitorDataVo> vRepositoryNoExpire = (ConcurrentHashMap<MultiKey, MonitorDataVo>) fRepositoryNoExpire.get(repository);
ConcurrentHashMap<String, MonitorDataVo> v2RepositoryNoExpire = multiKeyToNormalMap(vRepositoryNoExpire);
result.setRepositoryNoExpire(v2RepositoryNoExpire);
return result;
}
@Override
public void setVoValuesToObject(BackupContentVO.RepositoryFields vo, Repository object) throws Exception {
ExpiringMap<MultiKey, Object> vRepository = new ExpiringMap<>();
vo.getRepository().entrySet().stream()
.forEach(e -> {
String[] splitted = e.getKey().split("_");
vRepository.put(new MultiKey(splitted), e.getValue());
});
Field fRepository = Repository.class.getDeclaredField("repository");
fRepository.setAccessible(true);
fRepository.set(object, vRepository);
ConcurrentHashMap<MultiKey, Object> vRepositoryNoExpire = new ConcurrentHashMap<>();
vo.getRepositoryNoExpire().entrySet().stream()
.forEach(e -> {
String[] splitted = e.getKey().split("_");
vRepositoryNoExpire.put(new MultiKey(splitted), e.getValue());
});
Field fRepositoryNoExpire = Repository.class.getDeclaredField("repositoryNoExpire");
fRepositoryNoExpire.setAccessible(true);
fRepositoryNoExpire.set(object, vRepositoryNoExpire);
}
private <T> ConcurrentHashMap<String, T> multiKeyToNormalMap(Map<MultiKey, T> vRepository) {
if (vRepository == null)
return null;
return vRepository.entrySet().stream()
.collect(Collectors.toMap(
e -> Arrays.stream(e.getKey().getKeys())
.map(Object::toString)
.collect(Collectors.joining("_")),
Map.Entry::getValue, (oldVal, newVal) -> newVal, ConcurrentHashMap::new
));
}
}
@@ -0,0 +1,33 @@
package com.eactive.eai.rms.onl.dashboard.modern.backup.mapper;
import com.eactive.eai.rms.common.util.TypeCastUtils;
import com.eactive.eai.rms.onl.dashboard.modern.backup.BackupContentVO;
import com.eactive.eai.rms.onl.dashboard.modern.service.ThroughputCalculationService;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
public class ThroughputVoMapper implements BackupMapper<BackupContentVO.ThroughputFields, ThroughputCalculationService> {
@Override
public BackupContentVO.ThroughputFields toVo(ThroughputCalculationService throughputCalculationService) throws Exception {
BackupContentVO.ThroughputFields throughputFields = new BackupContentVO.ThroughputFields();
for (Field field : BackupContentVO.ThroughputFields.class.getDeclaredFields()) {
Method method = Arrays.stream(BackupContentVO.ThroughputFields.class.getDeclaredMethods()).filter(m -> m.getName().equalsIgnoreCase("set" + field.getName())).findAny().orElse(null);
if (method != null)
method.invoke(throughputFields, TypeCastUtils.castOrElse(getReflection(ThroughputCalculationService.class, field.getName(), throughputCalculationService), field.getType(), null));
}
return throughputFields;
}
@Override
public void setVoValuesToObject(BackupContentVO.ThroughputFields vo, ThroughputCalculationService object) throws Exception {
for (Field field : BackupContentVO.ThroughputFields.class.getDeclaredFields()) {
Method method = Arrays.stream(BackupContentVO.ThroughputFields.class.getDeclaredMethods()).filter(m -> m.getName().equalsIgnoreCase("get" + field.getName())).findAny().orElse(null);
if (method != null)
setReflection(ThroughputCalculationService.class, field.getName(), object, TypeCastUtils.castOrElse(method.invoke(vo), field.getType(), null));
}
}
}
@@ -0,0 +1,955 @@
package com.eactive.eai.rms.onl.dashboard.modern.controller;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.data.entity.onl.property.PropertyService;
import com.eactive.eai.rms.onl.common.service.EaiServerInfoService;
import com.eactive.eai.rms.onl.dashboard.dao.DashboardDAO;
import com.eactive.eai.rms.onl.dashboard.modern.dto.*;
import com.eactive.eai.rms.onl.dashboard.modern.mapper.DashboardDataMapper;
import com.eactive.eai.rms.onl.dashboard.modern.mapper.SmsNotificationMapper;
import com.eactive.eai.rms.onl.dashboard.modern.service.SocketMonitorService;
import com.eactive.eai.rms.onl.dashboard.modern.service.ThroughputCalculationService;
import com.eactive.eai.rms.onl.dashboard.modern.service.TransactionSummaryService;
import com.eactive.eai.rms.onl.dashboard.service.Dashboard2RepositoryService;
import com.eactive.eai.rms.onl.dashboard.service.DashboardService;
import com.eactive.eai.rms.onl.tracking.vo.MemStaticsVo;
import com.eactive.eai.rms.onl.vo.AdaptersVO;
import com.eactive.eai.rms.onl.vo.EAIEngineStatusVo;
import com.eactive.eai.rms.onl.vo.HostStatusVo;
import com.eactive.eai.rms.onl.vo.SmsVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* 모던 대시보드 컨트롤러
* ASIS 대시보드에서 TOBE 대시보드로 마이그레이션된 API를 제공합니다.
*/
@Slf4j
@Controller
public class ModernDashboardController {
@Autowired
@Qualifier("monitoringContext")
private transient MonitoringContext monitoringContext;
@Autowired
@Qualifier("eaiServerInfoService")
private EaiServerInfoService eaiServerInfoService;
@Autowired
@Qualifier("dashboardService")
private DashboardService dashboardService;
@Autowired
@Qualifier("dashboard2RepositoryService")
private Dashboard2RepositoryService dashboard2Repository;
@Autowired
@Qualifier("dashboardDao")
private DashboardDAO dashboardDao;
@Autowired
private DashboardDataMapper dataMapper;
@Autowired
private SmsNotificationMapper smsNotificationMapper;
@Autowired
private ThroughputCalculationService throughputCalculationService;
@Autowired
private SocketMonitorService socketMonitorService;
@Autowired
private TransactionSummaryService transactionSummaryService;
@Autowired
@Qualifier("propertyService")
private PropertyService propertyService;
/**
* 모던 대시보드 메인 페이지
*/
@RequestMapping(path = "/modern-dashboard.do")
public String modernDashboard(HttpServletRequest request) {
return "dashboard-modern/modern-dashboard";
}
/**
* 서버 상태 정보 제공 API
* 전체 서버 해당 인스턴스의 상태 정보를 제공합니다.
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=server-status")
@ResponseBody
public Map<String, Object> getServerStatus(@RequestParam(required = false) String serverName) {
Map<String, Object> response = new HashMap<>();
try {
// 서버 타입 목록 가져오기 (특정 서버 이름이 제공된 경우 해당 서버만 포함)
List<DataSourceType> serverTypes;
if (serverName != null && !serverName.isEmpty() && !serverName.equals("all") && !serverName.equals("ALL")) {
DataSourceType server = DataSourceTypeManager.getDataSourceType(serverName.toUpperCase());
serverTypes = server != null ? Collections.singletonList(server) : new ArrayList<>();
} else {
serverTypes = DataSourceTypeManager.getOnlineDataSourceTypes();
// GW 추가
if( DataSourceTypeManager.getDataSourceType("FGW") != null ){
serverTypes.add(DataSourceTypeManager.getDataSourceType("FGW"));
}
if( DataSourceTypeManager.getDataSourceType("CGW") != null ){
serverTypes.add(DataSourceTypeManager.getDataSourceType("CGW"));
}
}
List<ServerStatusDTO> serverList = new ArrayList<>();
for (DataSourceType serverType : serverTypes) {
// 서버 컨텍스트 설정
DataSourceContextHolder.setDataSourceType(serverType);
String agentMode = propertyService.getPrpty2valById("RMIInfo", "agent.mode");
if( agentMode == null || agentMode.isEmpty() ){
agentMode = "";
}
// 호스트명으로 인스턴스 그룹화
Map<String, List<Map<String, String>>> instancesByHost = new HashMap<String, List<Map<String, String>>>();
if( "K8S".equals(agentMode) ){
List<Map<String, String>> hostList = dashboard2Repository.getDatasourceByHost(serverType.getName());
for (Map<String, String> instance : hostList) {
String hostname = instance.get("HOSTNAME");
instancesByHost.computeIfAbsent(hostname, k -> new ArrayList<>()).add(instance);
}
} else {
// 서버에 대한 인스턴스 목록 조회
List<Map<String, String>> ipList = eaiServerInfoService.getEaiServerIpListDB();
for (Map<String, String> instance : ipList) {
String hostname = instance.get("HOSTNAME");
instancesByHost.computeIfAbsent(hostname, k -> new ArrayList<>()).add(instance);
}
}
// 호스트별로 서버 상태 DTO 생성
for (Map.Entry<String, List<Map<String, String>>> entry : instancesByHost.entrySet()) {
String hostname = entry.getKey();
List<Map<String, String>> hostInstances = entry.getValue();
// 서버 상태 DTO 생성
ServerStatusDTO serverDTO = new ServerStatusDTO();
serverDTO.setName(hostname);
serverDTO.setText(hostname + " (" + serverType.getText() + ")");
serverDTO.setHostname(hostname);
// 호스트 상태 조회 리소스 사용률 설정
HostStatusVo hostVo = dashboardService.findHostStatus(hostInstances);
if (hostVo != null) {
serverDTO.setCpu(hostVo.getCpu());
serverDTO.setMemory(hostVo.getMemory());
serverDTO.setDisk(hostVo.getDisk());
}
// 서버 상태 설정
if (serverDTO.getCpu() > 90 || serverDTO.getMemory() > 90 || serverDTO.getDisk() > 90) {
serverDTO.setStatus("error");
} else if (serverDTO.getCpu() > 70 || serverDTO.getMemory() > 70 || serverDTO.getDisk() > 70) {
serverDTO.setStatus("warning");
} else {
serverDTO.setStatus("normal");
}
// 인스턴스 목록 조회 설정
List<EAIEngineStatusVo> engineStatusList = dashboardService.findEngineStatus(hostInstances, true);
List<InstanceDTO> instances = new ArrayList<>();
for (EAIEngineStatusVo engineStatus : engineStatusList) {
InstanceDTO instance = new InstanceDTO();
instance.setId(engineStatus.getInstanceName());
String serverState = StringUtils.defaultString(engineStatus.getServerState());
// 인스턴스 상태 설정
switch (serverState) {
case "0":
instance.setStatus("error");
break;
case "1":
instance.setStatus("warning");
break;
case "2":
instance.setStatus("normal");
break;
default:
instance.setStatus("inactive");
}
// 처리량 설정 (초당 처리량)
MemStaticsVo memStatics = dashboard2Repository.getMemTradeVoByHost(
engineStatus.getHostName(), engineStatus.getInstanceName());
if (memStatics != null) {
// 처리량 계산 서비스에서 초당 처리량 가져오기
instance.setThroughput(throughputCalculationService.getThroughput(
engineStatus.getHostName(), engineStatus.getInstanceName()));
} else {
instance.setThroughput("0/s");
}
instances.add(instance);
}
serverDTO.setInstances(instances);
serverList.add(serverDTO);
}
}
response.put("servers", serverList);
return response;
} catch (Exception e) {
log.error("서버 상태 정보 조회 중 오류 발생", e);
response.put("error", "서버 상태 정보 조회 중 오류가 발생했습니다.");
return response;
}
}
/**
* 어댑터 상태 정보 제공 API
* 서버별 어댑터(Socket, HTTP) 상태 정보를 제공합니다.
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=adapter-status")
@ResponseBody
public Map<String, Object> getAdapterStatus(@RequestParam(required = false) String serverName) {
Map<String, Object> response = new HashMap<>();
try {
// 서버 타입 목록 가져오기 (특정 서버 이름이 제공된 경우 해당 서버만 포함)
List<DataSourceType> servers;
if (serverName != null && !serverName.isEmpty() && !serverName.equals("all") && !serverName.equals("ALL")) {
DataSourceType server = DataSourceTypeManager.getDataSourceType(serverName.toUpperCase());
servers = server != null ? Collections.singletonList(server) : new ArrayList<>();
} else {
// 온라인 데이터 소스 타입 가져오기 (BAT 서버 제외)
servers = DataSourceTypeManager.getOnlineDataSourceTypes().stream()
.filter(server -> !DataSourceTypeManager.BAT.equals(server.getName()))
.collect(Collectors.toList());
// GW 추가
if( DataSourceTypeManager.getDataSourceType("FGW") != null ){
servers.add(DataSourceTypeManager.getDataSourceType("FGW"));
}
if( DataSourceTypeManager.getDataSourceType("CGW") != null ){
servers.add(DataSourceTypeManager.getDataSourceType("CGW"));
}
}
List<Map<String, Object>> serverAdapters = new ArrayList<>();
for (DataSourceType server : servers) {
// 서버 컨텍스트 설정
DataSourceContextHolder.setDataSourceType(server);
// 서버에 대한 인스턴스 목록 조회
List<Map<String, String>> ipList = eaiServerInfoService.getEaiServerIpListDB();
// 어댑터 상태 목록 조회 (개별 인스턴스별 상태)
List<AdaptersVO> adaptersList = dashboardService.findAdapterStatusList(ipList);
// 어댑터 상태 집계 정보 생성
AdapterStatusSummaryContainerDTO summaryContainer = dataMapper.fromAdaptersVOList(adaptersList);
// 기존 방식의 어댑터 상태 정보도 함께 제공 (하위 호환성 유지)
AdaptersVO adaptersVO = dashboardService.findAdapterStatusTotalList(ipList);
List<AdapterStatusDTO> adapterDTOs = dataMapper.fromAdaptersVO(adaptersVO);
Map<String, Object> serverAdapter = new HashMap<>();
serverAdapter.put("serverName", server.getName());
serverAdapter.put("serverText", server.getText());
serverAdapter.put("adapters", adapterDTOs);
serverAdapter.put("adapterSummaries", summaryContainer.getAdapterSummaryList());
serverAdapter.put("overallStatus", summaryContainer.getOverallStatus());
serverAdapters.add(serverAdapter);
}
response.put("serverAdapters", serverAdapters);
return response;
} catch (Exception e) {
log.error("어댑터 상태 정보 조회 중 오류 발생", e);
response.put("error", "어댑터 상태 정보 조회 중 오류가 발생했습니다.");
return response;
}
}
/**
* 트랜잭션 정보 제공 API
* 서버별 트랜잭션 처리 현황(성공, 실패, 타임아웃, 시스템 에러) 제공합니다.
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=transaction")
@ResponseBody
public Map<String, Object> getTransactionInfo(@RequestParam(required = false) String serverName) {
Map<String, Object> response = new HashMap<>();
try {
// 서버 타입 목록 가져오기 (특정 서버 이름이 제공된 경우 해당 서버만 포함)
List<DataSourceType> servers;
if (serverName != null && !serverName.isEmpty() && !serverName.equals("all") && !serverName.equals("ALL")) {
DataSourceType server = DataSourceTypeManager.getDataSourceType(serverName.toUpperCase());
servers = server != null ? Collections.singletonList(server) : new ArrayList<>();
} else {
servers = DataSourceTypeManager.getOnlineDataSourceTypes();
}
List<TransactionDTO> transactions = new ArrayList<>();
TransactionDTO totalTransaction = new TransactionDTO();
totalTransaction.setType("TOTAL");
for (DataSourceType server : servers) {
// 서버 컨텍스트 설정
DataSourceContextHolder.setDataSourceType(server);
// 서버의 트랜잭션 정보 조회
MemStaticsVo memStatics = dashboard2Repository.getMemTradeVoByService(server.getName());
TransactionDTO transaction = dataMapper.fromMemStaticsVo(memStatics, server.getName());
transactions.add(transaction);
// 전체 트랜잭션 집계에 추가
totalTransaction.setSuccess(totalTransaction.getSuccess() + transaction.getSuccess());
totalTransaction.setFail(totalTransaction.getFail() + transaction.getFail());
totalTransaction.setTimeout(totalTransaction.getTimeout() + transaction.getTimeout());
totalTransaction.setSyserror(totalTransaction.getSyserror() + transaction.getSyserror());
}
// 전체 트랜잭션 정보 추가
transactions.add(totalTransaction);
response.put("transactions", transactions);
return response;
} catch (Exception e) {
log.error("트랜잭션 정보 조회 중 오류 발생", e);
response.put("error", "트랜잭션 정보 조회 중 오류가 발생했습니다.");
return response;
}
}
/**
* 피크 성능 정보 제공 API
* 서버별 최대 처리량 TPS(초당 트랜잭션 ) 정보를 제공합니다.
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=peak-performance")
@ResponseBody
public Map<String, Object> getPeakPerformance(@RequestParam(required = false) String type) {
Map<String, Object> response = new HashMap<>();
try {
// 피크 성능 유형 (TPS 또는 거래건수)
String peakType = type != null && "trade".equals(type) ? "trade" : "tps";
List<PeakPerformanceDTO> peakTps = new ArrayList<>();
List<PeakPerformanceDTO> peakTrade = new ArrayList<>();
// 온라인 서비스 피크 정보 조회
for (DataSourceType server : DataSourceTypeManager.getOnlineDataSourceTypes()) {
String peakInfo;
peakInfo = dashboard2Repository.getPeakTps(server.getName());
if (peakInfo != null && !peakInfo.isEmpty()) {
PeakPerformanceDTO dto = dataMapper.fromPeakPerformanceString(peakInfo, peakType);
peakTps.add(dto);
}
peakInfo = dashboard2Repository.getPeakTrade(server.getName());
if (peakInfo != null && !peakInfo.isEmpty()) {
PeakPerformanceDTO dto = dataMapper.fromPeakPerformanceString(peakInfo, peakType);
peakTrade.add(dto);
}
}
// BAP(일괄전송) 서버 피크 정보 조회
DataSourceType bapServer = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.BAP);
if (bapServer != null) {
String peakInfo;
// peakInfo = dashboard2Repository.getPeakTps(bapServer.getName());
//
// if (peakInfo != null && !peakInfo.isEmpty()) {
// PeakPerformanceDTO dto = dataMapper.fromPeakPerformanceString(peakInfo, peakType);
// peakTps.add(dto);
// }
peakInfo = dashboard2Repository.getPeakTrade(bapServer.getName());
if (peakInfo != null && !peakInfo.isEmpty()) {
PeakPerformanceDTO dto = dataMapper.fromPeakPerformanceString(peakInfo, peakType);
peakTrade.add(dto);
}
}
// BAT(배치) 서버 피크 정보 조회
DataSourceType batServer = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.BAT);
if (batServer != null) {
String peakInfo;
// peakInfo = dashboard2Repository.getPeakTps(batServer.getName());
//
// if (peakInfo != null && !peakInfo.isEmpty()) {
// PeakPerformanceDTO dto = dataMapper.fromPeakPerformanceString(peakInfo, peakType);
// peakTps.add(dto);
// }
peakInfo = dashboard2Repository.getPeakTrade(batServer.getName());
if (peakInfo != null && !peakInfo.isEmpty()) {
PeakPerformanceDTO dto = dataMapper.fromPeakPerformanceString(peakInfo, peakType);
peakTrade.add(dto);
}
}
response.put("peakTps", peakTps);
response.put("peakTrade", peakTrade);
response.put("peakType", peakType);
return response;
} catch (Exception e) {
log.error("피크 성능 정보 조회 중 오류 발생", e);
response.put("error", "피크 성능 정보 조회 중 오류가 발생했습니다.");
return response;
}
}
/**
* 배치 처리 상태 제공 API
* 일괄 전송(BAP) 배치(BAT) 처리 상태 정보를 제공합니다.
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=batch-process")
@ResponseBody
public Map<String, Object> getBatchProcess() {
Map<String, Object> response = new HashMap<>();
try {
List<BatchProcessDTO> batchData = new ArrayList<>();
// BAP(일괄전송) 배치 처리 정보 조회
if( DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.BAP) != null ){
String bapBatchInfo = dashboard2Repository.getProcBatch();
if (bapBatchInfo != null && !bapBatchInfo.isEmpty()) {
BatchProcessDTO bapDTO = dataMapper.fromBatchProcessString(DataSourceTypeManager.BAP, bapBatchInfo);
batchData.add(bapDTO);
}
}
// BAT(배치) 배치 처리 정보 조회
if( DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.BAT) != null ){
String batBatchInfo = dashboard2Repository.getProcEaiBatch();
if (batBatchInfo != null && !batBatchInfo.isEmpty()) {
BatchProcessDTO batDTO = dataMapper.fromBatchProcessString(DataSourceTypeManager.BAT, batBatchInfo);
batchData.add(batDTO);
}
}
response.put("batchData", batchData);
return response;
} catch (Exception e) {
log.error("배치 처리 정보 조회 중 오류 발생", e);
response.put("error", "배치 처리 정보 조회 중 오류가 발생했습니다.");
return response;
}
}
/**
* 인스턴스 상세 정보 제공 API
* 특정 서버 또는 모든 서버의 인스턴스별 상세 정보를 제공합니다.
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=instance-detail")
@ResponseBody
public Map<String, Object> getInstanceDetail(@RequestParam(required = false) String serverName) {
Map<String, Object> response = new HashMap<>();
try {
// 서버 타입 목록 가져오기 (특정 서버 이름이 제공된 경우 해당 서버만 포함)
List<DataSourceType> servers;
if (serverName != null && !serverName.isEmpty() && !serverName.equals("all") && !serverName.equals("ALL")) {
DataSourceType server = DataSourceTypeManager.getDataSourceType(serverName.toUpperCase());
servers = server != null ? Collections.singletonList(server) : new ArrayList<>();
} else {
servers = DataSourceTypeManager.getOnlineDataSourceTypes();
}
List<Map<String, Object>> instanceDetails = new ArrayList<>();
for (DataSourceType server : servers) {
// 서버 컨텍스트 설정
DataSourceContextHolder.setDataSourceType(server);
// 서버에 대한 인스턴스 목록 조회
List<Map<String, String>> ipList = eaiServerInfoService.getEaiServerIpListDB();
// 인스턴스별 상세 정보 수집
for (Map<String, String> instanceInfo : ipList) {
String hostName = instanceInfo.get("HOSTNAME");
String instanceName = instanceInfo.get("EAISVRINSTNM");
// 엔진 상태 조회
EAIEngineStatusVo engineStatus = new EAIEngineStatusVo();
engineStatus.setHostName(hostName);
engineStatus.setInstanceName(instanceName);
engineStatus.setService(server.getName());
EAIEngineStatusVo engineStatusResult = dashboardService.findEngineStatus(engineStatus);
// 어댑터 상태 조회
AdaptersVO adaptersVO = new AdaptersVO();
adaptersVO.setHostName(hostName);
adaptersVO.setInstanceName(instanceName);
adaptersVO.setService(server.getName());
AdaptersVO adaptersResult = dashboardService.findAdapterStatus(adaptersVO);
// 트랜잭션 정보 조회
MemStaticsVo memStatics = dashboard2Repository.getMemTradeVoByHost(hostName, instanceName);
// DTO로 변환
ServerStatusDTO serverDTO = dataMapper.fromEAIEngineStatusVo(engineStatusResult);
List<AdapterStatusDTO> adapterDTOs = dataMapper.fromAdaptersVO(adaptersResult);
TransactionDTO transactionDTO = dataMapper.fromMemStaticsVo(memStatics, server.getName());
// 인스턴스 상세 정보 구성
Map<String, Object> instanceDetail = new HashMap<>();
instanceDetail.put("serverName", server.getName());
instanceDetail.put("serverText", server.getText());
instanceDetail.put("hostName", hostName);
instanceDetail.put("instanceName", instanceName);
instanceDetail.put("server", serverDTO);
instanceDetail.put("adapters", adapterDTOs);
instanceDetail.put("transaction", transactionDTO);
instanceDetails.add(instanceDetail);
}
}
response.put("instanceDetails", instanceDetails);
return response;
} catch (Exception e) {
log.error("인스턴스 상세 정보 조회 중 오류 발생", e);
response.put("error", "인스턴스 상세 정보 조회 중 오류가 발생했습니다.");
return response;
}
}
/**
* 실시간 성능 데이터 제공 API
* 서버 어댑터의 최신 성능 데이터를 제공합니다.
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=performance-data")
@ResponseBody
public Map<String, Object> getPerformanceData() {
Map<String, Object> response = new HashMap<>();
try {
// 서버별 성능 정보 수집
List<Map<String, Object>> serverPerformance = new ArrayList<>();
for (DataSourceType server : DataSourceTypeManager.getOnlineDataSourceTypes()) {
// 서버 컨텍스트 설정
DataSourceContextHolder.setDataSourceType(server);
// 서버에 대한 인스턴스 목록 조회
List<Map<String, String>> ipList = eaiServerInfoService.getEaiServerIpListDB();
// 호스트 상태 조회
HostStatusVo hostVo = dashboardService.findHostStatus(ipList);
// 실시간 트랜잭션 정보 조회
MemStaticsVo memStatics = dashboard2Repository.getMemTradeVoByService(server.getName());
// 서버 성능 정보 구성
Map<String, Object> performance = new HashMap<>();
performance.put("serverName", server.getName());
performance.put("serverText", server.getText());
performance.put("cpu", hostVo != null ? hostVo.getCpu() : 0);
performance.put("memory", hostVo != null ? hostVo.getMemory() : 0);
performance.put("disk", hostVo != null ? hostVo.getDisk() : 0);
performance.put("procTotal", memStatics != null ? memStatics.getProcTotal() : 0);
performance.put("errorTotal", memStatics != null ?
(memStatics.getErrorTotal() + memStatics.getTimeoutTotal() + memStatics.getSysErrorTotal()) : 0);
serverPerformance.add(performance);
}
response.put("serverPerformance", serverPerformance);
response.put("timestamp", System.currentTimeMillis());
return response;
} catch (Exception e) {
log.error("성능 데이터 조회 중 오류 발생", e);
response.put("error", "성능 데이터 조회 중 오류가 발생했습니다.");
return response;
}
}
/**
* 트랜잭션 시계열 데이터 제공 API
* 서버별 시간에 따른 트랜잭션 처리 현황(성공, 실패, 타임아웃, 시스템 에러) 제공합니다.
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=transaction-timeseries")
@ResponseBody
public Map<String, Object> getTransactionTimeSeries(
@RequestParam(required = false) String serverName,
@RequestParam(required = false, defaultValue = "10") int minutes) {
Map<String, Object> response = new HashMap<>();
try {
// 서버 타입 목록 가져오기 (특정 서버 이름이 제공된 경우 해당 서버만 포함)
List<DataSourceType> servers;
if (serverName != null && !serverName.isEmpty() && !serverName.equals("all") && !serverName.equals("ALL")) {
DataSourceType server = DataSourceTypeManager.getDataSourceType(serverName.toUpperCase());
servers = server != null ? Collections.singletonList(server) : new ArrayList<>();
} else {
servers = DataSourceTypeManager.getOnlineDataSourceTypes();
}
// 시계열 데이터 수집 기간 제한 (기본 10분)
minutes = Math.min(Math.max(minutes, 1), 60); // 1~60분 범위로 제한
// 서버별 시계열 데이터 수집
Map<String, Map<String, Object>> timeSeriesData = new HashMap<>();
// 현재 시간 저장
long currentTimestamp = System.currentTimeMillis();
// 현재 실시간 거래 정보 저장용
Map<String, MemStaticsVo> currentTransactions = new HashMap<>();
// 현재 거래 정보 먼저 조회 (실시간 추가용)
for (DataSourceType server : servers) {
String serverType = server.getName();
// 서버 컨텍스트 설정
DataSourceContextHolder.setDataSourceType(server);
// 서버의 트랜잭션 정보 조회
MemStaticsVo memStatics = dashboard2Repository.getMemTradeVoByService(serverType);
if (memStatics != null) {
currentTransactions.put(serverType, memStatics);
}
}
// ThroughputCalculationService에서 시계열 데이터 가져오기
for (DataSourceType server : servers) {
String serverType = server.getName();
// 시계열 데이터 조회
ThroughputCalculationService.TimeSeriesData seriesData =
throughputCalculationService.getTimeSeriesData(serverType, minutes);
if (seriesData != null) {
// 시계열 데이터 저장할 구조 생성
Map<String, Object> serviceData = new HashMap<>();
// 기존 타임스탬프와 데이터 복사
List<Long> timestamps = new ArrayList<>(seriesData.getTimestamps());
List<Integer> success = new ArrayList<>(seriesData.getSuccess());
List<Integer> fail = new ArrayList<>(seriesData.getFail());
List<Integer> timeout = new ArrayList<>(seriesData.getTimeout());
List<Integer> syserror = new ArrayList<>(seriesData.getSyserror());
// 현재 값을 시계열에 추가 (마지막 데이터가 없거나 10초 이상 지났을 경우)
MemStaticsVo currentStats = currentTransactions.get(serverType);
if (currentStats != null) {
boolean shouldAddCurrentData = true;
// 마지막 데이터와 현재 시간이 2초 이상 차이가 나면 현재 추가
if (!timestamps.isEmpty() && (currentTimestamp - timestamps.get(timestamps.size() - 1)) < 2000) {
shouldAddCurrentData = false;
}
if (shouldAddCurrentData) {
// 현재 값을 시계열 마지막에 추가
timestamps.add(currentTimestamp);
// 성공 건수 = 전체 - (실패 + 타임아웃 + 시스템에러)
int successCount = currentStats.getProcTotal() - currentStats.getErrorTotal()
- currentStats.getTimeoutTotal() - currentStats.getSysErrorTotal();
success.add(Math.max(0, successCount));
fail.add(currentStats.getErrorTotal());
timeout.add(currentStats.getTimeoutTotal());
syserror.add(currentStats.getSysErrorTotal());
}
}
// 최종 데이터 저장
serviceData.put("timestamps", timestamps);
serviceData.put("success", success);
serviceData.put("fail", fail);
serviceData.put("timeout", timeout);
serviceData.put("syserror", syserror);
// 서버의 데이터 저장
timeSeriesData.put(serverType, serviceData);
} else {
// 데이터가 없는 경우 데이터 생성
Map<String, Object> emptyData = new HashMap<>();
emptyData.put("timestamps", new ArrayList<>());
emptyData.put("success", new ArrayList<>());
emptyData.put("fail", new ArrayList<>());
emptyData.put("timeout", new ArrayList<>());
emptyData.put("syserror", new ArrayList<>());
timeSeriesData.put(serverType, emptyData);
}
}
// 전체 합계 데이터 조회
ThroughputCalculationService.TimeSeriesData totalData =
throughputCalculationService.getTimeSeriesData("TOTAL", minutes);
if (totalData != null) {
Map<String, Object> totalSeriesData = new HashMap<>();
// 기존 타임스탬프와 데이터 복사
List<Long> timestamps = new ArrayList<>(totalData.getTimestamps());
List<Integer> success = new ArrayList<>(totalData.getSuccess());
List<Integer> fail = new ArrayList<>(totalData.getFail());
List<Integer> timeout = new ArrayList<>(totalData.getTimeout());
List<Integer> syserror = new ArrayList<>(totalData.getSyserror());
// 현재 전체 합계 계산
int totalSuccess = 0;
int totalFail = 0;
int totalTimeout = 0;
int totalSyserror = 0;
for (String serviceType : currentTransactions.keySet()) {
MemStaticsVo stats = currentTransactions.get(serviceType);
if (stats != null) {
totalSuccess += stats.getProcTotal() - stats.getErrorTotal()
- stats.getTimeoutTotal() - stats.getSysErrorTotal();
totalFail += stats.getErrorTotal();
totalTimeout += stats.getTimeoutTotal();
totalSyserror += stats.getSysErrorTotal();
}
}
// 마지막 데이터와 현재 시간이 2초 이상 차이가 나면 현재 추가
boolean shouldAddCurrentData = true;
if (!timestamps.isEmpty() && (currentTimestamp - timestamps.get(timestamps.size() - 1)) < 2000) {
shouldAddCurrentData = false;
}
if (shouldAddCurrentData) {
timestamps.add(currentTimestamp);
success.add(totalSuccess);
fail.add(totalFail);
timeout.add(totalTimeout);
syserror.add(totalSyserror);
}
totalSeriesData.put("timestamps", timestamps);
totalSeriesData.put("success", success);
totalSeriesData.put("fail", fail);
totalSeriesData.put("timeout", timeout);
totalSeriesData.put("syserror", syserror);
timeSeriesData.put("TOTAL", totalSeriesData);
} else {
// 전체 데이터가 없는 경우, 더미 데이터 생성은 제거
Map<String, Object> emptyData = new HashMap<>();
emptyData.put("timestamps", new ArrayList<>());
emptyData.put("success", new ArrayList<>());
emptyData.put("fail", new ArrayList<>());
emptyData.put("timeout", new ArrayList<>());
emptyData.put("syserror", new ArrayList<>());
timeSeriesData.put("TOTAL", emptyData);
}
response.put("timeSeriesData", timeSeriesData);
response.put("minutes", minutes);
response.put("timestamp", System.currentTimeMillis());
return response;
} catch (Exception e) {
log.error("트랜잭션 시계열 정보 조회 중 오류 발생", e);
response.put("error", "트랜잭션 시계열 정보 조회 중 오류가 발생했습니다.");
return response;
}
}
/**
* 서버 리소스 시계열 데이터 제공 API
* 특정 서버의 CPU, 메모리, 디스크 사용량 시계열 데이터를 제공합니다.
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=server-resource")
@ResponseBody
public Map<String, Object> getServerResourceData(
@RequestParam String hostname,
@RequestParam(required = false, defaultValue = "7") int count) {
Map<String, Object> response = new HashMap<>();
try {
// 서버 리소스 시계열 데이터 조회
ThroughputCalculationService.ServerResourceData resourceData =
throughputCalculationService.getServerResourceData(hostname, count);
// 응답 데이터 구성
Map<String, Object> data = new HashMap<>();
data.put("hostname", hostname);
data.put("timestamps", resourceData.getTimestamps());
data.put("cpu", resourceData.getCpu());
data.put("memory", resourceData.getMemory());
data.put("disk", resourceData.getDisk());
response.put("data", data);
return response;
} catch (Exception e) {
log.error("서버 리소스 시계열 데이터 조회 중 오류 발생", e);
response.put("error", "서버 리소스 시계열 데이터 조회 중 오류가 발생했습니다.");
return response;
}
}
/**
* SMS 장애 통보 정보를 조회합니다.
* @return SMS 장애 통보 정보
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=sms-notification")
@ResponseBody
public Map<String, Object> getSmsNotification() {
Map<String, Object> result = new HashMap<>();
List<SmsVO> totalSmsList = new ArrayList<>();
try {
List<DataSourceType> servers = DataSourceTypeManager.getOnlineDataSourceTypes();
// GW 추가
if( DataSourceTypeManager.getDataSourceType("FGW") != null ){
servers.add(DataSourceTypeManager.getDataSourceType("FGW"));
}
if( DataSourceTypeManager.getDataSourceType("CGW") != null ){
servers.add(DataSourceTypeManager.getDataSourceType("CGW"));
}
for (DataSourceType d : servers) {
DataSourceContextHolder.setDataSourceType(d);
List ipList = eaiServerInfoService.getEaiServerIpListDB();
List<SmsVO> smsList = dashboardService.findSmsList(ipList, d.getName());
totalSmsList.addAll(smsList);
}
// SmsVO 목록을 SmsNotificationDto 목록으로 변환
List<SmsNotificationDto> smsNotificationDtoList = smsNotificationMapper.mapToSmsNotificationDtoList(totalSmsList);
result.put("success", true);
result.put("data", smsNotificationDtoList);
} catch (Exception e) {
log.error("SMS 장애 통보 정보 조회 중 오류 발생", e);
result.put("success", false);
result.put("message", "SMS 장애 통보 정보 조회 중 오류가 발생했습니다.");
}
return result;
}
/**
* Get socket status data
* @param serverName Optional server name filter
* @param adapterGroup Optional adapter group filter
* @return SocketStatusDTO with all socket status data
*/
@RequestMapping(value = "/socket-status")
@ResponseBody
public ResponseEntity<SocketStatusDTO> getSocketStatus(
@RequestParam(value = "serverName", required = false) String serverName,
@RequestParam(value = "adapterGroup", required = false) String adapterGroup) {
SocketStatusDTO result = socketMonitorService.getSocketStatus(serverName, adapterGroup);
return ResponseEntity.ok(result);
}
/**
* Get socket process data
* @param serverName Optional server name filter
* @param adapterGroup Optional adapter group filter
* @return SocketProcessDTO with all socket process data
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=socket-process")
@ResponseBody
public ResponseEntity<SocketProcessDTO> getSocketProcess(
@RequestParam(value = "serverName", required = false) String serverName,
@RequestParam(value = "adapterGroup", required = false) String adapterGroup) {
SocketProcessDTO result = socketMonitorService.getSocketProcess(serverName, adapterGroup);
return ResponseEntity.ok(result);
}
/**
* Get combined socket data (both status and process)
* @param serverName Optional server name filter
* @param adapterGroup Optional adapter group filter
* @return Map containing both socket status and process data
*/
@RequestMapping(path = "/modern-dashboard-api.do", params = "type=socket-combined")
@ResponseBody
public ResponseEntity<Map<String, Object>> getCombinedSocketData(
@RequestParam(value = "serverName", required = false) String serverName,
@RequestParam(value = "adapterGroup", required = false) String adapterGroup) {
Map<String, Object> result = socketMonitorService.getCombinedSocketData(serverName, adapterGroup);
return ResponseEntity.ok(result);
}
/**
* 거래현황 시계열 데이터 조회
* @param seconds 조회할 단위 기간 (기본값 60초, 최대 120초)
* @return 시계열 데이터
*/
@GetMapping(path = "/modern-dashboard-api.do", params = "type=timeseries")
public ResponseEntity<Map<String, Object>> getTimeSeriesData(
@RequestParam(defaultValue = "60") int seconds) {
TransactionSummaryService.TimeSeriesData data = transactionSummaryService.getTimeSeriesData(seconds);
// 전체 기간 동안의 합계 계산
Map<String, Integer> totals = data.calculateTotalCounts();
// 데이터 포인트들을 오류 건수 -> 정상 건수 순으로 정렬
List<Map<String, Object>> sortedDataPoints = IntStream.range(0, data.getTimestamps().size())
.mapToObj(i -> {
Map<String, Object> point = new HashMap<>();
point.put("timestamp", data.getTimestamps().get(i));
point.put("error", data.getError().get(i));
point.put("success", data.getSuccess().get(i));
point.put("avgTime", data.getAvgTime().get(i));
return point;
})
.sorted((a, b) -> {
// 1차: 오류 건수 내림차순
int errorCompare = ((Integer)b.get("error")).compareTo((Integer)a.get("error"));
if (errorCompare != 0) return errorCompare;
// 2차: 정상 건수 내림차순
return ((Integer)b.get("success")).compareTo((Integer)a.get("success"));
})
.collect(Collectors.toList());
Map<String, Object> response = new HashMap<>();
response.put("seconds", seconds);
response.put("totalError", totals.get("error"));
response.put("totalSuccess", totals.get("success"));
response.put("dataPoints", sortedDataPoints);
return ResponseEntity.ok(response);
}
}
@@ -0,0 +1,18 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import lombok.Data;
/**
* 어댑터 상태 정보 DTO
* 시스템 어댑터의 상태 정보를 전달하기 위한 데이터 객체입니다.
*/
@Data
public class AdapterStatusDTO {
private String serverName; // 서버 이름
private String instanceId; // 인스턴스 ID
private String adapterType; // 어댑터 유형(Socket, HTTP)
private String status; // 상태(normal, warning, error, inactive)
private int totalCount; // 전체
private int runningCount; // 실행 중인
private int stopCount; // 정지된
}
@@ -0,0 +1,73 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 어댑터 상태 요약 컨테이너 DTO
* 모든 어댑터 유형의 상태 요약 정보를 담는 컨테이너입니다.
*/
@Data
public class AdapterStatusSummaryContainerDTO {
private Map<String, AdapterStatusSummaryDTO> adapterSummaries;
private String overallStatus; // 전체 상태 (normal, warning, error, inactive)
public AdapterStatusSummaryContainerDTO() {
this.adapterSummaries = new HashMap<>();
this.overallStatus = "normal";
// 기본 어댑터 유형 초기화 (Socket, HTTP만 포함, Socket2는 Socket으로 통합)
adapterSummaries.put("Socket", new AdapterStatusSummaryDTO("Socket"));
adapterSummaries.put("HTTP", new AdapterStatusSummaryDTO("HTTP"));
}
/**
* 특정 어댑터 유형의 상태 요약 정보를 반환합니다.
* @param adapterType 어댑터 유형
* @return 해당 어댑터 유형의 상태 요약 정보
*/
public AdapterStatusSummaryDTO getAdapterSummary(String adapterType) {
return adapterSummaries.get(adapterType);
}
/**
* 모든 어댑터 유형의 상태 요약 정보를 리스트로 반환합니다.
* @return 어댑터 상태 요약 정보 리스트
*/
public List<AdapterStatusSummaryDTO> getAdapterSummaryList() {
return new ArrayList<>(adapterSummaries.values());
}
/**
* 전체 상태를 업데이트합니다.
*/
public void updateOverallStatus() {
boolean hasError = false;
boolean hasWarning = false;
boolean hasNormal = false;
for (AdapterStatusSummaryDTO summary : adapterSummaries.values()) {
if ("error".equals(summary.getOverallStatus())) {
hasError = true;
} else if ("warning".equals(summary.getOverallStatus())) {
hasWarning = true;
} else if ("normal".equals(summary.getOverallStatus())) {
hasNormal = true;
}
}
if (hasError) {
overallStatus = "error";
} else if (hasWarning) {
overallStatus = "warning";
} else if (hasNormal) {
overallStatus = "normal";
} else {
overallStatus = "inactive";
}
}
}
@@ -0,0 +1,76 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import lombok.Data;
/**
* 어댑터 상태 요약 정보 DTO
* 어댑터 유형별 상태 집계 정보를 제공합니다.
*/
@Data
public class AdapterStatusSummaryDTO {
private String adapterType; // 어댑터 유형(SNA, Socket, Tuxedo, MQ, HTTP, EJB)
// 상태별 카운트
private int errorCount; // 에러 상태
private int warningCount; // 부분정상(경고) 상태
private int normalCount; // 정상 상태
private int inactiveCount; // 정지 상태
// 전체 상태 (UI 표시용)
private String overallStatus; // 전체 상태 (normal, warning, error, inactive)
public AdapterStatusSummaryDTO(String adapterType) {
this.adapterType = adapterType;
this.errorCount = 0;
this.warningCount = 0;
this.normalCount = 0;
this.inactiveCount = 0;
this.overallStatus = "normal";
}
/**
* 상태 코드에 따라 카운트를 증가시킵니다.
* @param statusCode 상태 코드 (0: 에러, 1: 부분정상, 2: 정상, 3: 정지)
*/
public void incrementStatusCount(int statusCode) {
switch (statusCode) {
case 0: // 에러
errorCount++;
break;
case 1: // 부분정상
warningCount++;
break;
case 2: // 정상
normalCount++;
break;
case 3: // 정지
inactiveCount++;
break;
}
// 전체 상태 업데이트
updateOverallStatus();
}
/**
* 카운트 정보를 기반으로 전체 상태를 업데이트합니다.
*/
private void updateOverallStatus() {
if (errorCount > 0) {
overallStatus = "error";
} else if (warningCount > 0) {
overallStatus = "warning";
} else if (normalCount > 0) {
overallStatus = "normal";
} else {
overallStatus = "inactive";
}
}
/**
* 전체 어댑터 수를 반환합니다.
*/
public int getTotalCount() {
return errorCount + warningCount + normalCount + inactiveCount;
}
}
@@ -0,0 +1,12 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import lombok.Data;
@Data
public class AlertMessageDTO {
private String type; // 메시지 유형(개발, 운영 )
private String instance; // 인스턴스 ID
private String timestamp; // 발생 시간
private String message; // 메시지 내용
private String severity; // 심각도(error, warning, info)
}
@@ -0,0 +1,141 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
/**
* 배치 처리 현황 DTO
*/
public class BatchProcessDTO {
private String serviceType; // BAP 또는 BAT
private String procBatch; // 원본 CSV 데이터
private int sendSuccess; // 송신 성공
private int receiveSuccess; // 수신 성공
private int sendError; // 송신 에러
private int receiveError; // 수신 에러
private int sendWaiting; // 송신 대기
private int receiveWaiting; // 수신 대기
public BatchProcessDTO() {
// 기본 생성자
}
public String getServiceType() {
return serviceType;
}
public void setServiceType(String serviceType) {
this.serviceType = serviceType;
}
public String getProcBatch() {
return procBatch;
}
public void setProcBatch(String procBatch) {
this.procBatch = procBatch;
// CSV 데이터 파싱
if (procBatch != null && !procBatch.isEmpty()) {
String[] fields = procBatch.split(",");
if (fields.length >= 6) {
try {
this.sendSuccess = Integer.parseInt(fields[0].trim());
this.receiveSuccess = Integer.parseInt(fields[1].trim());
this.sendError = Integer.parseInt(fields[2].trim());
this.receiveError = Integer.parseInt(fields[3].trim());
this.sendWaiting = Integer.parseInt(fields[4].trim());
this.receiveWaiting = Integer.parseInt(fields[5].trim());
} catch (NumberFormatException e) {
// 파싱 실패 기본값 유지
}
}
}
}
public int getSendSuccess() {
return sendSuccess;
}
public void setSendSuccess(int sendSuccess) {
this.sendSuccess = sendSuccess;
}
public int getReceiveSuccess() {
return receiveSuccess;
}
public void setReceiveSuccess(int receiveSuccess) {
this.receiveSuccess = receiveSuccess;
}
public int getSendError() {
return sendError;
}
public void setSendError(int sendError) {
this.sendError = sendError;
}
public int getReceiveError() {
return receiveError;
}
public void setReceiveError(int receiveError) {
this.receiveError = receiveError;
}
public int getSendWaiting() {
return sendWaiting;
}
public void setSendWaiting(int sendWaiting) {
this.sendWaiting = sendWaiting;
}
public int getReceiveWaiting() {
return receiveWaiting;
}
public void setReceiveWaiting(int receiveWaiting) {
this.receiveWaiting = receiveWaiting;
}
/**
* 송신 건수 반환
*/
public int getTotalSend() {
return sendSuccess + sendError + sendWaiting;
}
/**
* 수신 건수 반환
*/
public int getTotalReceive() {
return receiveSuccess + receiveError + receiveWaiting;
}
/**
* 처리 건수 반환
*/
public int getTotal() {
return getTotalSend() + getTotalReceive();
}
/**
* 송신 오류율 계산 (%)
*/
public double getSendErrorRate() {
if (getTotalSend() == 0) {
return 0.0;
}
return (double) sendError / getTotalSend() * 100;
}
/**
* 수신 오류율 계산 (%)
*/
public double getReceiveErrorRate() {
if (getTotalReceive() == 0) {
return 0.0;
}
return (double) receiveError / getTotalReceive() * 100;
}
}
@@ -0,0 +1,48 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import lombok.Data;
/**
* 인스턴스 정보 DTO
* 서버 인스턴스의 상태 정보를 전달하기 위한 데이터 객체입니다.
*/
@Data
public class InstanceDTO {
private String id; // 인스턴스 ID
private String status; // 상태(normal, warning, error, inactive)
private String throughput; // 처리량(초당 처리 건수)
/**
* 처리량 수치만 추출
*/
public int getThroughputValue() {
if (throughput == null || throughput.isEmpty()) {
return 0;
}
try {
// "123/s" 형식에서 숫자만 추출
return Integer.parseInt(throughput.replaceAll("[^0-9]", ""));
} catch (NumberFormatException e) {
return 0;
}
}
/**
* 상태에 따른 표시용 텍스트 반환
*/
public String getStatusText() {
switch (status) {
case "normal":
return "정상";
case "warning":
return "경고";
case "error":
return "에러";
case "inactive":
return "비활성";
default:
return "알 수 없음";
}
}
}
@@ -0,0 +1,45 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import lombok.Data;
/**
* 기관 연결 상태 모니터링을 위한 DTO
*/
@Data
public class InstitutionStatusDTO {
/**
* 기관 코드
*/
private String institutionCode;
/**
* 기관명
*/
private String institutionName;
/**
* 담당자명
*/
private String contactPerson;
/**
* 담당자 연락처
*/
private String contactNumber;
/**
* 정상 어댑터
*/
private int normalAdapterCount;
/**
* 오류 어댑터
*/
private int errorAdapterCount;
/**
* 회선 상태 (NORMAL, WARN, ERROR)
*/
private String lineStatus;
}
@@ -0,0 +1,28 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import lombok.Data;
/**
* 피크 성능 정보 DTO
* 시스템의 최대 성능 기록을 전달하기 위한 데이터 객체입니다.
*/
@Data
public class PeakPerformanceDTO {
private String serviceType; // 서비스 유형(EAI, FEP )
private String date; // 날짜(YYYYMMDD)
private int transactionCount; // 처리 건수
private int tps; // 초당 트랜잭션 (Transactions Per Second)
/**
* 날짜를 포맷팅하여 반환 (YYYY-MM-DD 형식)
*/
public String getFormattedDate() {
if (date == null || date.length() < 8) {
return date;
}
return date.substring(0, 4) + "-" +
date.substring(4, 6) + "-" +
date.substring(6, 8);
}
}
@@ -0,0 +1,93 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 서버 상태 정보 DTO
* 서버의 상태 정보와 하위 인스턴스 목록을 포함하는 데이터 객체입니다.
*/
@Data
public class ServerStatusDTO {
private String name; // 서버 이름
private String text; // 서버 표시명
private String hostname; // 서버 표시명
private String status; // 상태(normal, warning, error)
private int cpu; // CPU 사용률(%)
private int memory; // 메모리 사용률(%)
private int disk; // 디스크 사용률(%)
private List<InstanceDTO> instances = new ArrayList<>(); // 인스턴스 목록
/**
* 특정 인스턴스 ID에 해당하는 인스턴스를 찾습니다.
* @param instanceId 찾을 인스턴스 ID
* @return 해당 인스턴스 또는 null
*/
public InstanceDTO findInstance(String instanceId) {
if (instanceId == null || instances == null) {
return null;
}
for (InstanceDTO instance : instances) {
if (instanceId.equals(instance.getId())) {
return instance;
}
}
return null;
}
/**
* 에러 상태의 인스턴스 수를 반환합니다.
*/
public int getErrorInstanceCount() {
int count = 0;
for (InstanceDTO instance : instances) {
if ("error".equals(instance.getStatus())) {
count++;
}
}
return count;
}
/**
* 경고 상태의 인스턴스 수를 반환합니다.
*/
public int getWarningInstanceCount() {
int count = 0;
for (InstanceDTO instance : instances) {
if ("warning".equals(instance.getStatus())) {
count++;
}
}
return count;
}
/**
* 정상 상태의 인스턴스 수를 반환합니다.
*/
public int getNormalInstanceCount() {
int count = 0;
for (InstanceDTO instance : instances) {
if ("normal".equals(instance.getStatus())) {
count++;
}
}
return count;
}
/**
* 비활성 상태의 인스턴스 수를 반환합니다.
*/
public int getInactiveInstanceCount() {
int count = 0;
for (InstanceDTO instance : instances) {
if ("inactive".equals(instance.getStatus())) {
count++;
}
}
return count;
}
}
@@ -0,0 +1,109 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
/**
* SMS 장애 통보 정보를 담는 DTO 클래스
*/
public class SmsNotificationDto {
@JsonFormat(pattern = "MMdd HH:mm:ss.SSS")
private LocalDateTime dateTime; // 발생 일시
private Integer count; // 발생 건수
private String instanceName; // 인스턴스
private String errorContent; // 에러 내용
private String adapterDesc; // 어댑터 설명 (어댑터 오류인 경우)
private String adapterId; // 어댑터 ID (어댑터 오류인 경우)
private String interfaceId; // 인터페이스 ID (인터페이스 오류인 경우)
private String errorType; // 오류 유형 (ADAPTER/INTERFACE)
private String serviceType; // 서비스 유형
// Getters and Setters
public LocalDateTime getDateTime() {
return dateTime;
}
public void setDateTime(LocalDateTime dateTime) {
this.dateTime = dateTime;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public String getInstanceName() {
return instanceName;
}
public void setInstanceName(String instanceName) {
this.instanceName = instanceName;
}
public String getErrorContent() {
return errorContent;
}
public void setErrorContent(String errorContent) {
this.errorContent = errorContent;
}
public String getAdapterDesc() {
return adapterDesc;
}
public void setAdapterDesc(String adapterDesc) {
this.adapterDesc = adapterDesc;
}
public String getAdapterId() {
return adapterId;
}
public void setAdapterId(String adapterId) {
this.adapterId = adapterId;
}
public String getInterfaceId() {
return interfaceId;
}
public void setInterfaceId(String interfaceId) {
this.interfaceId = interfaceId;
}
public String getErrorType() {
return errorType;
}
public void setErrorType(String errorType) {
this.errorType = errorType;
}
public String getServiceType() {
return serviceType;
}
public void setServiceType(String serviceType) {
this.serviceType = serviceType;
}
@Override
public String toString() {
return "SmsNotificationDto{" +
"dateTime=" + dateTime +
", count=" + count +
", instanceName='" + instanceName + '\'' +
", errorContent='" + errorContent + '\'' +
", adapterDesc='" + adapterDesc + '\'' +
", adapterId='" + adapterId + '\'' +
", interfaceId='" + interfaceId + '\'' +
", errorType='" + errorType + '\'' +
", serviceType='" + serviceType + '\'' +
'}';
}
}
@@ -0,0 +1,220 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import java.util.List;
/**
* Socket Process DTO for modern dashboard
*/
public class SocketProcessDTO {
private JsonPageVoDTO jsonPageVo;
private int total;
private int page;
private int records;
private List<SocketProcessItemDTO> rows;
public SocketProcessDTO() {
}
public JsonPageVoDTO getJsonPageVo() {
return jsonPageVo;
}
public void setJsonPageVo(JsonPageVoDTO jsonPageVo) {
this.jsonPageVo = jsonPageVo;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getRecords() {
return records;
}
public void setRecords(int records) {
this.records = records;
}
public List<SocketProcessItemDTO> getRows() {
return rows;
}
public void setRows(List<SocketProcessItemDTO> rows) {
this.rows = rows;
}
/**
* JsonPageVo DTO
*/
public static class JsonPageVoDTO {
private int page;
private int totalCount;
private int rows;
private int total;
private int startNum;
private int endNum;
public JsonPageVoDTO() {
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getTotalCount() {
return totalCount;
}
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}
public int getRows() {
return rows;
}
public void setRows(int rows) {
this.rows = rows;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public int getStartNum() {
return startNum;
}
public void setStartNum(int startNum) {
this.startNum = startNum;
}
public int getEndNum() {
return endNum;
}
public void setEndNum(int endNum) {
this.endNum = endNum;
}
}
/**
* Socket Process Item DTO
*/
public static class SocketProcessItemDTO {
private String maxConnection;
private String status;
private String eaiSevrInstncName;
private String adptrBzwkGroupName;
private String currentConnection;
private String recvCount;
private String adptrBzwkName;
private String adptrBzwkDesc;
private String sendCount;
private String errorCount;
public SocketProcessItemDTO() {
}
public String getMaxConnection() {
return maxConnection;
}
public void setMaxConnection(String maxConnection) {
this.maxConnection = maxConnection;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getEaiSevrInstncName() {
return eaiSevrInstncName;
}
public void setEaiSevrInstncName(String eaiSevrInstncName) {
this.eaiSevrInstncName = eaiSevrInstncName;
}
public String getAdptrBzwkGroupName() {
return adptrBzwkGroupName;
}
public void setAdptrBzwkGroupName(String adptrBzwkGroupName) {
this.adptrBzwkGroupName = adptrBzwkGroupName;
}
public String getCurrentConnection() {
return currentConnection;
}
public void setCurrentConnection(String currentConnection) {
this.currentConnection = currentConnection;
}
public String getRecvCount() {
return recvCount;
}
public void setRecvCount(String recvCount) {
this.recvCount = recvCount;
}
public String getAdptrBzwkName() {
return adptrBzwkName;
}
public void setAdptrBzwkName(String adptrBzwkName) {
this.adptrBzwkName = adptrBzwkName;
}
public String getAdptrBzwkDesc() {
return adptrBzwkDesc;
}
public void setAdptrBzwkDesc(String adptrBzwkDesc) {
this.adptrBzwkDesc = adptrBzwkDesc;
}
public String getSendCount() {
return sendCount;
}
public void setSendCount(String sendCount) {
this.sendCount = sendCount;
}
public String getErrorCount() {
return errorCount;
}
public void setErrorCount(String errorCount) {
this.errorCount = errorCount;
}
}
}
@@ -0,0 +1,148 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import java.util.List;
/**
* Socket Status DTO for modern dashboard
*/
public class SocketStatusDTO {
private int total;
private int page;
private int records;
private List<SocketStatusItemDTO> rows;
public SocketStatusDTO() {
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getRecords() {
return records;
}
public void setRecords(int records) {
this.records = records;
}
public List<SocketStatusItemDTO> getRows() {
return rows;
}
public void setRows(List<SocketStatusItemDTO> rows) {
this.rows = rows;
}
/**
* Socket Status Item DTO
*/
public static class SocketStatusItemDTO {
private String status;
private String eaiSevrInstncName;
private String testMasterYn;
private String adptrNickName;
private String startStop;
private String adptrBzwkGroupName;
private String adptrBzwkName;
private String adptrType;
private String starting;
private String adptrBzwkDesc;
public SocketStatusItemDTO() {
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getEaiSevrInstncName() {
return eaiSevrInstncName;
}
public void setEaiSevrInstncName(String eaiSevrInstncName) {
this.eaiSevrInstncName = eaiSevrInstncName;
}
public String getTestMasterYn() {
return testMasterYn;
}
public void setTestMasterYn(String testMasterYn) {
this.testMasterYn = testMasterYn;
}
public String getAdptrNickName() {
return adptrNickName;
}
public void setAdptrNickName(String adptrNickName) {
this.adptrNickName = adptrNickName;
}
public String getStartStop() {
return startStop;
}
public void setStartStop(String startStop) {
this.startStop = startStop;
}
public String getAdptrBzwkGroupName() {
return adptrBzwkGroupName;
}
public void setAdptrBzwkGroupName(String adptrBzwkGroupName) {
this.adptrBzwkGroupName = adptrBzwkGroupName;
}
public String getAdptrBzwkName() {
return adptrBzwkName;
}
public void setAdptrBzwkName(String adptrBzwkName) {
this.adptrBzwkName = adptrBzwkName;
}
public String getAdptrType() {
return adptrType;
}
public void setAdptrType(String adptrType) {
this.adptrType = adptrType;
}
public String getStarting() {
return starting;
}
public void setStarting(String starting) {
this.starting = starting;
}
public String getAdptrBzwkDesc() {
return adptrBzwkDesc;
}
public void setAdptrBzwkDesc(String adptrBzwkDesc) {
this.adptrBzwkDesc = adptrBzwkDesc;
}
}
}
@@ -0,0 +1,30 @@
package com.eactive.eai.rms.onl.dashboard.modern.dto;
import lombok.Data;
/**
* 트랜잭션 정보 DTO
* 시스템 처리된 트랜잭션 정보를 전달하기 위한 데이터 객체입니다.
*/
@Data
public class TransactionDTO {
private String type; // 서비스 유형(EAI, FEP )
private int success; // 성공 건수
private int fail; // 실패(업무 에러) 건수
private int timeout; // 타임아웃 건수
private int syserror; // 시스템 에러 건수
/**
* 처리 건수 반환
*/
public int getTotal() {
return success + fail + timeout + syserror;
}
/**
* 에러 건수 합계 반환 (업무 에러 + 타임아웃 + 시스템 에러)
*/
public int getTotalError() {
return fail + timeout + syserror;
}
}
@@ -0,0 +1,354 @@
package com.eactive.eai.rms.onl.dashboard.modern.mapper;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.onl.dashboard.modern.dto.*;
import com.eactive.eai.rms.onl.tracking.vo.MemStaticsVo;
import com.eactive.eai.rms.onl.vo.AdaptersVO;
import com.eactive.eai.rms.onl.vo.EAIEngineStatusVo;
import com.eactive.eai.rms.onl.vo.HostStatusVo;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 대시보드 데이터 매퍼
* VO 객체를 DTO로 변환하는 역할을 담당합니다.
*/
@Component
public class DashboardDataMapper {
/**
* EAIEngineStatusVo를 ServerStatusDTO로 변환
* 서버 상태 정보를 포함합니다.
*/
public ServerStatusDTO fromEAIEngineStatusVo(EAIEngineStatusVo vo) {
ServerStatusDTO dto = new ServerStatusDTO();
if (vo == null) {
return dto;
}
// 서버 기본 정보 설정
dto.setName(vo.getServerName() != null ? vo.getServerName() : vo.getHostName());
dto.setText(vo.getService());
// 서버 상태 설정 (serverStateImage: 0=중지/에러, 1=경고, 2=정상)
switch (vo.getServerStateImage()) {
case 0:
dto.setStatus("error");
break;
case 1:
dto.setStatus("warning");
break;
case 2:
dto.setStatus("normal");
break;
default:
dto.setStatus("unknown");
}
// 리소스 사용률 설정
if (vo.getHeapSizeCurrent() > 0) {
dto.setCpu((int)(100 - (vo.getHeapFreeCurrent() * 100) / vo.getHeapSizeCurrent()));
} else {
dto.setCpu(0);
}
dto.setMemory(vo.getHeapFreePercent() > 0 ? 100 - vo.getHeapFreePercent() : 0);
dto.setDisk(vo.getJmsFileStoreUseAge());
// 인스턴스 정보 설정
List<InstanceDTO> instances = new ArrayList<>();
InstanceDTO instance = new InstanceDTO();
instance.setId(vo.getInstanceName());
// 인스턴스 상태 설정
switch (vo.getServerStateImage()) {
case 0:
instance.setStatus("error");
break;
case 1:
instance.setStatus("warning");
break;
case 2:
instance.setStatus("normal");
break;
default:
instance.setStatus("inactive");
}
// 처리량 정보 설정
instance.setThroughput(vo.getPendingRequestCurrentCount() + "/s");
instances.add(instance);
dto.setInstances(instances);
return dto;
}
/**
* HostStatusVo를 ServerStatusDTO로 변환
* 호스트 상태 정보를 포함합니다.
*/
public ServerStatusDTO fromHostStatusVo(HostStatusVo vo) {
ServerStatusDTO dto = new ServerStatusDTO();
if (vo == null) {
return dto;
}
dto.setName(vo.getHostName());
dto.setText(vo.getHostName());
dto.setCpu(vo.getCpu());
dto.setMemory(vo.getMemory());
dto.setDisk(vo.getDisk());
dto.setStatus(getStatusFromUsage(vo.getCpu(), vo.getMemory(), vo.getDisk()));
// 인스턴스 리스트는 다른 곳에서 채워질 것임
dto.setInstances(new ArrayList<>());
return dto;
}
/**
* AdaptersVO를 AdapterStatusDTO 목록으로 변환
* Socket, HTTP 어댑터 정보만 포함합니다.
*/
public List<AdapterStatusDTO> fromAdaptersVO(AdaptersVO vo) {
List<AdapterStatusDTO> dtoList = new ArrayList<>();
if (vo == null) {
return dtoList;
}
// Socket 어댑터 상태 (Socket과 Socket2를 하나로 통합)
AdapterStatusDTO socketDTO = new AdapterStatusDTO();
socketDTO.setServerName(vo.getHostName());
socketDTO.setInstanceId(vo.getInstanceName());
socketDTO.setAdapterType("Socket");
// Socket과 Socket2의 상태 심각한 상태를 선택
int socketStatus = vo.getSocket(3);
int socket2Status = vo.getSocket2(3);
int combinedStatus = Math.min(socketStatus, socket2Status); // 낮은 값이 심각한 상태
socketDTO.setStatus(getStatusTextFromCode(combinedStatus));
// Socket과 Socket2의 카운트를 합산
socketDTO.setTotalCount(vo.getSocket(0) + vo.getSocket2(0));
socketDTO.setRunningCount(vo.getSocket(1) + vo.getSocket2(1));
socketDTO.setStopCount(vo.getSocket(2) + vo.getSocket2(2));
dtoList.add(socketDTO);
// HTTP 어댑터 상태
AdapterStatusDTO httpDTO = new AdapterStatusDTO();
httpDTO.setServerName(vo.getHostName());
httpDTO.setInstanceId(vo.getInstanceName());
httpDTO.setAdapterType("HTTP");
httpDTO.setStatus(getStatusTextFromCode(vo.getHttp(3)));
httpDTO.setTotalCount(vo.getHttp(0));
httpDTO.setRunningCount(vo.getHttp(1));
httpDTO.setStopCount(vo.getHttp(2));
dtoList.add(httpDTO);
return dtoList;
}
/**
* 상태 코드를 텍스트로 변환
*/
private String getStatusTextFromCode(int statusCode) {
switch (statusCode) {
case 0:
return "error";
case 1:
return "warning";
case 2:
return "normal";
case 3:
return "inactive";
default:
return "unknown";
}
}
/**
* MemStaticsVo를 TransactionDTO로 변환
* 트랜잭션 통계 정보를 포함합니다.
*/
public TransactionDTO fromMemStaticsVo(MemStaticsVo vo, String serviceType) {
TransactionDTO dto = new TransactionDTO();
if (vo == null) {
return dto;
}
dto.setType(serviceType); // 서비스 타입을 파라미터로 받아서 설정
dto.setSuccess(vo.getProcTotal() - vo.getTimeoutTotal() - vo.getErrorTotal() - vo.getSysErrorTotal());
dto.setTimeout(vo.getTimeoutTotal());
dto.setFail(vo.getErrorTotal());
dto.setSyserror(vo.getSysErrorTotal());
return dto;
}
/**
* DB 저장소에서 가져온 피크 성능 문자열을 PeakPerformanceDTO로 변환
*/
public PeakPerformanceDTO fromPeakPerformanceString(String peakDataStr, String type) {
PeakPerformanceDTO dto = new PeakPerformanceDTO();
if (peakDataStr == null || peakDataStr.isEmpty()) {
return dto;
}
String[] fields = peakDataStr.split(",");
if (fields.length < 4) {
return dto;
}
dto.setServiceType(fields[0]); // 서비스 유형
dto.setDate(fields[1]); // 날짜
try {
dto.setTransactionCount(Integer.parseInt(fields[2])); // 처리 건수
if( "--".equals(fields[3]) ){
dto.setTps(0);
} else {
dto.setTps(Integer.parseInt(fields[3])); // TPS
}
} catch (NumberFormatException e) {
dto.setTransactionCount(0);
dto.setTps(0);
}
return dto;
}
/**
* 일괄 전송 처리 데이터를 BatchProcessDTO로 변환
* @param csvData 형식: 송신성공,수신성공,송신에러,수신에러,송신대기,수신대기
*/
public BatchProcessDTO fromBatchProcessString(String serviceType, String csvData) {
BatchProcessDTO dto = new BatchProcessDTO();
dto.setServiceType(serviceType);
dto.setProcBatch(csvData); // 원본 CSV 데이터 설정
return dto;
}
/**
* 서버 인스턴스 정보를 포함하는 서버 상태 DTO 생성
*/
public ServerStatusDTO createServerStatus(DataSourceType server, Map<String, Object> serverData) {
ServerStatusDTO dto = new ServerStatusDTO();
dto.setName(server.getName());
dto.setText(server.getText());
// CPU, 메모리, 디스크 사용률 설정
if (serverData.containsKey("cpu")) {
dto.setCpu((int) serverData.get("cpu"));
}
if (serverData.containsKey("memory")) {
dto.setMemory((int) serverData.get("memory"));
}
if (serverData.containsKey("disk")) {
dto.setDisk((int) serverData.get("disk"));
}
// 서버 상태 설정
dto.setStatus(getStatusFromUsage(dto.getCpu(), dto.getMemory(), dto.getDisk()));
// 인스턴스 목록 설정
List<InstanceDTO> instances = new ArrayList<>();
if (serverData.containsKey("instances")) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> instanceDataList = (List<Map<String, Object>>) serverData.get("instances");
for (Map<String, Object> instanceData : instanceDataList) {
InstanceDTO instance = new InstanceDTO();
if (instanceData.containsKey("id")) {
instance.setId((String) instanceData.get("id"));
}
if (instanceData.containsKey("status")) {
instance.setStatus((String) instanceData.get("status"));
}
if (instanceData.containsKey("throughput")) {
instance.setThroughput((String) instanceData.get("throughput"));
}
instances.add(instance);
}
}
dto.setInstances(instances);
return dto;
}
/**
* 사용률에 따른 상태 반환
*/
private String getStatusFromUsage(int cpu, int memory, int disk) {
if (cpu > 90 || memory > 90 || disk > 90) {
return "error";
} else if (cpu > 70 || memory > 70 || disk > 70) {
return "warning";
} else {
return "normal";
}
}
/**
* 서버 이름과 인스턴스 목록을 기반으로 인스턴스별 상태를 생성
*/
public List<Map<String, Object>> createInstanceStatusList(String serverName, List<EAIEngineStatusVo> engineStatusList) {
List<Map<String, Object>> instanceStatusList = new ArrayList<>();
for (EAIEngineStatusVo engineStatus : engineStatusList) {
Map<String, Object> instanceStatus = new HashMap<>();
instanceStatus.put("serverName", serverName);
instanceStatus.put("instanceName", engineStatus.getInstanceName());
instanceStatus.put("status", getStatusTextFromCode(engineStatus.getServerStateImage()));
instanceStatus.put("cpu", engineStatus.getHeapFreePercent() > 0 ? 100 - engineStatus.getHeapFreePercent() : 0);
instanceStatus.put("memory", engineStatus.getHeapFreePercent() > 0 ? engineStatus.getHeapFreePercent() : 0);
instanceStatus.put("disk", engineStatus.getJmsFileStoreUseAge());
instanceStatus.put("throughput", engineStatus.getPendingRequestCurrentCount() + "/s");
instanceStatusList.add(instanceStatus);
}
return instanceStatusList;
}
/**
* AdaptersVO 목록을 AdapterStatusSummaryContainerDTO로 변환
* 어댑터 유형별 상태 집계 정보를 제공합니다.
*/
public AdapterStatusSummaryContainerDTO fromAdaptersVOList(List<AdaptersVO> adaptersList) {
AdapterStatusSummaryContainerDTO container = new AdapterStatusSummaryContainerDTO();
if (adaptersList == null || adaptersList.isEmpty()) {
return container;
}
// 어댑터의 상태를 집계
for (AdaptersVO vo : adaptersList) {
if (vo == null) continue;
// Socket 어댑터 상태 집계 (Socket과 Socket2를 하나로 통합)
container.getAdapterSummary("Socket").incrementStatusCount(vo.getSocket(3));
container.getAdapterSummary("Socket").incrementStatusCount(vo.getSocket2(3));
// HTTP 어댑터 상태 집계
container.getAdapterSummary("HTTP").incrementStatusCount(vo.getHttp(3));
}
// 전체 상태 업데이트
container.updateOverallStatus();
return container;
}
}
@@ -0,0 +1,169 @@
package com.eactive.eai.rms.onl.dashboard.modern.mapper;
import com.eactive.eai.rms.onl.dashboard.modern.dto.SmsNotificationDto;
import com.eactive.eai.rms.onl.vo.SmsVO;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* SMS 장애 통보 정보를 DTO로 변환하는 매퍼 클래스
*/
@Component
public class SmsNotificationMapper {
private static final Pattern DATE_TIME_PATTERN = Pattern.compile("(\\d{4}) (\\d{2}:\\d{2}:\\d{2}\\.\\d{3})");
private static final Pattern COUNT_PATTERN = Pattern.compile("\\[(\\d+)\\]");
/**
* SmsVO 목록을 SmsNotificationDto 목록으로 변환합니다.
*
* @param smsList SmsVO 목록
* @return SmsNotificationDto 목록
*/
public List<SmsNotificationDto> mapToSmsNotificationDtoList(List<SmsVO> smsList) {
List<SmsNotificationDto> dtoList = new ArrayList<>();
if (smsList == null || smsList.isEmpty()) {
return dtoList;
}
for (SmsVO smsVO : smsList) {
List<String> messages = smsVO.getMessage();
if (messages != null && !messages.isEmpty()) {
for (String msg : messages) {
SmsNotificationDto dto = parseMessage(msg);
if (dto != null) {
dto.setServiceType(smsVO.getService()); // SmsVO의 service 값을 serviceType으로 매핑
dtoList.add(dto);
}
}
}
}
return dtoList;
}
/**
* 메시지를 파싱하여 SmsNotificationDto 객체를 생성합니다.
*
* @param message 메시지 문자열
* @return SmsNotificationDto 객체
*/
private SmsNotificationDto parseMessage(String message) {
if (message == null || message.trim().isEmpty()) {
return null;
}
try {
String[] parts = message.split("\\|");
// Determine message type based on number of delimiters
boolean isAdapterError = parts.length == 3;
boolean isInterfaceError = parts.length == 2;
if (!isAdapterError && !isInterfaceError) {
return null;
}
SmsNotificationDto dto = new SmsNotificationDto();
// Parse the first part (common for both types)
String mainPart = parts[0];
// Extract date and time
Matcher dateTimeMatcher = DATE_TIME_PATTERN.matcher(mainPart);
if (dateTimeMatcher.find()) {
String mmdd = dateTimeMatcher.group(1);
String time = dateTimeMatcher.group(2);
String year = String.valueOf(LocalDate.now().getYear());
String dateTimeStr = year + mmdd + " " + time; // Assuming current year
LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr,
DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss.SSS"));
dto.setDateTime(dateTime);
}
// 당일 데이터가 아닌건은 skip
if( !LocalDate.now().equals(dto.getDateTime().toLocalDate()) ){
return null;
}
// Extract count
Matcher countMatcher = COUNT_PATTERN.matcher(mainPart);
if (countMatcher.find()) {
dto.setCount(Integer.parseInt(countMatcher.group(1)));
}
// Extract instance name (between [] and first space after it)
int instanceStart = mainPart.indexOf("]") + 1;
int errorStart = mainPart.indexOf(" ", instanceStart);
if (instanceStart > 0 && errorStart > instanceStart) {
dto.setInstanceName(mainPart.substring(instanceStart, errorStart));
}
// Extract error content
if (errorStart > 0) {
dto.setErrorContent(mainPart.substring(errorStart).trim());
}
if (isAdapterError) {
// Adapter error specific fields
dto.setAdapterDesc(parts[1].trim());
dto.setAdapterId(parts[2].trim());
dto.setErrorType("ADAPTER");
} else {
// Interface error specific fields
dto.setInterfaceId(parts[1].trim());
dto.setErrorType("INTERFACE");
}
return dto;
} catch (Exception e) {
// Log error if needed
return null;
}
}
public static void main(String[] args) {
SmsNotificationMapper mapper = new SmsNotificationMapper();
SmsVO smsVO = new SmsVO();
// 테스트할 메시지들
List<String> messages = Arrays.asList(
"0311 09:59:16.537[1]fep11 가용소켓연결없음|BASICTEST101S1",
"0311 10:05:39.779[49]fep11 Client커넥션오류|싱글어댑터 Prim|_TST_OU_NET_AsC{ALL}",
"0311 10:05:39.780[48]fep11 Client커넥션오류|비동기 표준 아|_TST_OU_NET_AsC{JSON_ALL}",
"0311 17:33:04.182[1]fep11 추적관리필드추출에러|BASICTEST101S1"
);
smsVO.setMessage(messages);
List<SmsNotificationDto> results = mapper.mapToSmsNotificationDtoList(Arrays.asList(smsVO));
System.out.println("=== SMS 메시지 파싱 결과 ===");
for (SmsNotificationDto dto : results) {
System.out.println("\n[메시지 정보]");
System.out.println("시간: " + dto.getDateTime());
System.out.println("건수: " + dto.getCount());
System.out.println("인스턴스: " + dto.getInstanceName());
System.out.println("에러내용: " + dto.getErrorContent());
System.out.println("에러유형: " + dto.getErrorType());
if ("ADAPTER".equals(dto.getErrorType())) {
System.out.println("어댑터설명: " + dto.getAdapterDesc());
System.out.println("어댑터ID: " + dto.getAdapterId());
} else {
System.out.println("인터페이스ID: " + dto.getInterfaceId());
}
System.out.println("------------------------");
}
}
}
@@ -0,0 +1,350 @@
package com.eactive.eai.rms.onl.dashboard.modern.service;
import com.eactive.eai.data.entity.onl.adapter.AdapterGroup;
import com.eactive.eai.data.entity.onl.server.EAIServer;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.data.entity.onl.adapter.AdapterGroupService;
import com.eactive.eai.rms.data.entity.onl.server.EAIServerService;
import com.eactive.eai.rms.onl.common.util.DataSetUtil;
import com.eactive.eai.rms.onl.common.vo.HostInfomationForm;
import com.eactive.eai.rms.onl.dashboard.modern.dto.InstitutionStatusDTO;
import com.eactive.eai.rms.onl.dashboard.modern.dto.SocketProcessDTO;
import com.eactive.eai.rms.onl.dashboard.modern.dto.SocketStatusDTO;
import com.eactive.eai.rms.onl.manage.adapter.adapter.mapper.AdapterGroupUIMapper;
import com.eactive.eai.rms.onl.manage.adapter.adapter.ui.AdapterGroupUI;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service for socket monitoring in modern dashboard
*/
@Service
public class SocketMonitorService {
private static final String AGENT_PAGE_STATUS = "adapter_socket2_agent1.jsp";
private static final String AGENT_PAGE_PROCESS = "adapter_socket2_agent2.jsp";
private final EAIServerService eaiServerService;
private final AdapterGroupService adapterGroupService;
private final AdapterGroupUIMapper adapterGroupUIMapper;
@Autowired
public SocketMonitorService(EAIServerService eaiServerService, AdapterGroupService adapterGroupService, AdapterGroupUIMapper adapterGroupUIMapper) {
this.eaiServerService = eaiServerService;
this.adapterGroupService = adapterGroupService;
this.adapterGroupUIMapper = adapterGroupUIMapper;
}
/**
* Get socket status data
* @param serverName Optional server name filter
* @param adapterGroup Optional adapter group filter
* @return SocketStatusDTO with all socket status data
*/
public SocketStatusDTO getSocketStatus(String serverName, String adapterGroup) {
try {
// Create a list to store all adapter data from all servers
List<HashMap<String, Object>> allAdapterData = new ArrayList<>();
// Loop through all online data source types
for (DataSourceType server : DataSourceTypeManager.getOnlineDataSourceTypes()) {
// Set server context
DataSourceContextHolder.setDataSourceType(server);
// Get server list for current data source
List<EAIServer> serverList = eaiServerService.selectEaiServerIpList();
if (serverList.isEmpty()) {
continue; // Skip if no servers found for this data source
}
List<String> urls = new ArrayList<>();
for (EAIServer serverMap : serverList) {
String serverUrl = "http://" + serverMap.getEaisevrip() + ":"
+ serverMap.getSevrlsnportname()
+ HostInfomationForm.WEB_AGENT_CONTEXT_PATH
+ AGENT_PAGE_STATUS + "?adapterType=SOC"
+ "&eaiSvrInstNm="
+ serverMap.getEaisevrinstncname();
urls.add(serverUrl);
}
// No filtering - get all data
DataSetUtil gUtil = new DataSetUtil();
List<HashMap<String, Object>> list = gUtil.flushXmlToClient(urls);
// Add to the combined list
allAdapterData.addAll(list);
}
// Reset the data source context
DataSourceContextHolder.clearDataSourceType();
// Convert the result to SocketStatusDTO
SocketStatusDTO result = new SocketStatusDTO();
result.setTotal(1);
result.setPage(1);
result.setRecords(allAdapterData.size());
// Convert HashMap list to SocketStatusItemDTO list
List<SocketStatusDTO.SocketStatusItemDTO> rows = new ArrayList<>();
for (HashMap<String, Object> item : allAdapterData) {
SocketStatusDTO.SocketStatusItemDTO row = new SocketStatusDTO.SocketStatusItemDTO();
// Map the fields from HashMap to DTO
row.setStatus((String) item.get("Status"));
row.setEaiSevrInstncName((String) item.get("EaiSevrInstncName"));
row.setTestMasterYn((String) item.get("TestMasterYn"));
row.setAdptrNickName((String) item.get("AdptrNickName"));
row.setStartStop((String) item.get("StartStop"));
row.setAdptrBzwkGroupName((String) item.get("AdptrBzwkGroupName"));
row.setAdptrBzwkName((String) item.get("AdptrBzwkName"));
row.setAdptrType((String) item.get("AdptrType"));
row.setStarting((String) item.get("Starting"));
row.setAdptrBzwkDesc((String) item.get("AdptrBzwkDesc"));
rows.add(row);
}
// Sort rows by AdptrBzwkGroupName in ascending order
rows.sort(Comparator.comparing(
row -> row.getAdptrBzwkGroupName() != null ? row.getAdptrBzwkGroupName() : ""
));
result.setRows(rows);
return result;
} catch (Exception e) {
throw new RuntimeException("Failed to get socket status data", e);
}
}
/**
* Get socket process data
* @param serverName Optional server name filter
* @param adapterGroup Optional adapter group filter
* @return SocketProcessDTO with all socket process data
*/
public SocketProcessDTO getSocketProcess(String serverName, String adapterGroup) {
try {
// Create a list to store all adapter data from all servers
List<HashMap<String, Object>> allAdapterData = new ArrayList<>();
// Loop through all online data source types
for (DataSourceType server : DataSourceTypeManager.getOnlineDataSourceTypes()) {
// Set server context
DataSourceContextHolder.setDataSourceType(server);
// Get server list for current data source
List<EAIServer> serverList = eaiServerService.selectEaiServerIpList();
if (serverList.isEmpty()) {
continue; // Skip if no servers found for this data source
}
List<String> urls = new ArrayList<>();
for (EAIServer serverMap : serverList) {
String serverUrl = "http://" + serverMap.getEaisevrip() + ":"
+ serverMap.getSevrlsnportname()
+ HostInfomationForm.WEB_AGENT_CONTEXT_PATH
+ AGENT_PAGE_PROCESS + "?cmd=get+connection+summary"
+ "&eaiSvrInstNm=" + serverMap.getEaisevrinstncname();
urls.add(serverUrl);
}
// No filtering - get all data
DataSetUtil gUtil = new DataSetUtil();
List<HashMap<String, Object>> list = gUtil.flushXmlToClient(urls);
// Add to the combined list
allAdapterData.addAll(list);
}
// Reset the data source context
DataSourceContextHolder.clearDataSourceType();
// Convert the result to SocketProcessDTO
SocketProcessDTO result = new SocketProcessDTO();
// Create JsonPageVoDTO
SocketProcessDTO.JsonPageVoDTO jsonPageVo = new SocketProcessDTO.JsonPageVoDTO();
jsonPageVo.setPage(1);
jsonPageVo.setTotalCount(allAdapterData.size());
jsonPageVo.setRows(10000);
jsonPageVo.setTotal(1);
jsonPageVo.setStartNum(1);
jsonPageVo.setEndNum(10000);
result.setJsonPageVo(jsonPageVo);
result.setTotal(1);
result.setPage(1);
result.setRecords(allAdapterData.size());
// Convert HashMap list to SocketProcessItemDTO list
List<SocketProcessDTO.SocketProcessItemDTO> rows = new ArrayList<>();
for (HashMap<String, Object> item : allAdapterData) {
SocketProcessDTO.SocketProcessItemDTO row = new SocketProcessDTO.SocketProcessItemDTO();
// Map the fields from HashMap to DTO
row.setMaxConnection((String) item.get("MaxConnection"));
row.setCurrentConnection((String) item.get("CurrentConnection"));
row.setStatus((String) item.get("Status"));
row.setEaiSevrInstncName((String) item.get("EaiSevrInstncName"));
row.setAdptrBzwkGroupName((String) item.get("AdptrBzwkGroupName"));
row.setRecvCount((String) item.get("RecvCount"));
row.setAdptrBzwkName((String) item.get("AdptrBzwkName"));
row.setAdptrBzwkDesc((String) item.get("AdptrBzwkDesc"));
row.setSendCount((String) item.get("SendCount"));
row.setErrorCount((String) item.get("ErrorCount"));
rows.add(row);
}
// Sort rows by AdptrBzwkGroupName in ascending order
rows.sort(Comparator.comparing(
row -> row.getAdptrBzwkGroupName() != null ? row.getAdptrBzwkGroupName() : ""
));
result.setRows(rows);
return result;
} catch (Exception e) {
throw new RuntimeException("Failed to get socket process data", e);
}
}
/**
* Get institution monitoring status data
* @param statusData Socket status data with adapter information
* @return List of institution status information
*/
public List<InstitutionStatusDTO> getInstitutionStatus(SocketStatusDTO statusData) {
try {
// Set context to access AdapterGroup data
DataSourceContextHolder.setDataSourceType(DataSourceTypeManager.getOnlineDataSourceTypes().get(0));
// Get institutions with non-empty adptrinsticode
List<AdapterGroup> adapterGroups = adapterGroupService.findByAdptrinsticodeNotEmpty();
List<AdapterGroupUI> adapterGroupUIs = adapterGroups.stream()
.map(adapterGroupUIMapper::toVoWithoutChildrenMapping)
.collect(Collectors.toList());
// Group adapter groups by institution code
Map<String, List<AdapterGroupUI>> institutionGroups = adapterGroupUIs.stream()
.filter(ag -> StringUtils.isNotBlank(ag.getAdptrinsticode()))
.collect(Collectors.groupingBy(AdapterGroupUI::getAdptrinsticode));
List<InstitutionStatusDTO> result = new ArrayList<>();
// Process each institution
for (Map.Entry<String, List<AdapterGroupUI>> entry : institutionGroups.entrySet()) {
String institutionCode = entry.getKey();
List<AdapterGroupUI> institutionAdapters = entry.getValue();
if (institutionAdapters.isEmpty()) {
continue;
}
// Use the first adapter's information for institution details
AdapterGroupUI firstAdapter = institutionAdapters.get(0);
// Create a new institution status DTO
InstitutionStatusDTO institutionStatus = new InstitutionStatusDTO();
institutionStatus.setInstitutionCode(institutionCode);
institutionStatus.setInstitutionName(firstAdapter.getAdptrinstiName());
institutionStatus.setContactPerson(firstAdapter.getOsidinstibzwkrsempname());
institutionStatus.setContactNumber(firstAdapter.getOsidinstitelno());
// Get adapter group names for this institution
List<String> adapterGroupNames = institutionAdapters.stream()
.map(AdapterGroupUI::getAdptrbzwkgroupname)
.collect(Collectors.toList());
// Count normal and error adapters
int normalCount = 0;
int errorCount = 0;
// Check status in statusData
if (statusData != null && statusData.getRows() != null) {
for (SocketStatusDTO.SocketStatusItemDTO statusItem : statusData.getRows()) {
if (adapterGroupNames.contains(statusItem.getAdptrBzwkGroupName())) {
if ("NORMAL".equals(statusItem.getStatus()) ||
"true".equals(statusItem.getStatus()) ||
"OK".equals(statusItem.getStatus())) {
normalCount++;
} else {
errorCount++;
}
}
}
}
institutionStatus.setNormalAdapterCount(normalCount);
institutionStatus.setErrorAdapterCount(errorCount);
// Determine line status
if (normalCount == 0 && errorCount == 0) {
institutionStatus.setLineStatus("UNDEFINED");
} else if (errorCount == 0 && normalCount > 0) {
institutionStatus.setLineStatus("NORMAL");
} else if (errorCount > 0 && normalCount > 0) {
institutionStatus.setLineStatus("WARN");
} else if (errorCount > 0 && normalCount == 0) {
institutionStatus.setLineStatus("ERROR");
}
result.add(institutionStatus);
}
// Reset data source context
DataSourceContextHolder.clearDataSourceType();
// Sort the result by status priority: ERROR > WARN > NORMAL > UNDEFINED
result.sort((a, b) -> {
// Define status priority (higher number = higher priority)
Map<String, Integer> statusPriority = new HashMap<>();
statusPriority.put("ERROR", 3);
statusPriority.put("WARN", 2);
statusPriority.put("NORMAL", 1);
statusPriority.put("UNDEFINED", 0);
// Get priority values for comparison
int priorityA = statusPriority.getOrDefault(a.getLineStatus(), 0);
int priorityB = statusPriority.getOrDefault(b.getLineStatus(), 0);
// Sort by priority (descending)
return Integer.compare(priorityB, priorityA);
});
return result;
} catch (Exception e) {
DataSourceContextHolder.clearDataSourceType();
throw new RuntimeException("Failed to get institution status data", e);
}
}
/**
* Get combined socket data (both status and process)
* @param serverName Optional server name filter
* @param adapterGroup Optional adapter group filter
* @return Map containing both socket status and process data
*/
public Map<String, Object> getCombinedSocketData(String serverName, String adapterGroup) {
Map<String, Object> result = new HashMap<>();
SocketStatusDTO statusData = getSocketStatus(serverName, adapterGroup);
SocketProcessDTO processData = getSocketProcess(serverName, adapterGroup);
List<InstitutionStatusDTO> institutionStatus = getInstitutionStatus(statusData);
result.put("socketStatus", statusData);
result.put("socketProcess", processData);
result.put("institutionStatus", institutionStatus);
return result;
}
}
@@ -0,0 +1,571 @@
package com.eactive.eai.rms.onl.dashboard.modern.service;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.onl.common.service.EaiServerInfoService;
import com.eactive.eai.rms.onl.dashboard.service.Dashboard2RepositoryService;
import com.eactive.eai.rms.onl.dashboard.service.DashboardService;
import com.eactive.eai.rms.onl.tracking.vo.MemStaticsVo;
import com.eactive.eai.rms.onl.vo.HostStatusVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 처리량 계산 서비스
* 인스턴스별 초당 처리량을 계산하여 저장합니다.
* 시계열 데이터를 유지하여 트렌드 분석에 사용합니다.
*/
@Slf4j
@Service
public class ThroughputCalculationService {
@Autowired
@Qualifier("eaiServerInfoService")
private EaiServerInfoService eaiServerInfoService;
@Autowired
@Qualifier("dashboard2RepositoryService")
private Dashboard2RepositoryService dashboard2Repository;
@Autowired
@Qualifier("dashboardService")
private DashboardService dashboardService;
// 인스턴스별 처리량 정보를 저장하는 작은 캐시
private final ConcurrentHashMap<String, ThroughputCacheInfo> throughputCache = new ConcurrentHashMap<>();
// 서비스 타입별 시계열 데이터를 저장하는
// : 서비스타입(EAI, FEP ), : TimeSeriesData 객체
private final ConcurrentHashMap<String, TimeSeriesData> timeSeriesMap = new ConcurrentHashMap<>();
// 서버별 리소스 사용량 시계열 데이터를 저장하는
// : 호스트명, : ServerResourceData 객체
private final ConcurrentHashMap<String, ServerResourceData> serverResourceMap = new ConcurrentHashMap<>();
// 서비스 타입별 마지막 누적 값을 저장하는
private final ConcurrentHashMap<String, Map<String, Integer>> lastAccumulatedValuesMap = new ConcurrentHashMap<>();
// 스케줄러
private ScheduledExecutorService scheduler;
// 시계열 데이터 저장 기간 ()
private static final int TIME_SERIES_RETENTION_MINUTES = 60;
// 서버 정보 시계열 데이터 저장 기간 ()
private static final int SERVER_STATUS_TIME_SERIES_RETENTION_MINUTES = 2;
// 시계열 데이터 샘플링 간격 ()
private static final int TIME_SERIES_SAMPLING_SECONDS = 1;
// 처리량 정보를 담는 내부 클래스 (캐시용)
private static class ThroughputCacheInfo {
private long timestamp; // 마지막 조회 시간
private int totalCount; // 마지막 누적 건수
public ThroughputCacheInfo(long timestamp, int totalCount) {
this.timestamp = timestamp;
this.totalCount = totalCount;
}
}
// 시계열 데이터를 담는 내부 클래스
public static class TimeSeriesData {
private List<Long> timestamps = new ArrayList<>(); // 타임스탬프 목록
private List<Integer> success = new ArrayList<>(); // 성공 건수
private List<Integer> fail = new ArrayList<>(); // 실패 건수
private List<Integer> timeout = new ArrayList<>(); // 타임아웃 건수
private List<Integer> syserror = new ArrayList<>(); // 시스템 에러 건수
// 시계열 데이터 추가
public void addDataPoint(long timestamp, int successCount, int failCount, int timeoutCount, int syserrorCount) {
timestamps.add(timestamp);
success.add(successCount);
fail.add(failCount);
timeout.add(timeoutCount);
syserror.add(syserrorCount);
// 오래된 데이터 정리 (최대 저장 기간을 초과하는 데이터 제거)
long cutoffTime = System.currentTimeMillis() - (TIME_SERIES_RETENTION_MINUTES * 60 * 1000);
while (!timestamps.isEmpty() && timestamps.get(0) < cutoffTime) {
timestamps.remove(0);
success.remove(0);
fail.remove(0);
timeout.remove(0);
syserror.remove(0);
}
}
// Getter 메서드
public List<Long> getTimestamps() {
return timestamps;
}
public List<Integer> getSuccess() {
return success;
}
public List<Integer> getFail() {
return fail;
}
public List<Integer> getTimeout() {
return timeout;
}
public List<Integer> getSyserror() {
return syserror;
}
}
// 서버 리소스 사용량 시계열 데이터를 담는 내부 클래스
public static class ServerResourceData {
private List<Long> timestamps = new ArrayList<>(); // 타임스탬프 목록
private List<Integer> cpu = new ArrayList<>(); // CPU 사용률
private List<Integer> memory = new ArrayList<>(); // 메모리 사용률
private List<Integer> disk = new ArrayList<>(); // 디스크 사용률
// 시계열 데이터 추가
public void addDataPoint(long timestamp, int cpuUsage, int memoryUsage, int diskUsage) {
timestamps.add(timestamp);
cpu.add(cpuUsage);
memory.add(memoryUsage);
disk.add(diskUsage);
}
public void removeExpiredDataPoint(){
// 오래된 데이터 정리 (최대 저장 기간을 초과하는 데이터 제거)
long cutoffTime = System.currentTimeMillis() - (SERVER_STATUS_TIME_SERIES_RETENTION_MINUTES * 60 * 1000);
while (!timestamps.isEmpty() && timestamps.get(0) < cutoffTime) {
timestamps.remove(0);
cpu.remove(0);
memory.remove(0);
disk.remove(0);
}
}
// Getter 메서드
public List<Long> getTimestamps() {
return timestamps;
}
public List<Integer> getCpu() {
return cpu;
}
public List<Integer> getMemory() {
return memory;
}
public List<Integer> getDisk() {
return disk;
}
// 최근 N개의 데이터 포인트 반환
public ServerResourceData getRecentDataPoints(int count) {
ServerResourceData result = new ServerResourceData();
int size = timestamps.size();
int startIndex = Math.max(0, size - count);
for (int i = startIndex; i < size; i++) {
result.timestamps.add(timestamps.get(i));
result.cpu.add(cpu.get(i));
result.memory.add(memory.get(i));
result.disk.add(disk.get(i));
}
return result;
}
}
/**
* 초기화 메서드
* 서비스 로드 스케줄러를 시작합니다.
*/
@PostConstruct
public void init() {
log.info("처리량 계산 서비스 초기화");
// 스케줄러 생성
scheduler = Executors.newScheduledThreadPool(2);
// 시계열 데이터 수집 작업 추가 (지정된 간격마다 실행)
scheduler.scheduleWithFixedDelay(this::collectTimeSeriesData, 5, TIME_SERIES_SAMPLING_SECONDS, TimeUnit.SECONDS);
// 서버 리소스 사용량 데이터 수집 작업 추가 (10초마다 실행)
scheduler.scheduleWithFixedDelay(this::collectServerResourceData, 3, 10, TimeUnit.SECONDS);
log.info("처리량 계산 스케줄러 시작됨");
// 서비스 타입 초기화
for (DataSourceType server : DataSourceTypeManager.getOnlineDataSourceTypes()) {
timeSeriesMap.putIfAbsent(server.getName(), new TimeSeriesData());
}
// BAT(배치) 서버 추가
DataSourceType batServer = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.BAT);
if (batServer != null) {
timeSeriesMap.putIfAbsent(batServer.getName(), new TimeSeriesData());
}
// BAP(일괄전송) 서버 추가
DataSourceType bapServer = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.BAP);
if (bapServer != null) {
timeSeriesMap.putIfAbsent(bapServer.getName(), new TimeSeriesData());
}
// 전체 데이터용 TOTAL 엔트리 추가
timeSeriesMap.putIfAbsent("TOTAL", new TimeSeriesData());
}
/**
* 종료 메서드
* 서비스 종료 스케줄러를 정리합니다.
*/
@PreDestroy
public void destroy() {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
log.info("처리량 계산 스케줄러 종료됨");
}
}
/**
* 서버 리소스 사용량 데이터 수집
* 10초마다 실행되어 서버의 CPU, 메모리, 디스크 사용량을 수집합니다.
*/
public void collectServerResourceData() {
try {
// 수집 시간
long currentTime = System.currentTimeMillis();
// 모든 온라인 데이터소스 타입에 대해 처리
List<DataSourceType> servers = DataSourceTypeManager.getOnlineDataSourceTypes();
// GW 추가
if( DataSourceTypeManager.getDataSourceType("FGW") != null ){
servers.add(DataSourceTypeManager.getDataSourceType("FGW"));
}
if( DataSourceTypeManager.getDataSourceType("CGW") != null ){
servers.add(DataSourceTypeManager.getDataSourceType("CGW"));
}
for (DataSourceType server : servers) {
// 서버 컨텍스트 설정
DataSourceContextHolder.setDataSourceType(server);
// 서버에 대한 인스턴스 목록 조회
List<Map<String, String>> ipList = eaiServerInfoService.getEaiServerIpListDB();
// 호스트명으로 인스턴스 그룹화
Map<String, List<Map<String, String>>> instancesByHost = new HashMap<>();
for (Map<String, String> instance : ipList) {
String hostname = instance.get("HOSTNAME");
instancesByHost.computeIfAbsent(hostname, k -> new ArrayList<>()).add(instance);
}
// 호스트별로 리소스 사용량 수집
for (Map.Entry<String, List<Map<String, String>>> entry : instancesByHost.entrySet()) {
String hostname = entry.getKey();
List<Map<String, String>> hostInstances = entry.getValue();
// 호스트 상태 조회
HostStatusVo hostVo = dashboardService.findHostStatus(hostInstances);
if (hostVo != null) {
// 서버 리소스 데이터 객체 가져오기 또는 생성
ServerResourceData resourceData = serverResourceMap.computeIfAbsent(hostname, k -> new ServerResourceData());
// 데이터 포인트 추가
resourceData.addDataPoint(currentTime, hostVo.getCpu(), hostVo.getMemory(), hostVo.getDisk());
}
}
}
// 기간 만료 데이터 삭제
serverResourceMap.values().forEach(ServerResourceData::removeExpiredDataPoint);
} catch (Exception e) {
log.error("서버 리소스 사용량 데이터 수집 중 오류 발생", e);
}
}
/**
* 시계열 데이터 수집
* 정해진 간격마다 실행되어 서비스 타입별 처리 건수를 수집합니다.
*/
public void collectTimeSeriesData() {
try {
// 수집 시간
long currentTime = System.currentTimeMillis();
// 서비스 타입별 현재 누적 데이터
Map<String, Integer> currentSuccessByService = new HashMap<>();
Map<String, Integer> currentFailByService = new HashMap<>();
Map<String, Integer> currentTimeoutByService = new HashMap<>();
Map<String, Integer> currentSyserrorByService = new HashMap<>();
// 전체 합계
int totalSuccess = 0;
int totalFail = 0;
int totalTimeout = 0;
int totalSyserror = 0;
// 모든 온라인 데이터소스 타입에 대해 처리
List<DataSourceType> servers = DataSourceTypeManager.getOnlineDataSourceTypes();
for (DataSourceType server : servers) {
// 서버 컨텍스트 설정
DataSourceContextHolder.setDataSourceType(server);
// 서버의 트랜잭션 정보 조회
MemStaticsVo memStatics = dashboard2Repository.getMemTradeVoByService(server.getName());
if (memStatics != null) {
// 현재 누적 계산
int currentSuccess = memStatics.getProcTotal() - memStatics.getTimeoutTotal() - memStatics.getErrorTotal() - memStatics.getSysErrorTotal();
int currentFail = memStatics.getErrorTotal();
int currentTimeout = memStatics.getTimeoutTotal();
int currentSyserror = memStatics.getSysErrorTotal();
// 서비스별 데이터 저장
currentSuccessByService.put(server.getName(), currentSuccess);
currentFailByService.put(server.getName(), currentFail);
currentTimeoutByService.put(server.getName(), currentTimeout);
currentSyserrorByService.put(server.getName(), currentSyserror);
// 전체 합계에 추가
totalSuccess += currentSuccess;
totalFail += currentFail;
totalTimeout += currentTimeout;
totalSyserror += currentSyserror;
}
}
// 서비스 타입별 시계열 데이터 업데이트
for (String serviceType : timeSeriesMap.keySet()) {
if (serviceType.equals("TOTAL")) {
// TOTAL 데이터는 아래에서 별도로 처리
continue;
}
// 현재 누적
int currentSuccess = currentSuccessByService.getOrDefault(serviceType, 0);
int currentFail = currentFailByService.getOrDefault(serviceType, 0);
int currentTimeout = currentTimeoutByService.getOrDefault(serviceType, 0);
int currentSyserror = currentSyserrorByService.getOrDefault(serviceType, 0);
// 마지막 누적 가져오기
Map<String, Integer> lastValues = lastAccumulatedValuesMap.getOrDefault(serviceType, new HashMap<>());
// 차이 계산
int deltaSuccess = currentSuccess - lastValues.getOrDefault("success", 0);
int deltaFail = currentFail - lastValues.getOrDefault("fail", 0);
int deltaTimeout = currentTimeout - lastValues.getOrDefault("timeout", 0);
int deltaSyserror = currentSyserror - lastValues.getOrDefault("syserror", 0);
// 인스턴스 재기동 감지 (누적치가 감소한 경우)
boolean isReset = false;
if (deltaSuccess < 0 || deltaFail < 0 || deltaTimeout < 0 || deltaSyserror < 0) {
isReset = true;
}
// 시계열 데이터에 추가할 결정
// 재기동된 경우 건수는 0으로 처리
int addSuccess = isReset ? 0 : Math.max(0, deltaSuccess);
int addFail = isReset ? 0 : Math.max(0, deltaFail);
int addTimeout = isReset ? 0 : Math.max(0, deltaTimeout);
int addSyserror = isReset ? 0 : Math.max(0, deltaSyserror);
// 시계열 데이터 추가
TimeSeriesData timeSeriesData = timeSeriesMap.get(serviceType);
if (timeSeriesData != null) {
timeSeriesData.addDataPoint(currentTime, addSuccess, addFail, addTimeout, addSyserror);
}
// 마지막 누적 업데이트 (현재 값으로 항상 갱신)
Map<String, Integer> newLastValues = new HashMap<>();
newLastValues.put("success", currentSuccess);
newLastValues.put("fail", currentFail);
newLastValues.put("timeout", currentTimeout);
newLastValues.put("syserror", currentSyserror);
lastAccumulatedValuesMap.put(serviceType, newLastValues);
}
// TOTAL 시계열 데이터 업데이트
TimeSeriesData totalTimeSeriesData = timeSeriesMap.get("TOTAL");
if (totalTimeSeriesData != null) {
// 마지막 누적 가져오기
Map<String, Integer> lastTotalValues = lastAccumulatedValuesMap.getOrDefault("TOTAL", new HashMap<>());
// 차이 계산
int deltaSuccess = totalSuccess - lastTotalValues.getOrDefault("success", 0);
int deltaFail = totalFail - lastTotalValues.getOrDefault("fail", 0);
int deltaTimeout = totalTimeout - lastTotalValues.getOrDefault("timeout", 0);
int deltaSyserror = totalSyserror - lastTotalValues.getOrDefault("syserror", 0);
// 인스턴스 재기동 감지
boolean isReset = false;
if (deltaSuccess < 0 || deltaFail < 0 || deltaTimeout < 0 || deltaSyserror < 0) {
isReset = true;
}
// 시계열 데이터에 추가할 결정
int addSuccess = isReset ? 0 : Math.max(0, deltaSuccess);
int addFail = isReset ? 0 : Math.max(0, deltaFail);
int addTimeout = isReset ? 0 : Math.max(0, deltaTimeout);
int addSyserror = isReset ? 0 : Math.max(0, deltaSyserror);
totalTimeSeriesData.addDataPoint(currentTime, addSuccess, addFail, addTimeout, addSyserror);
// 마지막 누적 업데이트
Map<String, Integer> newLastTotalValues = new HashMap<>();
newLastTotalValues.put("success", totalSuccess);
newLastTotalValues.put("fail", totalFail);
newLastTotalValues.put("timeout", totalTimeout);
newLastTotalValues.put("syserror", totalSyserror);
lastAccumulatedValuesMap.put("TOTAL", newLastTotalValues);
}
} catch (Exception e) {
log.error("시계열 데이터 수집 중 오류 발생", e);
}
}
/**
* 특정 인스턴스의 처리량 조회
* 실시간으로 계산하여 반환합니다.
* @param hostName 호스트명
* @param instanceName 인스턴스명
* @return 초당 처리량 문자열 (: "10.5/s")
*/
public String getThroughput(String hostName, String instanceName) {
try {
// 현재 처리 건수 조회
MemStaticsVo memStatics = dashboard2Repository.getMemTradeVoByHost(hostName, instanceName);
if (memStatics != null) {
// 현재 시간과 누적 건수
long currentTime = System.currentTimeMillis();
int currentTotal = memStatics.getProcTotal();
String instanceKey = hostName + "_" + instanceName;
// 이전 조회 데이터 가져오기
ThroughputCacheInfo prevInfo = throughputCache.get(instanceKey);
double throughput = 0.0;
boolean isReset = false;
if (prevInfo != null) {
// 경과 시간 계산 ( 단위)
double elapsedSeconds = (currentTime - prevInfo.timestamp) / 1000.0;
// 최소 0.1초 이상 경과했을 때만 계산
if (elapsedSeconds >= 0.1) {
int processedCount = currentTotal - prevInfo.totalCount;
// 처리 건수가 음수이면 인스턴스 재기동으로 간주
if (processedCount < 0) {
processedCount = 0;
isReset = true;
}
throughput = processedCount / elapsedSeconds;
}
}
// 캐시 업데이트
throughputCache.put(instanceKey, new ThroughputCacheInfo(currentTime, currentTotal));
// 처리량 문자열 반환
return String.format("%.1f/s", throughput);
}
} catch (Exception e) {
log.error("처리량 계산 중 오류 발생", e);
}
return "0/s";
}
/**
* 특정 서버의 리소스 사용량 시계열 데이터 조회
* @param hostname 호스트명
* @param count 조회할 데이터 포인트 (최대 7개)
* @return 서버 리소스 사용량 시계열 데이터
*/
public ServerResourceData getServerResourceData(String hostname, int count) {
// 데이터 포인트 제한 (최대 7개)
count = Math.min(count, 7);
ServerResourceData resourceData = serverResourceMap.get(hostname);
if (resourceData != null) {
return resourceData.getRecentDataPoints(count);
} else {
return new ServerResourceData();
}
}
/**
* 특정 서비스 타입의 시계열 데이터 조회
* @param serviceType 서비스 타입 (EAI, FEP )
* @param minutes 조회할 단위 기간 (최대 60분)
* @return 시계열 데이터
*/
public TimeSeriesData getTimeSeriesData(String serviceType, int minutes) {
// 기간 제한 (1~60분)
minutes = Math.min(Math.max(minutes, 1), TIME_SERIES_RETENTION_MINUTES);
// 기본적으로 요청된 서비스 타입의 시계열 데이터 사용
TimeSeriesData originalData = timeSeriesMap.get(serviceType);
// 서비스 타입이 없거나 "ALL" 경우 TOTAL 데이터 사용
if (originalData == null || "ALL".equalsIgnoreCase(serviceType) || "all".equals(serviceType)) {
originalData = timeSeriesMap.get("TOTAL");
}
// 데이터가 없으면 데이터 반환
if (originalData == null) {
return new TimeSeriesData();
}
// 기간에 맞게 데이터 필터링
TimeSeriesData filteredData = new TimeSeriesData();
long cutoffTime = System.currentTimeMillis() - (minutes * 60 * 1000);
List<Long> originalTimestamps = originalData.getTimestamps();
List<Integer> originalSuccess = originalData.getSuccess();
List<Integer> originalFail = originalData.getFail();
List<Integer> originalTimeout = originalData.getTimeout();
List<Integer> originalSyserror = originalData.getSyserror();
// 최근 데이터부터 시작하여 지정된 기간 내의 데이터만 필터링
for (int i = originalTimestamps.size() - 1; i >= 0; i--) {
if (originalTimestamps.get(i) >= cutoffTime) {
filteredData.timestamps.add(0, originalTimestamps.get(i));
filteredData.success.add(0, originalSuccess.get(i));
filteredData.fail.add(0, originalFail.get(i));
filteredData.timeout.add(0, originalTimeout.get(i));
filteredData.syserror.add(0, originalSyserror.get(i));
} else {
// 시간 범위를 벗어나면 종료
break;
}
}
return filteredData;
}
}
@@ -0,0 +1,334 @@
package com.eactive.eai.rms.onl.dashboard.modern.service;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.onl.common.service.EaiServerInfoService;
import com.eactive.eai.rms.onl.dashboard.service.Dashboard2RepositoryService;
import com.eactive.eai.rms.onl.server.repository.TransactionRepositoryService;
import com.eactive.eai.rms.onl.vo.TransactionTrackingVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 거래현황 집계 서비스
* 인터페이스별 10초 단위 처리량을 계산하여 저장합니다.
* 시계열 데이터를 유지하여 트렌드 분석에 사용합니다.
*/
@Slf4j
@Service
public class TransactionSummaryService {
@Autowired
@Qualifier("dashboard2RepositoryService")
private Dashboard2RepositoryService dashboard2Repository;
@Autowired
@Qualifier("transactionRepositoryService")
private TransactionRepositoryService transactionRepositoryService;
@Autowired
private EaiServerInfoService eaiServerInfoService;
// 인터페이스별 처리량 정보를 저장하는
// : 서비스명, : 시계열 데이터
private final ConcurrentHashMap<String, TimeSeriesData> timeSeriesMap = new ConcurrentHashMap<>();
// 서비스별 마지막 누적 값을 저장하는
private final ConcurrentHashMap<String, Map<String, Integer>> lastAccumulatedValuesMap = new ConcurrentHashMap<>();
// 스케줄러
private ScheduledExecutorService scheduler;
// 시계열 데이터 저장 기간 ()
private static final int TIME_SERIES_RETENTION_SECONDS = 120;
// 시계열 데이터 샘플링 간격 ()
private static final int TIME_SERIES_SAMPLING_SECONDS = 10;
// 시계열 데이터를 담는 내부 클래스
public static class TimeSeriesData {
private List<Long> timestamps = new ArrayList<>(); // 타임스탬프 목록
private List<Integer> success = new ArrayList<>(); // 성공 건수
private List<Integer> error = new ArrayList<>(); // 오류 건수
private List<Double> avgTime = new ArrayList<>(); // 평균 소요시간
// 시계열 데이터 추가
public void addDataPoint(long timestamp, int successCount, int errorCount, double avgProcessTime) {
timestamps.add(timestamp);
success.add(successCount);
error.add(errorCount);
avgTime.add(avgProcessTime);
// 오래된 데이터 정리 (최대 저장 기간을 초과하는 데이터 제거)
long cutoffTime = System.currentTimeMillis() - (TIME_SERIES_RETENTION_SECONDS * 1000);
while (!timestamps.isEmpty() && timestamps.get(0) < cutoffTime) {
timestamps.remove(0);
success.remove(0);
error.remove(0);
avgTime.remove(0);
}
}
// Getter 메서드들
public List<Long> getTimestamps() {
return timestamps;
}
public List<Integer> getSuccess() {
return success;
}
public List<Integer> getError() {
return error;
}
public List<Double> getAvgTime() {
return avgTime;
}
// 특정 기간 동안의 합계 계산
public Map<String, Integer> calculateTotalCounts() {
int totalSuccess = 0;
int totalError = 0;
for (int i = 0; i < success.size(); i++) {
totalSuccess += success.get(i);
totalError += error.get(i);
}
Map<String, Integer> totals = new HashMap<>();
totals.put("success", totalSuccess);
totals.put("error", totalError);
return totals;
}
}
/**
* 초기화 메서드
* 서비스 로드 스케줄러를 시작합니다.
*/
@PostConstruct
public void init() {
log.info("거래현황 집계 서비스 초기화");
// 스케줄러 생성
scheduler = Executors.newScheduledThreadPool(1);
// 시계열 데이터 수집 작업 추가 (10초마다 실행)
scheduler.scheduleWithFixedDelay(this::collectTimeSeriesData, 0, TIME_SERIES_SAMPLING_SECONDS, TimeUnit.SECONDS);
log.info("거래현황 집계 스케줄러 시작됨");
// 전체 데이터용 TOTAL 엔트리 추가
timeSeriesMap.putIfAbsent("TOTAL", new TimeSeriesData());
}
/**
* 종료 메서드
* 서비스 종료 스케줄러를 정리합니다.
*/
@PreDestroy
public void destroy() {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
log.info("거래현황 집계 스케줄러 종료됨");
}
}
/**
* 시계열 데이터 수집
* 10초마다 실행되어 서비스별 처리 건수를 수집합니다.
*/
public void collectTimeSeriesData() {
try {
// 수집 시간
long currentTime = System.currentTimeMillis();
// 서비스별 현재 누적 데이터
Map<String, Integer> currentSuccessByService = new HashMap<>();
Map<String, Integer> currentErrorByService = new HashMap<>();
Map<String, Double> currentProcessingTimeByService = new HashMap<>();
// 전체 합계
int totalSuccess = 0;
int totalError = 0;
double totalProcessingTime = 0.0;
int totalCount = 0;
// 모든 온라인 데이터소스 타입에 대해 처리
List<DataSourceType> servers = DataSourceTypeManager.getOnlineDataSourceTypes();
log.debug("온라인 DataSource 개수: {}", servers.size());
for (DataSourceType ds : servers) {
log.debug(" - DataSource: {}", ds.getName());
}
for (DataSourceType server : servers) {
try {
// 서버 컨텍스트 설정
log.debug("DataSource 전환: {}", server.getName());
DataSourceContextHolder.setDataSourceType(server);
// 서버에 대한 인스턴스 목록 조회
List<Map<String, String>> serverList = eaiServerInfoService.getEaiServerIpListDB();
log.debug(" - 서버 목록 개수: {}", serverList != null ? serverList.size() : 0);
if (serverList == null || serverList.isEmpty()) {
log.debug(" - {} DataSource에 대한 서버 목록이 없습니다. 건너뜁니다.", server.getName());
continue;
}
for (Map<String, String> serverInfo : serverList) {
// 트랜잭션 정보 조회를 위한 VO 생성
TransactionTrackingVo origin = new TransactionTrackingVo();
origin.setHostName(serverInfo.get("HOSTNAME"));
origin.setInstanceName(serverInfo.get("EAISVRINSTNM"));
// 트랜잭션 정보 조회
TransactionTrackingVo vo = transactionRepositoryService.getTrackingVo(origin);
if (vo != null) {
// 현재 누적 계산
int currentSuccess = vo.getTotalProc();
int currentError = vo.getTotalError();
double timeTotal = vo.getTimeTotal();
// 서비스별 데이터 저장 (서버명_인스턴스명 형식으로 생성)
String serviceKey = origin.getHostName() + "_" + origin.getInstanceName();
currentSuccessByService.put(serviceKey, currentSuccess);
currentErrorByService.put(serviceKey, currentError);
currentProcessingTimeByService.put(serviceKey, timeTotal);
// 전체 합계에 추가
totalSuccess += currentSuccess;
totalError += currentError;
totalProcessingTime += timeTotal;
totalCount++;
}
}
} catch (Exception e) {
log.error("DataSource {} 처리 중 오류 발생: {}", server.getName(), e.getMessage(), e);
} finally {
// DataSource 컨텍스트 클리어
DataSourceContextHolder.clearDataSourceType();
}
}
// 서비스 타입별 시계열 데이터 업데이트
for (String serviceType : currentSuccessByService.keySet()) {
// 현재 누적
int currentSuccess = currentSuccessByService.get(serviceType);
int currentError = currentErrorByService.get(serviceType);
double processingTime = currentProcessingTimeByService.get(serviceType);
// 마지막 누적 가져오기
Map<String, Integer> lastValues = lastAccumulatedValuesMap.getOrDefault(serviceType, new HashMap<>());
// 차이 계산
int deltaSuccess = currentSuccess - lastValues.getOrDefault("success", 0);
int deltaError = currentError - lastValues.getOrDefault("error", 0);
// 음수 방지 (카운터가 리셋된 경우)
deltaSuccess = Math.max(0, deltaSuccess);
deltaError = Math.max(0, deltaError);
// 평균 처리시간 계산
double avgProcessingTime = (deltaSuccess + deltaError > 0) ? processingTime / (deltaSuccess + deltaError) : 0.0;
// 시계열 데이터 추가
TimeSeriesData timeSeriesData = timeSeriesMap.computeIfAbsent(serviceType, k -> new TimeSeriesData());
timeSeriesData.addDataPoint(currentTime, deltaSuccess, deltaError, avgProcessingTime);
// 마지막 누적 업데이트
Map<String, Integer> newLastValues = new HashMap<>();
newLastValues.put("success", currentSuccess);
newLastValues.put("error", currentError);
lastAccumulatedValuesMap.put(serviceType, newLastValues);
}
// TOTAL 시계열 데이터 업데이트
TimeSeriesData totalTimeSeriesData = timeSeriesMap.get("TOTAL");
if (totalTimeSeriesData != null && totalCount > 0) {
// 마지막 누적 가져오기
Map<String, Integer> lastTotalValues = lastAccumulatedValuesMap.getOrDefault("TOTAL", new HashMap<>());
// 차이 계산
int deltaSuccess = totalSuccess - lastTotalValues.getOrDefault("success", 0);
int deltaError = totalError - lastTotalValues.getOrDefault("error", 0);
// 음수 방지
deltaSuccess = Math.max(0, deltaSuccess);
deltaError = Math.max(0, deltaError);
// 평균 처리시간 계산
double avgProcessingTime = (deltaSuccess + deltaError > 0) ? totalProcessingTime / (deltaSuccess + deltaError) : 0.0;
totalTimeSeriesData.addDataPoint(currentTime, deltaSuccess, deltaError, avgProcessingTime);
// 마지막 누적 업데이트
Map<String, Integer> newLastTotalValues = new HashMap<>();
newLastTotalValues.put("success", totalSuccess);
newLastTotalValues.put("error", totalError);
lastAccumulatedValuesMap.put("TOTAL", newLastTotalValues);
}
} catch (Exception e) {
log.error("시계열 데이터 수집 중 오류 발생: {}", e.getMessage(), e);
}
}
/**
* 시계열 데이터 조회
* @param seconds 조회할 단위 기간 (최대 120초)
* @return 시계열 데이터
*/
public TimeSeriesData getTimeSeriesData(int seconds) {
// 기간 제한 (1~120초)
seconds = Math.min(Math.max(seconds, 1), TIME_SERIES_RETENTION_SECONDS);
// TOTAL 데이터 사용
TimeSeriesData originalData = timeSeriesMap.get("TOTAL");
// 데이터가 없으면 데이터 반환
if (originalData == null) {
return new TimeSeriesData();
}
// 기간에 맞게 데이터 필터링
TimeSeriesData filteredData = new TimeSeriesData();
long cutoffTime = System.currentTimeMillis() - (seconds * 1000);
List<Long> originalTimestamps = originalData.getTimestamps();
List<Integer> originalSuccess = originalData.getSuccess();
List<Integer> originalError = originalData.getError();
List<Double> originalAvgTime = originalData.getAvgTime();
// 최근 데이터부터 시작하여 지정된 기간 내의 데이터만 필터링
for (int i = originalTimestamps.size() - 1; i >= 0; i--) {
if (originalTimestamps.get(i) >= cutoffTime) {
filteredData.timestamps.add(0, originalTimestamps.get(i));
filteredData.success.add(0, originalSuccess.get(i));
filteredData.error.add(0, originalError.get(i));
filteredData.avgTime.add(0, originalAvgTime.get(i));
} else {
// 시간 범위를 벗어나면 종료
break;
}
}
return filteredData;
}
}
@@ -1,142 +1,142 @@
package com.eactive.eai.rms.onl.dashboard.repository;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import javax.annotation.PostConstruct;
import org.apache.commons.collections.map.MultiKeyMap;
import org.apache.log4j.Logger;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Repository;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.onl.tracking.vo.DbStaticsVo;
import com.eactive.eai.rms.onl.tracking.vo.MemStaticsVo;
import com.eactive.eai.rms.onl.vo.SmsVO;
import org.apache.commons.collections.map.MultiKeyMap;
import org.apache.log4j.Logger;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Repository;
import javax.annotation.PostConstruct;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
@Scope(value = "singleton")
@Repository("dashboard2Repository")
public class Dashboard2Repository implements Serializable {
/** The Constant serialVersionUID. */
private static final long serialVersionUID = -8968729820736492173L;
/** The Constant serialVersionUID. */
private static final long serialVersionUID = -8968729820736492173L;
/** The Constant logger. */
private static final Logger logger = Logger.getLogger(Dashboard2Repository.class);
// 서비스 -> Map(EAI 서비스코드,서버구분코드)
private HashMap<String,HashMap<String,String>> repoEAISevrDstcd;
// 대시보드 거래건수 ( 메모리 버전 )
private MultiKeyMap repoMemTransaction;
// 서비스, 호스트 -> vo
/** The Constant logger. */
private static final Logger logger = Logger.getLogger(Dashboard2Repository.class);
// 서비스 -> Map(EAI 서비스코드,서버구분코드)
private HashMap<String,HashMap<String,String>> repoEAISevrDstcd;
// 대시보드 거래건수 ( 메모리 버전 )
private MultiKeyMap repoMemTransaction;
// 서비스, 호스트 -> vo
private MultiKeyMap repository;
// 서비스 -> 호스트네임 리스트
private HashMap<String,List<String>> repoHostNames;
// 호스트명 -> 인스턴스네임 리스트
private HashMap<String,List<String>> repoInstNames;
// 호스트명 -> datasource, 인스턴스네임 list
private HashMap<String, Map<String, List<String>>> repoDataSourceInstNames;
// DB 저장된 통계정보 최대값.
private String maxProcTime;
// 호스트 telnet 연결 성공 여부 ( ip -> "1/0" )
private HashMap<String,String> repoHostAlive;
// SMS 메시지 리스트
private List<SmsVO> repoSms;
// Peak Trade by TPS( service -> date,trade,tps )
private HashMap<String,String> repoPeakTps;
// Peak Trade by Trade ( service -> date,trade,tps )
private HashMap<String,String> repoPeakTrade;
// 일괄전송 전송현황 건수 ( 정상송신,정상수신,에러송신,에러수신,대기송신,대기수신)
private int[] procBatch = new int[6];
private int[] procEaiBatch = new int[6];
//ITSM 대시보드 연동 거래처리 집계
private HashMap<String,HashMap<String,String>> itsmDashTranInfo;
//ITSM PEAK DAY 정보
private HashMap<String,HashMap<String,String>> itsmDashPeakdayInfo;
//ITSM 대시보드 연동 거래처리 집계
private HashMap<String,HashMap<String,String>> itsmDashTranInfo;
//ITSM PEAK DAY 정보
private HashMap<String,HashMap<String,String>> itsmDashPeakdayInfo;
//ITSM 대시보드 연동 날짜별 거래건수
private HashMap<String,List> itsmDashDayTranList;
//ITSM 대시보드 연동 날짜별 거래건수
private HashMap<String,List> itsmDashHourTranList;
//ITSM 대시보드 연동 서비스별 업무에러 MAP
private HashMap<String,HashMap<String,String>> itsmDashBizErrorMap;
//ITSM 대시보드 연동 서비스별 타임아웃에러 MAP
private HashMap<String,HashMap<String,String>> itsmDashTimeoutErrorMap;
//ITSM 통신에러 MAP
private HashMap<String,HashMap<String,String>> itsmDashConnectErrorMap;
//ITSM 대시비도연동 서버 인스턴스별 거래건수
private HashMap<String,HashMap<String,String>> itsmDashServerInstTotalProcMap;
private String itsmDashTranInfoBatch;
private String itsmDashTranInfoBatch2;
private HashMap<String, HashMap<String, String>> itsmDashEaiBatchErrorList;
private HashMap<String, HashMap<String, String>> itsmDashFepBatchErrorList;
private HashMap<String,List> itsmDashDayTranList;
//ITSM 대시보드 연동 날짜별 거래건수
private HashMap<String,List> itsmDashHourTranList;
//ITSM 대시보드 연동 서비스별 업무에러 MAP
private HashMap<String,HashMap<String,String>> itsmDashBizErrorMap;
//ITSM 대시보드 연동 서비스별 타임아웃에러 MAP
private HashMap<String,HashMap<String,String>> itsmDashTimeoutErrorMap;
//ITSM 통신에러 MAP
private HashMap<String,HashMap<String,String>> itsmDashConnectErrorMap;
//ITSM 대시비도연동 서버 인스턴스별 거래건수
private HashMap<String,HashMap<String,String>> itsmDashServerInstTotalProcMap;
private String itsmDashTranInfoBatch;
private String itsmDashTranInfoBatch2;
private HashMap<String, HashMap<String, String>> itsmDashEaiBatchErrorList;
private HashMap<String, HashMap<String, String>> itsmDashFepBatchErrorList;
// 서비스별로 집계정보 저장
// private Dashboard2ProcVo inTotalVo;
// private Dashboard2ProcVo outTotalVo;
// private Dashboard2ProcVo commTotalVo;
// private Dashboard2ProcVo dmzTotalVo;
// private Dashboard2ProcVo batchTotalVo;
//
//
// private String lastTime; // 최종으로 저장된 startTime(모든 서비스에 공통)
/**
* 저장소 초기화 한다.
*/
@PostConstruct
public void createRepository() {
public void createRepository() {
repoEAISevrDstcd = new HashMap<String,HashMap<String,String>>();
repoMemTransaction = new MultiKeyMap();
repository = new MultiKeyMap();
repoHostNames = new HashMap<String,List<String>>();
repoInstNames = new HashMap<String,List<String>>();
repoHostAlive = new HashMap<String,String>();
repoSms = new ArrayList<SmsVO>();
repoPeakTps = new HashMap<String,String>();
repoPeakTrade = new HashMap<String,String>();
itsmDashTranInfo = new HashMap<String,HashMap<String,String>>();
itsmDashPeakdayInfo = new HashMap<String,HashMap<String,String>>();
itsmDashHourTranList = new HashMap<String,List>();
itsmDashDayTranList = new HashMap<String,List>();
itsmDashBizErrorMap = new HashMap<String,HashMap<String,String>>();
itsmDashTimeoutErrorMap = new HashMap<String,HashMap<String,String>>();
itsmDashConnectErrorMap = new HashMap<String,HashMap<String,String>>();
itsmDashServerInstTotalProcMap = new HashMap<String,HashMap<String,String>>();
itsmDashEaiBatchErrorList = new HashMap<String,HashMap<String,String>>();
itsmDashFepBatchErrorList = new HashMap<String,HashMap<String,String>>();
}
repository = new MultiKeyMap();
repoHostNames = new HashMap<String,List<String>>();
repoInstNames = new HashMap<String,List<String>>();
repoDataSourceInstNames = new HashMap<String,Map<String,List<String>>>();
repoHostAlive = new HashMap<String,String>();
repoSms = new ArrayList<SmsVO>();
repoPeakTps = new HashMap<String,String>();
repoPeakTrade = new HashMap<String,String>();
itsmDashTranInfo = new HashMap<String,HashMap<String,String>>();
itsmDashPeakdayInfo = new HashMap<String,HashMap<String,String>>();
itsmDashHourTranList = new HashMap<String,List>();
itsmDashDayTranList = new HashMap<String,List>();
itsmDashBizErrorMap = new HashMap<String,HashMap<String,String>>();
itsmDashTimeoutErrorMap = new HashMap<String,HashMap<String,String>>();
itsmDashConnectErrorMap = new HashMap<String,HashMap<String,String>>();
itsmDashServerInstTotalProcMap = new HashMap<String,HashMap<String,String>>();
itsmDashEaiBatchErrorList = new HashMap<String,HashMap<String,String>>();
itsmDashFepBatchErrorList = new HashMap<String,HashMap<String,String>>();
}
/**
* 저장소 데이터를 리셋 한다.
*/
@@ -146,30 +146,31 @@ public class Dashboard2Repository implements Serializable {
repository.clear();
repoHostNames.clear();
repoInstNames.clear();
repoDataSourceInstNames.clear();
repoHostAlive.clear();
repoSms.clear();
repoPeakTps.clear();
repoPeakTrade.clear();
for( int i = 0 ; i < 6 ; i++ ) {
procBatch[i] = 0 ;
procEaiBatch[i] = 0 ;
}
}
public void initMemTrRepository() {
repoMemTransaction.clear();
repoMemTransaction = new MultiKeyMap();
}
public void checkMemTrRepository() {
logger.info("check repoMemTransaction size=" + repoMemTransaction.size() );
}
public void removeMemTrRepository(String host, String inst) {
repoMemTransaction.remove(host, inst);
}
/**
* 주어진 서비스의 DB 에서 읽어온 EAI서비스코드,서버구분코드 맵을 리턴한다.
* @param service
@@ -178,7 +179,7 @@ public class Dashboard2Repository implements Serializable {
public HashMap<String,String> getRepoEAIServrDstcd(String service) {
return repoEAISevrDstcd.get(service);
}
/**
* 주어진 서비스명으로 메모리에서 읽어온 맵을 리턴한다.
* @param service
@@ -187,12 +188,12 @@ public class Dashboard2Repository implements Serializable {
public HashMap<String,String> getItsmDashTranInfo(String service) {
return itsmDashTranInfo.get(service);
}
public HashMap<String,String> getItsmDashPeakdayInfo(String service) {
return itsmDashPeakdayInfo.get(service);
}
/**
* 주어진 서비스명으로 메모리에서 읽어온 리스트을 리턴한다.
* @param service
@@ -201,35 +202,35 @@ public class Dashboard2Repository implements Serializable {
public List itsmDashHourTranList(String service) {
return itsmDashHourTranList.get(service);
}
public List itsmDashDayTranList(String service) {
return itsmDashDayTranList.get(service);
}
public HashMap<String, HashMap<String, String>> itsmDashBizErrorMap() {
return itsmDashBizErrorMap;
}
public HashMap<String, HashMap<String, String>> itsmDashTimeoutErrorMap() {
return itsmDashTimeoutErrorMap;
}
public HashMap<String, HashMap<String, String>> itsmDashConnectErrorMap() {
return itsmDashConnectErrorMap;
}
public HashMap<String,String> itsmDashServerInstTotalProcMap(String service) {
return itsmDashServerInstTotalProcMap.get(service);
}
public HashMap<String,HashMap<String,String>> itsmDashEaiBatchErrorList() {
return itsmDashEaiBatchErrorList;
}
public HashMap<String,HashMap<String,String>> itsmDashFepBatchErrorList() {
return itsmDashFepBatchErrorList;
}
/**
* 주어진 host 명이 포함된 DB 에서 읽어온 EAI서비스코드,서버구분코드 맵을 리턴한다.
* @param host
@@ -240,19 +241,19 @@ public class Dashboard2Repository implements Serializable {
// if( service != null ) {
// return getRepoEAIServrDstcd(service.getKey());
// }
//
//
// return null ;
// }
//TODO JUN
//TODO JUN
public HashMap<String,String> getRepoEAIServrDstcdByHost(String host) {
DataSourceType dataSourceType = getServiceTypeByHost(host);
if( dataSourceType != null ) {
return getRepoEAIServrDstcd(dataSourceType.getName());
}
return null ;
}
/**
* 주어진 host 명이 포함된 DB 서비스 타입을 찾아서 리턴한다.
* @param host
@@ -269,10 +270,10 @@ public class Dashboard2Repository implements Serializable {
// }
// }
// }
//
//
// return null ;
// }
//TODO JUN
//TODO JUN
public DataSourceType getServiceTypeByHost(String host) {
Iterator<String> it = repoHostNames.keySet().iterator();
while( it.hasNext() ) {
@@ -284,9 +285,9 @@ public class Dashboard2Repository implements Serializable {
}
}
}
return null ;
}
}
/**
* Repository 주어진 서비스를 키로 하여 EAI서비스코드,서버구분코드 맵을 추가한다.
* @param service
@@ -295,7 +296,7 @@ public class Dashboard2Repository implements Serializable {
public void setRepoEAISevrDstcd(String service, HashMap<String,String> map) {
repoEAISevrDstcd.put(service, map);
}
/**
* Repository 주어진 서비스를 키로 하여 메모리 거래현황 맵을 추가한다.
* @param service
@@ -304,45 +305,45 @@ public class Dashboard2Repository implements Serializable {
public void setItsmDashTranInfo(String service, HashMap<String,String> map) {
itsmDashTranInfo.put(service, map);
}
public void setItsmDashPeakdayInfo(String service, HashMap<String,String> map) {
itsmDashPeakdayInfo.put(service, map);
}
/**
* Repository 주어진 서비스를 키로 하여 메모리 시간별거래건수 맵을 추가한다.
* @param service
* @param map
*/
public void setItsmDashHourTranList(String service, List list) {
itsmDashHourTranList.put(service, list);
}
/**
* Repository 주어진 서비스를 키로 하여 메모리 시간별거래건수 맵을 추가한다.
* @param service
* @param map
*/
public void setItsmDashDayTranList(String service, List list) {
itsmDashDayTranList.put(service, list);
}
/**
* Repository 주어진 서비스를 키로 하여 메모리 업무에러리스트 맵을 추가한다.
* @param service
* @param map
*/
public void setItsmDashBizErrorMap(HashMap<String,HashMap<String, String>> map) {
this.itsmDashBizErrorMap = map;
}
/**
* Repository 주어진 서비스를 키로 하여 메모리 타임아웃에러리스트 맵을 추가한다.
* @param service
@@ -351,38 +352,38 @@ public class Dashboard2Repository implements Serializable {
public void setItsmDashTimeoutErrorMap(HashMap<String, HashMap<String, String>> map) {
this.itsmDashTimeoutErrorMap = map;
}
public void setItsmDashConnectErrorMap(HashMap<String, HashMap<String, String>> map) {
this.itsmDashConnectErrorMap = map;
}
public void setItsmDashServerInstTotalProcMap(String service ,HashMap<String,String> map) {
this.itsmDashServerInstTotalProcMap.put(service, map);
}
public void setItsmDashTranInfoBatch( String batchCount ) {
this.itsmDashTranInfoBatch = batchCount ;
}
public String getItsmDashTranInfoBatch() {
return this.itsmDashTranInfoBatch;
}
public void setItsmDashTranInfoBatch2( String batchCount2 ) {
this.itsmDashTranInfoBatch2 = batchCount2 ;
}
public String getItsmDashTranInfoBatch2() {
return this.itsmDashTranInfoBatch2;
}
public void setItsmDashEaiBatchErrorList(HashMap<String,HashMap<String,String>> eaiBatchErrorList) {
this.itsmDashEaiBatchErrorList = eaiBatchErrorList;
}
public void setItsmDashFepBatchErrorList(HashMap<String,HashMap<String,String>> fepBatchErrorList) {
this.itsmDashFepBatchErrorList = fepBatchErrorList;
}
@@ -394,8 +395,8 @@ public class Dashboard2Repository implements Serializable {
public MultiKeyMap getRepoMemTr() {
return repoMemTransaction;
}
/**
* 저장소 객체를 리턴한다.
* @return
@@ -403,7 +404,7 @@ public class Dashboard2Repository implements Serializable {
public MultiKeyMap getRepository() {
return repository;
}
/**
* 서비스 -> 호스트명 리스트를 저장한다.
* @param service
@@ -412,7 +413,7 @@ public class Dashboard2Repository implements Serializable {
public void setHostNames(String service, List<String> list ) {
repoHostNames.put(service,list);
}
/**
* 호스트명 -> 인스턴스 리스트를 저장한다.
* @param host
@@ -421,7 +422,11 @@ public class Dashboard2Repository implements Serializable {
public void setInstNames(String host, List<String> list ) {
repoInstNames.put(host,list);
}
public void setDataSourceInstNames(String host, Map<String, List<String>> putData ) {
repoDataSourceInstNames.put(host, putData);
}
/**
* Memory 거래현황정보 추가
* @param host
@@ -447,28 +452,28 @@ public class Dashboard2Repository implements Serializable {
vo.setHostName(host);
vo.setInstName(inst);
}
return vo;
}
/**
* 주어진 호스트명 에서 수신된 Vo 리스트를 찾아서 리턴한다.
* @param host
* @return
*/
public List<MemStaticsVo> getMemTradeVoByHost( String host ) {
List<MemStaticsVo> list = new ArrayList<MemStaticsVo>();
List<MemStaticsVo> list = new ArrayList<MemStaticsVo>();
List<String> insts = getInstNames(host);
if( insts != null ) {
for( String inst : insts ) {
list.add( getMemTradeVo(host,inst) );
}
}
return list;
}
/**
* 주어진 서비스에서 수신된 전체 Vo 리스트를 찾아서 리턴한다.
* @param service
@@ -482,86 +487,108 @@ public class Dashboard2Repository implements Serializable {
list.addAll( getMemTradeVoByHost(host) );
}
}
return list;
}
/**
* 주어진 서비스에서 수신된 전체 Vo 리스트를 찾아서 리턴한다.
* repoDataSourceInstNames 에서 host, instance 조회 MemTradeVo를 리턴한다.
* @param service
* @return
*/
public List<MemStaticsVo> getDashboardMemTradeVoByService(String service) {
return Optional.ofNullable(getHostNames(service))
.orElse(Collections.emptyList())
.stream()
.flatMap(host -> getInstListByHostAndService(host, service).stream()
.map(instNm -> getMemTradeVo(host, instNm)))
.collect(Collectors.toList());
}
private List<String> getInstListByHostAndService(String host, String service) {
return Optional.ofNullable(repoDataSourceInstNames)
.map(m -> m.get(host))
.map(serviceMap -> serviceMap.get(service))
.orElse(Collections.emptyList());
}
public void setTradeVo(String service, String host, DbStaticsVo vo) {
repository.put(service, host, vo);
}
public DbStaticsVo getTradeVo(String service, String host) {
DbStaticsVo vo = (DbStaticsVo)repository.get(service, host);
if( vo == null ) {
vo = new DbStaticsVo();
vo.init();
}
return vo;
}
public List<DbStaticsVo> getTradeVo(String service) {
List<DbStaticsVo> list = new ArrayList<DbStaticsVo>();
List<String> hostNames = repoHostNames.get(service);
if( hostNames != null ) {
for( String host : hostNames ) {
list.add( getTradeVo(service, host) );
}
}
if( list.size() == 0 ) {
DbStaticsVo vo = new DbStaticsVo();
vo.init();
list.add(vo);
}
return list;
}
public DbStaticsVo getTotalVo(String service) {
List<DbStaticsVo> list = getTradeVo(service);
DbStaticsVo retVo = new DbStaticsVo();
retVo.init();
for( DbStaticsVo vo : list ) {
retVo.add( vo );
}
return retVo ;
}
public void setMaxProcTime( String fulltime ) {
this.maxProcTime = fulltime ;
}
public String getMaxProcTime() {
return this.maxProcTime;
}
public boolean isInitHostRepository() {
if( repoHostAlive.size() > 0 )
return true ;
else
return false ;
}
public void addHostKey( List<String> listAddr ) {
if( listAddr != null ) {
for( String addr : listAddr ) {
repoHostAlive.put(addr, "1");
}
}
}
public boolean isHostAlive( String ipaddr, String port ) {
String alive = repoHostAlive.get(ipaddr + ":" + port);
// 디폴트는 성공으로 간주하고, 실패시에만 Fail 처리한다.
if( "0".equals(alive) ) {
return false ;
@@ -569,7 +596,7 @@ public class Dashboard2Repository implements Serializable {
return true ;
}
}
public boolean isHostAlive( String ipaddr ) {
boolean foundDead = false ;
@@ -589,74 +616,74 @@ public class Dashboard2Repository implements Serializable {
}
}
}
if( foundDead ) {
// 모든 inst 실패
return false ;
}
// 디폴트
return true ;
}
public void setHostAlive( String ipaddr, String port, boolean alive ) {
String aliveVal = "1" ;
if( alive == false ) {
aliveVal = "0" ;
}
repoHostAlive.put(ipaddr + ":" + port, aliveVal );
}
/**
* ip : port 형태의 주소값 리스트를 리턴한다.
* @return
*/
public List<String> getHostIPList() {
Iterator it = repoHostAlive.keySet().iterator();
List<String> list = new ArrayList<String>();
while( it.hasNext() ) {
String key = (String)it.next();
list.add( key );
}
return list ;
}
public List<String> getHostNames( String service ) {
return repoHostNames.get(service);
}
public List<String> getInstNames( String host ) {
return repoInstNames.get(host);
}
public void addSms( SmsVO vo ) {
repoSms.add( vo );
// 만일 100개가 넘으면 앞에서 부터 50개를 삭제한다.
if( repoSms.size() >= 100 ) {
}
}
public void setPeakTps( String service, String val ) {
repoPeakTps.put(service, val);
}
public String getPeakTps( String service ) {
return repoPeakTps.get(service);
}
public void setPeakTrade( String service, String val ) {
repoPeakTrade.put(service, val);
}
public String getPeakTrade( String service ) {
return repoPeakTrade.get(service);
}
public String getProcBatch( ) {
StringBuffer sb = new StringBuffer();
for( int i = 0 ; i < this.procBatch.length ; i++ ) {
@@ -664,7 +691,7 @@ public class Dashboard2Repository implements Serializable {
}
return sb.toString();
}
public String getProcEaiBatch( ) {
StringBuffer sb = new StringBuffer();
for( int i = 0 ; i < this.procEaiBatch.length ; i++ ) {
@@ -672,7 +699,7 @@ public class Dashboard2Repository implements Serializable {
}
return sb.toString();
}
public void setProcBatch( int p1, int p2, int p3, int p4, int p5, int p6 ) {
this.procBatch[0] = p1 ;
this.procBatch[1] = p2 ;
@@ -691,4 +718,37 @@ public class Dashboard2Repository implements Serializable {
this.procEaiBatch[5] = p6 ;
}
// 주어진 데이터소스명을 기준으로 hostname list를 리턴한다.
public List<Map<String,String>> getDatasourceByHost(String datasourcName){
List<String> hostList = repoHostNames.get(datasourcName);
List<Map<String,String>> serverInfoList = new ArrayList<Map<String,String>>();
if( hostList != null ){
for(String hostName : hostList) {
Map<String, List<String>> dataSourceInst = repoDataSourceInstNames.get(hostName);
if( dataSourceInst != null ){
List<String> instNameList = dataSourceInst.get(datasourcName);
if( instNameList != null ){
for (String instName : instNameList) {
Map<String, String> serverInfo = new HashMap<String, String>();
serverInfo.put("HOSTNAME", hostName);
serverInfo.put("EAISVRINSTNM", instName);
serverInfoList.add(serverInfo);
}
}
}
}
}
return serverInfoList;
}
// 주어진 host 명을 기준으로 map datasource, instance list 리턴한다.
public Map<String, List<String>> getDatasourceInstListByHostNm(String hostName){
return repoDataSourceInstNames.getOrDefault(hostName, new HashMap<String, List<String>>());
}
}
@@ -1,13 +1,12 @@
package com.eactive.eai.rms.onl.dashboard.service;
import java.util.HashMap;
import java.util.Vector;
import com.eactive.eai.rms.onl.dashboard.dao.DashboardErrorDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.eactive.eai.rms.onl.dashboard.dao.DashboardErrorDAO;
import java.util.HashMap;
import java.util.Vector;
@Service("dashboardErrorService")
public class DashboardErrorServiceImpl implements DashboardErrorService {
@@ -1,13 +1,8 @@
package com.eactive.eai.rms.onl.dashboard.service;
import java.util.List;
import com.eactive.eai.rms.onl.vo.*;
import com.eactive.eai.rms.onl.vo.AdaptersVO;
import com.eactive.eai.rms.onl.vo.EAIEngineStatusVo;
import com.eactive.eai.rms.onl.vo.HostStatusVo;
import com.eactive.eai.rms.onl.vo.RealTimeAdaptersVO;
import com.eactive.eai.rms.onl.vo.SmsVO;
import com.eactive.eai.rms.onl.vo.TransactionTrackingVo;
import java.util.List;
/**
* The Interface DashboardService.
@@ -1,22 +1,16 @@
package com.eactive.eai.rms.onl.dashboard.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.onl.server.repository.Repository;
import com.eactive.eai.rms.onl.server.repository.TransactionRepositoryService;
import com.eactive.eai.rms.onl.vo.AdaptersVO;
import com.eactive.eai.rms.onl.vo.EAIEngineStatusVo;
import com.eactive.eai.rms.onl.vo.HostStatusVo;
import com.eactive.eai.rms.onl.vo.RealTimeAdaptersVO;
import com.eactive.eai.rms.onl.vo.SmsVO;
import com.eactive.eai.rms.onl.vo.TransactionTrackingVo;
import com.eactive.eai.rms.onl.vo.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* The Class DashboardServiceImpl.
@@ -1,17 +1,5 @@
package com.eactive.eai.rms.onl.dashboard.service;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Optional;
import java.util.Vector;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.eactive.eai.common.util.SystemUtil;
import com.eactive.eai.data.entity.onl.property.Prop;
import com.eactive.eai.data.entity.onl.property.PropGroup;
@@ -23,6 +11,12 @@ import com.eactive.eai.rms.data.entity.onl.property.PropertyGroupService;
import com.eactive.eai.rms.onl.dashboard.dao.DashboardErrorDAO;
import com.eactive.eai.rms.onl.manage.comm.unifbwk.UnifbwkManService;
import com.eactive.eai.rms.onl.manage.comm.unifbwk.UnifbwkUI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* The Interface DashboardService.
@@ -64,7 +64,7 @@ public class TransactionSendHandler implements IMessageHandler {
// 호스트명, 인스턴스명을 초기화 한다.
//DataSourceContextHolder.setDataSourceType(DataSourceTypeManager.getFirstDataSourceType());
dashboard2Repository.initHostInstNames(false);
dashboard2Repository.initHostInstNames(false, null);
dashboard2Repository.initEaiSevrDstcd(false);
// 참고 : 지정된 호스트 이외의 데이터는 버린다.