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

This commit is contained in:
Rinjae
2025-11-17 14:52:32 +09:00
72 changed files with 49662 additions and 547 deletions
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>