API 통계 도메인 구조 개편 및 조회 기능 확장:
- API 통계 엔티티/리포지토리 다층 나눔 (분/시/일/월) - API 상세/요약 통계 및 기간별 누적 통계 DTO 추가 - 고객센터 연락처 조회 로직 추가 (@ModelAttribute) - 글로벌 푸터 적용 - @EMSDataSource 지원 엔티티-패키지 다중 검색 적용 - 기존 ApiStatisticsRepository 삭제, 분리된 구성으로 대체
This commit is contained in:
@@ -35,7 +35,7 @@ gateway:
|
||||
|
||||
hibernate-dialect: org.hibernate.dialect.Oracle12cDialect
|
||||
hibernate-physical-naming-strategy: com.eactive.apim.portal.common.entity.CustomPhysicalNamingStrategy
|
||||
entity-package: com.eactive.eai.data.entity.onl
|
||||
entity-package: com.eactive.eai.data.entity.onl,com.eactive.apim.gateway.data.statistics.entity
|
||||
|
||||
|
||||
portal:
|
||||
|
||||
@@ -46,5 +46,5 @@ gateway:
|
||||
|
||||
hibernate-dialect: org.hibernate.dialect.Oracle12cDialect
|
||||
hibernate-physical-naming-strategy: com.eactive.apim.portal.common.entity.CustomPhysicalNamingStrategy
|
||||
entity-package: com.eactive.eai.data.entity.onl
|
||||
entity-package: com.eactive.eai.data.entity.onl,com.eactive.apim.gateway.data.statistics.entity
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ gateway:
|
||||
|
||||
hibernate-dialect: org.hibernate.dialect.Oracle12cDialect
|
||||
hibernate-physical-naming-strategy: com.eactive.apim.portal.common.entity.CustomPhysicalNamingStrategy
|
||||
entity-package: com.eactive.eai.data.entity.onl
|
||||
entity-package: com.eactive.eai.data.entity.onl,com.eactive.apim.gateway.data.statistics.entity
|
||||
|
||||
portal:
|
||||
file:
|
||||
|
||||
@@ -1,18 +1,74 @@
|
||||
/* API Statistics Page Styles */
|
||||
|
||||
.api-statistics-container {
|
||||
/*max-width: 1200px;*/
|
||||
margin: 0 auto;
|
||||
padding: 48px 0px;
|
||||
}
|
||||
|
||||
/* Search Section */
|
||||
.statistics-notice {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 18px;
|
||||
background: #eef4fc;
|
||||
border: 1px solid #d6e4f5;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.statistics-notice-latest {
|
||||
color: #0049B4;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.statistics-search-section {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.search-mode-tabs {
|
||||
display: inline-flex;
|
||||
background: #eef1f5;
|
||||
border-radius: 22px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.mode-tab {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 7px 22px;
|
||||
border-radius: 18px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.mode-tab.active {
|
||||
background: #0049B4;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.month-range-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.month-input {
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -54,16 +110,13 @@
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
background: #003d99;
|
||||
}
|
||||
|
||||
.btn-search svg {
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
/* Summary Card */
|
||||
.statistics-summary-card {
|
||||
background: #f5f8fc;
|
||||
border-radius: 16px;
|
||||
@@ -103,11 +156,9 @@
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.app-select:hover {
|
||||
background-color: #f0f5ff;
|
||||
}
|
||||
|
||||
.app-select:focus {
|
||||
border-color: #003d99;
|
||||
box-shadow: 0 0 0 3px rgba(0, 73, 180, 0.1);
|
||||
@@ -122,7 +173,6 @@
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-total-text strong {
|
||||
font-size: 24px;
|
||||
color: #0049B4;
|
||||
@@ -140,11 +190,12 @@
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.summary-detail-item.success .detail-icon {
|
||||
color: #4A90D9;
|
||||
}
|
||||
|
||||
.summary-detail-item.timeout .detail-icon {
|
||||
color: #F5C36B;
|
||||
}
|
||||
.summary-detail-item.failure .detail-icon {
|
||||
color: #F5A3B5;
|
||||
}
|
||||
@@ -163,7 +214,6 @@
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Donut Chart */
|
||||
.summary-center {
|
||||
flex: 0 0 200px;
|
||||
display: flex;
|
||||
@@ -180,7 +230,6 @@
|
||||
transition: stroke-dasharray 0.3s ease, stroke-dashoffset 0.3s ease;
|
||||
}
|
||||
|
||||
/* Rate Display */
|
||||
.summary-right {
|
||||
flex: 0 0 230px;
|
||||
display: flex;
|
||||
@@ -195,7 +244,6 @@
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.rate-display:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -205,11 +253,12 @@
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.rate-indicator.success {
|
||||
background: #4A90D9;
|
||||
}
|
||||
|
||||
.rate-indicator.timeout {
|
||||
background: #F5C36B;
|
||||
}
|
||||
.rate-indicator.failure {
|
||||
background: #F5A3B5;
|
||||
}
|
||||
@@ -228,6 +277,10 @@
|
||||
color: #4A90D9;
|
||||
}
|
||||
|
||||
.timeout-rate .rate-value {
|
||||
color: #F5C36B;
|
||||
}
|
||||
|
||||
.failure-rate .rate-value {
|
||||
color: #F5A3B5;
|
||||
}
|
||||
@@ -237,12 +290,15 @@
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Statistics Table Card */
|
||||
.statistics-table-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.statistics-table-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@@ -262,7 +318,6 @@
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-title svg {
|
||||
stroke: #fff;
|
||||
}
|
||||
@@ -279,7 +334,6 @@
|
||||
font-size: 14px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-download:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
color: #fff;
|
||||
@@ -293,7 +347,6 @@
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.statistics-table thead th {
|
||||
padding: 16px 20px;
|
||||
text-align: left;
|
||||
@@ -302,16 +355,23 @@
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.statistics-table tbody td {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.statistics-table tbody tr:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.statistics-table tfoot .total-row {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
.statistics-table tfoot .total-row td {
|
||||
padding: 16px 20px;
|
||||
font-weight: 600;
|
||||
color: #0049B4;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.api-name {
|
||||
display: flex;
|
||||
@@ -328,20 +388,16 @@
|
||||
background: #f0f0f0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.api-icon svg {
|
||||
stroke: #666;
|
||||
}
|
||||
|
||||
.api-icon.total {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
|
||||
.api-icon.total svg {
|
||||
stroke: #0049B4;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-cell {
|
||||
min-width: 200px;
|
||||
}
|
||||
@@ -359,83 +415,64 @@
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-timeout {
|
||||
background: #F5C36B;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-failure {
|
||||
background: #F5A3B5;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Total Row */
|
||||
.statistics-table tfoot .total-row {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
|
||||
.statistics-table tfoot .total-row td {
|
||||
padding: 16px 20px;
|
||||
font-weight: 600;
|
||||
color: #0049B4;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-row td {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 992px) {
|
||||
.summary-content {
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.summary-left,
|
||||
.summary-center,
|
||||
.summary-right {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.summary-center {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.summary-right {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.api-statistics-container {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.statistics-summary-card {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.app-toggle-buttons {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statistics-table thead th,
|
||||
.statistics-table tbody td,
|
||||
.statistics-table tfoot td {
|
||||
padding: 12px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.progress-cell {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.search-filter-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -19148,12 +19148,72 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
padding: 48px 0px;
|
||||
}
|
||||
|
||||
.statistics-notice {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 18px;
|
||||
background: #eef4fc;
|
||||
border: 1px solid #d6e4f5;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.statistics-notice-latest {
|
||||
color: #0049B4;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.statistics-search-section {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.search-mode-tabs {
|
||||
display: inline-flex;
|
||||
background: #eef1f5;
|
||||
border-radius: 22px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.mode-tab {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 7px 22px;
|
||||
border-radius: 18px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.mode-tab.active {
|
||||
background: #0049B4;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.month-range-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.month-input {
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -19278,6 +19338,9 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
.summary-detail-item.success .detail-icon {
|
||||
color: #4A90D9;
|
||||
}
|
||||
.summary-detail-item.timeout .detail-icon {
|
||||
color: #F5C36B;
|
||||
}
|
||||
.summary-detail-item.failure .detail-icon {
|
||||
color: #F5A3B5;
|
||||
}
|
||||
@@ -19338,6 +19401,9 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
.rate-indicator.success {
|
||||
background: #4A90D9;
|
||||
}
|
||||
.rate-indicator.timeout {
|
||||
background: #F5C36B;
|
||||
}
|
||||
.rate-indicator.failure {
|
||||
background: #F5A3B5;
|
||||
}
|
||||
@@ -19356,6 +19422,10 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
color: #4A90D9;
|
||||
}
|
||||
|
||||
.timeout-rate .rate-value {
|
||||
color: #F5C36B;
|
||||
}
|
||||
|
||||
.failure-rate .rate-value {
|
||||
color: #F5A3B5;
|
||||
}
|
||||
@@ -19370,6 +19440,10 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.statistics-table-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@@ -19486,6 +19560,11 @@ input[type=checkbox]:checked + .custom-checkbox {
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-timeout {
|
||||
background: #F5C36B;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-failure {
|
||||
background: #F5A3B5;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -11,13 +11,77 @@
|
||||
padding: 48px 0px;
|
||||
}
|
||||
|
||||
// 집계 안내 문구
|
||||
.statistics-notice {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 18px;
|
||||
background: #eef4fc;
|
||||
border: 1px solid #d6e4f5;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.statistics-notice-latest {
|
||||
color: #0049B4;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// Search Section
|
||||
.statistics-search-section {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
// 일별/월별 모드 토글
|
||||
.search-mode-tabs {
|
||||
display: inline-flex;
|
||||
background: #eef1f5;
|
||||
border-radius: 22px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.mode-tab {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 7px 22px;
|
||||
border-radius: 18px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active {
|
||||
background: #0049B4;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// 월 선택 (월별 모드)
|
||||
.month-range-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.month-input {
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -149,6 +213,10 @@
|
||||
color: #4A90D9;
|
||||
}
|
||||
|
||||
&.timeout .detail-icon {
|
||||
color: #F5C36B;
|
||||
}
|
||||
|
||||
&.failure .detail-icon {
|
||||
color: #F5A3B5;
|
||||
}
|
||||
@@ -214,6 +282,10 @@
|
||||
background: #4A90D9;
|
||||
}
|
||||
|
||||
&.timeout {
|
||||
background: #F5C36B;
|
||||
}
|
||||
|
||||
&.failure {
|
||||
background: #F5A3B5;
|
||||
}
|
||||
@@ -233,6 +305,10 @@
|
||||
color: #4A90D9;
|
||||
}
|
||||
|
||||
.timeout-rate .rate-value {
|
||||
color: #F5C36B;
|
||||
}
|
||||
|
||||
.failure-rate .rate-value {
|
||||
color: #F5A3B5;
|
||||
}
|
||||
@@ -248,6 +324,11 @@
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@@ -377,6 +458,11 @@
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-timeout {
|
||||
background: #F5C36B;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-failure {
|
||||
background: #F5A3B5;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
@@ -14,16 +14,41 @@
|
||||
<link rel="stylesheet" type="text/css" th:href="@{/css/api-statistics.css}"/>
|
||||
<section class="api-statistics-container">
|
||||
|
||||
<!-- 집계 안내 문구 (집계 주기 분 + 최신 집계 시각, 서버 계산값) -->
|
||||
<div class="statistics-notice">
|
||||
<span th:if="${statsAggregationMinute != null}"
|
||||
th:text="'통계 데이터는 매시 ' + ${statsAggregationMinute} + '분 집계되며, 집계 이전 시간 기준으로 생성됩니다.'">통계 안내</span>
|
||||
<span th:if="${statsAggregationMinute == null}">통계 데이터는 매시 집계되며, 집계 이전 시간 기준으로 생성됩니다.</span>
|
||||
<span th:if="${statsLatestTime != null}" class="statistics-notice-latest"
|
||||
th:text="'※ 최신 집계 시각: ' + ${statsLatestTime} + ' 기준'">최신 집계 시각</span>
|
||||
</div>
|
||||
|
||||
<!-- Search Filter -->
|
||||
<div class="statistics-search-section">
|
||||
<div class="search-mode-tabs">
|
||||
<button type="button" class="mode-tab active" data-mode="DAILY">일별</button>
|
||||
<button type="button" class="mode-tab" data-mode="MONTHLY">월별</button>
|
||||
</div>
|
||||
|
||||
<div class="search-filter-row">
|
||||
<div class="date-range-picker">
|
||||
<!-- 일별: 날짜 범위 -->
|
||||
<div class="date-range-picker" id="dailyFilter">
|
||||
<input type="date" id="startDate" class="date-input"
|
||||
th:value="${#temporals.format(searchDto.startDate, 'yyyy-MM-dd')}">
|
||||
<span class="date-separator">-</span>
|
||||
<input type="date" id="endDate" class="date-input"
|
||||
th:value="${#temporals.format(searchDto.endDate, 'yyyy-MM-dd')}">
|
||||
</div>
|
||||
|
||||
<!-- 월별: 특정 년월 단일 선택 (가용월 목록) -->
|
||||
<div class="month-range-picker" id="monthlyFilter" style="display:none;">
|
||||
<select id="month" class="month-input">
|
||||
<option th:each="m,st : ${availableMonths}" th:value="${m}"
|
||||
th:text="${#strings.substring(m,0,4) + '-' + #strings.substring(m,4,6)}"
|
||||
th:selected="${st.index == 0}">2026-07</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-search" id="btnSearch">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
@@ -42,9 +67,9 @@
|
||||
<select id="appSelect" class="app-select">
|
||||
<option value="">전체 앱</option>
|
||||
<option th:each="app : ${appList}"
|
||||
th:value="${app.clientid}"
|
||||
th:text="${app.clientname}"
|
||||
th:selected="${searchDto.clientId != null and searchDto.clientId == app.clientid}">
|
||||
th:value="${app.clientId}"
|
||||
th:text="${app.clientName}"
|
||||
th:selected="${searchDto.clientId != null and searchDto.clientId == app.clientId}">
|
||||
앱 이름
|
||||
</option>
|
||||
</select>
|
||||
@@ -52,46 +77,55 @@
|
||||
|
||||
<div class="summary-info">
|
||||
<p class="summary-total-text">
|
||||
총 API 호출 수는 <strong id="totalAttempted" th:text="${#numbers.formatInteger(summary.totalAttempted, 0, 'COMMA')}">0</strong>건입니다.
|
||||
총 API 호출 수는 <strong id="totalCount" th:text="${#numbers.formatInteger(summary.totalCount, 0, 'COMMA')}">0</strong>건입니다.
|
||||
</p>
|
||||
<div class="summary-detail-list">
|
||||
<div class="summary-detail-item success">
|
||||
<span class="detail-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<circle cx="8" cy="8" r="6"/>
|
||||
</svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="6"/></svg>
|
||||
</span>
|
||||
<span class="detail-label">성공</span>
|
||||
<span class="detail-value" id="totalCompleted" th:text="${#numbers.formatInteger(summary.totalCompleted, 0, 'COMMA')}">0</span>
|
||||
<span class="detail-percent">(<span id="successRateText" th:text="${summary.successRate}">0</span>%)</span>
|
||||
<span class="detail-value" id="successCount" th:text="${#numbers.formatInteger(summary.successCount, 0, 'COMMA')}">0</span>
|
||||
<span class="detail-percent">(<span id="successRateText" th:text="${summary.totalCount == 0 ? '-' : summary.successRate}">0</span>%)</span>
|
||||
</div>
|
||||
<div class="summary-detail-item timeout">
|
||||
<span class="detail-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="6"/></svg>
|
||||
</span>
|
||||
<span class="detail-label">타임아웃</span>
|
||||
<span class="detail-value" id="timeoutCount" th:text="${#numbers.formatInteger(summary.timeoutCount, 0, 'COMMA')}">0</span>
|
||||
<span class="detail-percent">(<span id="timeoutRateText" th:text="${summary.totalCount == 0 ? '-' : summary.timeoutRate}">0</span>%)</span>
|
||||
</div>
|
||||
<div class="summary-detail-item failure">
|
||||
<span class="detail-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<circle cx="8" cy="8" r="6"/>
|
||||
</svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="6"/></svg>
|
||||
</span>
|
||||
<span class="detail-label">실패</span>
|
||||
<span class="detail-value" id="totalFailed" th:text="${#numbers.formatInteger(summary.totalFailed, 0, 'COMMA')}">0</span>
|
||||
<span class="detail-percent">(<span id="failureRateText" th:text="${summary.failureRate}">0</span>%)</span>
|
||||
<span class="detail-value" id="errorCount" th:text="${#numbers.formatInteger(summary.errorCount, 0, 'COMMA')}">0</span>
|
||||
<span class="detail-percent">(<span id="errorRateText" th:text="${summary.totalCount == 0 ? '-' : summary.errorRate}">0</span>%)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center: Donut Chart (SVG) -->
|
||||
<!-- Center: Donut Chart (SVG, 3 segments) -->
|
||||
<div class="summary-center">
|
||||
<svg id="donutChart" width="200" height="200" viewBox="0 0 200 200">
|
||||
<circle class="donut-bg" cx="100" cy="100" r="70" fill="none" stroke="#e0e0e0" stroke-width="24"/>
|
||||
<circle id="donutSuccess" class="donut-segment" cx="100" cy="100" r="70" fill="none"
|
||||
stroke="#4A90D9" stroke-width="24" stroke-linecap="round"
|
||||
stroke="#4A90D9" stroke-width="24" stroke-linecap="butt"
|
||||
th:attr="stroke-dasharray=${summary.successRate * 4.398} + ' 439.8'"
|
||||
transform="rotate(-90 100 100)"/>
|
||||
<circle id="donutFailure" class="donut-segment" cx="100" cy="100" r="70" fill="none"
|
||||
stroke="#F5A3B5" stroke-width="24" stroke-linecap="round"
|
||||
th:attr="stroke-dasharray=${summary.failureRate * 4.398} + ' 439.8',
|
||||
stroke-dashoffset="0" transform="rotate(-90 100 100)"/>
|
||||
<circle id="donutTimeout" class="donut-segment" cx="100" cy="100" r="70" fill="none"
|
||||
stroke="#F5C36B" stroke-width="24" stroke-linecap="butt"
|
||||
th:attr="stroke-dasharray=${summary.timeoutRate * 4.398} + ' 439.8',
|
||||
stroke-dashoffset='-' + ${summary.successRate * 4.398}"
|
||||
transform="rotate(-90 100 100)"/>
|
||||
<circle id="donutError" class="donut-segment" cx="100" cy="100" r="70" fill="none"
|
||||
stroke="#F5A3B5" stroke-width="24" stroke-linecap="butt"
|
||||
th:attr="stroke-dasharray=${summary.errorRate * 4.398} + ' 439.8',
|
||||
stroke-dashoffset='-' + ${(summary.successRate + summary.timeoutRate) * 4.398}"
|
||||
transform="rotate(-90 100 100)"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -100,13 +134,19 @@
|
||||
<div class="rate-display success-rate">
|
||||
<span class="rate-indicator success"></span>
|
||||
<span class="rate-label">성공률</span>
|
||||
<span class="rate-value" id="successRate" th:text="${summary.successRate}">0</span>
|
||||
<span class="rate-value" id="successRate" th:text="${summary.totalCount == 0 ? '-' : summary.successRate}">0</span>
|
||||
<span class="rate-percent">%</span>
|
||||
</div>
|
||||
<div class="rate-display timeout-rate">
|
||||
<span class="rate-indicator timeout"></span>
|
||||
<span class="rate-label">타임아웃율</span>
|
||||
<span class="rate-value" id="timeoutRate" th:text="${summary.totalCount == 0 ? '-' : summary.timeoutRate}">0</span>
|
||||
<span class="rate-percent">%</span>
|
||||
</div>
|
||||
<div class="rate-display failure-rate">
|
||||
<span class="rate-indicator failure"></span>
|
||||
<span class="rate-label">실패률</span>
|
||||
<span class="rate-value" id="failureRate" th:text="${summary.failureRate}">0</span>
|
||||
<span class="rate-value" id="errorRate" th:text="${summary.totalCount == 0 ? '-' : summary.errorRate}">0</span>
|
||||
<span class="rate-percent">%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,20 +158,11 @@
|
||||
<div class="table-header">
|
||||
<h3 class="table-title">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M18 9l-5 5-4-4-4 4"/>
|
||||
<path d="M3 3v18h18"/><path d="M18 9l-5 5-4-4-4 4"/>
|
||||
</svg>
|
||||
API별 통계
|
||||
</h3>
|
||||
<a th:href="@{/statistics/api/download(startDate=${searchDto.startDate},endDate=${searchDto.endDate},clientId=${searchDto.clientId})}"
|
||||
class="btn-download" id="btnDownload">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
다운로드
|
||||
</a>
|
||||
<!-- 다운로드 버튼은 UI에서 제거됨 (백엔드 /statistics/api/download 는 유지) -->
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
@@ -141,6 +172,7 @@
|
||||
<th>API명</th>
|
||||
<th>API호출수</th>
|
||||
<th>성공</th>
|
||||
<th>타임아웃</th>
|
||||
<th>실패</th>
|
||||
<th>통계</th>
|
||||
</tr>
|
||||
@@ -155,19 +187,20 @@
|
||||
</span>
|
||||
<span th:text="${detail.apiName}">API 이름</span>
|
||||
</td>
|
||||
<td th:text="${#numbers.formatInteger(detail.attemptedCount, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(detail.completedCount, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(detail.failedCount, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(detail.totalCount, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(detail.successCount, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(detail.timeoutCount, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(detail.errorCount, 0, 'COMMA')}">0</td>
|
||||
<td class="progress-cell">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-success" th:style="'width:' + ${detail.successRate} + '%'"></div>
|
||||
<div class="progress-failure" th:style="'width:' + ${detail.failureRate} + '%'"></div>
|
||||
<div class="progress-timeout" th:style="'width:' + ${detail.timeoutRate} + '%'"></div>
|
||||
<div class="progress-failure" th:style="'width:' + ${detail.errorRate} + '%'"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Empty State -->
|
||||
<tr th:if="${details == null or details.isEmpty()}" class="empty-row">
|
||||
<td colspan="5">데이터가 없습니다.</td>
|
||||
<td colspan="6">데이터가 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot id="statisticsTableFoot" th:if="${details != null and !details.isEmpty()}">
|
||||
@@ -175,19 +208,20 @@
|
||||
<td class="api-name">
|
||||
<span class="api-icon total">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M18 9l-5 5-4-4-4 4"/>
|
||||
<path d="M3 3v18h18"/><path d="M18 9l-5 5-4-4-4 4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>전체</span>
|
||||
</td>
|
||||
<td th:text="${#numbers.formatInteger(summary.totalAttempted, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(summary.totalCompleted, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(summary.totalFailed, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(summary.totalCount, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(summary.successCount, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(summary.timeoutCount, 0, 'COMMA')}">0</td>
|
||||
<td th:text="${#numbers.formatInteger(summary.errorCount, 0, 'COMMA')}">0</td>
|
||||
<td class="progress-cell">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-success" th:style="'width:' + ${summary.successRate} + '%'"></div>
|
||||
<div class="progress-failure" th:style="'width:' + ${summary.failureRate} + '%'"></div>
|
||||
<div class="progress-timeout" th:style="'width:' + ${summary.timeoutRate} + '%'"></div>
|
||||
<div class="progress-failure" th:style="'width:' + ${summary.errorRate} + '%'"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -196,59 +230,102 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 기간별(일별/월별) 누적 통계 -->
|
||||
<div class="statistics-table-card">
|
||||
<div class="table-header">
|
||||
<h3 class="table-title">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/>
|
||||
</svg>
|
||||
<span id="periodTableTitle">일별 통계</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="statistics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="periodLabel">일자</th>
|
||||
<th>API호출수</th>
|
||||
<th>성공</th>
|
||||
<th>타임아웃</th>
|
||||
<th>실패</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="periodTableBody">
|
||||
<tr th:each="p : ${periods}">
|
||||
<td th:text="${p.period}">2026-07-15 (수)</td>
|
||||
<td th:text="${p.hasData ? #numbers.formatInteger(p.totalCount, 0, 'COMMA') : '-'}">0</td>
|
||||
<td th:text="${p.hasData ? #numbers.formatInteger(p.successCount, 0, 'COMMA') : '-'}">0</td>
|
||||
<td th:text="${p.hasData ? #numbers.formatInteger(p.timeoutCount, 0, 'COMMA') : '-'}">0</td>
|
||||
<td th:text="${p.hasData ? #numbers.formatInteger(p.errorCount, 0, 'COMMA') : '-'}">0</td>
|
||||
</tr>
|
||||
<tr th:if="${periods == null or periods.isEmpty()}" class="empty-row">
|
||||
<td colspan="5">데이터가 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="contentScript">
|
||||
<script th:inline="javascript">
|
||||
$(document).ready(function() {
|
||||
// Initial data from server
|
||||
let currentData = {
|
||||
summary: {
|
||||
totalAttempted: [[${summary.totalAttempted}]],
|
||||
totalCompleted: [[${summary.totalCompleted}]],
|
||||
totalFailed: [[${summary.totalFailed}]],
|
||||
totalCount: [[${summary.totalCount}]],
|
||||
successCount: [[${summary.successCount}]],
|
||||
timeoutCount: [[${summary.timeoutCount}]],
|
||||
errorCount: [[${summary.errorCount}]],
|
||||
successRate: [[${summary.successRate}]],
|
||||
failureRate: [[${summary.failureRate}]]
|
||||
timeoutRate: [[${summary.timeoutRate}]],
|
||||
errorRate: [[${summary.errorRate}]]
|
||||
},
|
||||
details: [[${details}]]
|
||||
details: [[${details}]],
|
||||
periods: [[${periods}]]
|
||||
};
|
||||
|
||||
let selectedClientId = [[${searchDto.clientId}]] || '';
|
||||
let currentMode = 'DAILY';
|
||||
const CIRCUMFERENCE = 439.8; // 2 * PI * 70
|
||||
const MAX_DATE_RANGE_DAYS = 40;
|
||||
|
||||
// Update SVG Donut Chart
|
||||
// ── Donut (성공/타임아웃/실패 3세그먼트) ──
|
||||
function updateChart() {
|
||||
var successRate = currentData.summary.successRate || 0;
|
||||
var failureRate = currentData.summary.failureRate || 0;
|
||||
|
||||
var successLength = successRate * CIRCUMFERENCE / 100;
|
||||
var failureLength = failureRate * CIRCUMFERENCE / 100;
|
||||
|
||||
$('#donutSuccess').attr('stroke-dasharray', successLength + ' ' + CIRCUMFERENCE);
|
||||
$('#donutFailure').attr('stroke-dasharray', failureLength + ' ' + CIRCUMFERENCE);
|
||||
$('#donutFailure').attr('stroke-dashoffset', -successLength);
|
||||
var s = currentData.summary.successRate || 0;
|
||||
var t = currentData.summary.timeoutRate || 0;
|
||||
var e = currentData.summary.errorRate || 0;
|
||||
var sl = s * CIRCUMFERENCE / 100;
|
||||
var tl = t * CIRCUMFERENCE / 100;
|
||||
var el = e * CIRCUMFERENCE / 100;
|
||||
$('#donutSuccess').attr('stroke-dasharray', sl + ' ' + CIRCUMFERENCE).attr('stroke-dashoffset', 0);
|
||||
$('#donutTimeout').attr('stroke-dasharray', tl + ' ' + CIRCUMFERENCE).attr('stroke-dashoffset', -sl);
|
||||
$('#donutError').attr('stroke-dasharray', el + ' ' + CIRCUMFERENCE).attr('stroke-dashoffset', -(sl + tl));
|
||||
}
|
||||
|
||||
// Update Summary Display
|
||||
// ── Summary ──
|
||||
function updateSummaryDisplay() {
|
||||
$('#totalAttempted').text(numberWithCommas(currentData.summary.totalAttempted));
|
||||
$('#totalCompleted').text(numberWithCommas(currentData.summary.totalCompleted));
|
||||
$('#totalFailed').text(numberWithCommas(currentData.summary.totalFailed));
|
||||
$('#successRate, #successRateText').text(currentData.summary.successRate);
|
||||
$('#failureRate, #failureRateText').text(currentData.summary.failureRate);
|
||||
var sm = currentData.summary;
|
||||
var t = sm.totalCount;
|
||||
$('#totalCount').text(numberWithCommas(sm.totalCount));
|
||||
$('#successCount').text(numberWithCommas(sm.successCount));
|
||||
$('#timeoutCount').text(numberWithCommas(sm.timeoutCount));
|
||||
$('#errorCount').text(numberWithCommas(sm.errorCount));
|
||||
$('#successRate, #successRateText').text(dashRate(t, sm.successRate));
|
||||
$('#timeoutRate, #timeoutRateText').text(dashRate(t, sm.timeoutRate));
|
||||
$('#errorRate, #errorRateText').text(dashRate(t, sm.errorRate));
|
||||
}
|
||||
|
||||
// Update Table
|
||||
// ── API별 표 ──
|
||||
function updateTable() {
|
||||
const tbody = $('#statisticsTableBody');
|
||||
const tfoot = $('#statisticsTableFoot');
|
||||
tbody.empty();
|
||||
|
||||
if (currentData.details && currentData.details.length > 0) {
|
||||
currentData.details.forEach(function(detail) {
|
||||
const row = `
|
||||
currentData.details.forEach(function(d) {
|
||||
tbody.append(`
|
||||
<tr>
|
||||
<td class="api-name">
|
||||
<span class="api-icon">
|
||||
@@ -256,166 +333,163 @@
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>${detail.apiName || '-'}</span>
|
||||
<span>${d.apiName || '-'}</span>
|
||||
</td>
|
||||
<td>${numberWithCommas(detail.attemptedCount)}</td>
|
||||
<td>${numberWithCommas(detail.completedCount)}</td>
|
||||
<td>${numberWithCommas(detail.failedCount)}</td>
|
||||
<td>${numberWithCommas(d.totalCount)}</td>
|
||||
<td>${numberWithCommas(d.successCount)}</td>
|
||||
<td>${numberWithCommas(d.timeoutCount)}</td>
|
||||
<td>${numberWithCommas(d.errorCount)}</td>
|
||||
<td class="progress-cell">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-success" style="width:${detail.successRate}%"></div>
|
||||
<div class="progress-failure" style="width:${detail.failureRate}%"></div>
|
||||
<div class="progress-success" style="width:${d.successRate}%"></div>
|
||||
<div class="progress-timeout" style="width:${d.timeoutRate}%"></div>
|
||||
<div class="progress-failure" style="width:${d.errorRate}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
tbody.append(row);
|
||||
</tr>`);
|
||||
});
|
||||
renderTotalFooter();
|
||||
} else {
|
||||
tbody.html('<tr class="empty-row"><td colspan="6">데이터가 없습니다.</td></tr>');
|
||||
$('#statisticsTableFoot').remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Update footer
|
||||
if (tfoot.length === 0) {
|
||||
const footerHtml = `
|
||||
<tfoot id="statisticsTableFoot">
|
||||
<tr class="total-row">
|
||||
<td class="api-name">
|
||||
<span class="api-icon total">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M18 9l-5 5-4-4-4 4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>전체</span>
|
||||
</td>
|
||||
<td>${numberWithCommas(currentData.summary.totalAttempted)}</td>
|
||||
<td>${numberWithCommas(currentData.summary.totalCompleted)}</td>
|
||||
<td>${numberWithCommas(currentData.summary.totalFailed)}</td>
|
||||
<td class="progress-cell">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-success" style="width:${currentData.summary.successRate}%"></div>
|
||||
<div class="progress-failure" style="width:${currentData.summary.failureRate}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
`;
|
||||
tbody.after(footerHtml);
|
||||
} else {
|
||||
tfoot.find('td:eq(1)').text(numberWithCommas(currentData.summary.totalAttempted));
|
||||
tfoot.find('td:eq(2)').text(numberWithCommas(currentData.summary.totalCompleted));
|
||||
tfoot.find('td:eq(3)').text(numberWithCommas(currentData.summary.totalFailed));
|
||||
tfoot.find('.progress-success').css('width', currentData.summary.successRate + '%');
|
||||
tfoot.find('.progress-failure').css('width', currentData.summary.failureRate + '%');
|
||||
}
|
||||
function renderTotalFooter() {
|
||||
var sm = currentData.summary;
|
||||
var footerHtml = `
|
||||
<tfoot id="statisticsTableFoot">
|
||||
<tr class="total-row">
|
||||
<td class="api-name">
|
||||
<span class="api-icon total">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/><path d="M18 9l-5 5-4-4-4 4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>전체</span>
|
||||
</td>
|
||||
<td>${numberWithCommas(sm.totalCount)}</td>
|
||||
<td>${numberWithCommas(sm.successCount)}</td>
|
||||
<td>${numberWithCommas(sm.timeoutCount)}</td>
|
||||
<td>${numberWithCommas(sm.errorCount)}</td>
|
||||
<td class="progress-cell">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-success" style="width:${sm.successRate}%"></div>
|
||||
<div class="progress-timeout" style="width:${sm.timeoutRate}%"></div>
|
||||
<div class="progress-failure" style="width:${sm.errorRate}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>`;
|
||||
$('#statisticsTableFoot').remove();
|
||||
$('#statisticsTableBody').after(footerHtml);
|
||||
}
|
||||
|
||||
// ── 기간별(일/월) 표 ──
|
||||
function updatePeriodTable() {
|
||||
const tbody = $('#periodTableBody');
|
||||
tbody.empty();
|
||||
if (currentData.periods && currentData.periods.length > 0) {
|
||||
currentData.periods.forEach(function(p) {
|
||||
tbody.append(`
|
||||
<tr>
|
||||
<td>${p.period || '-'}</td>
|
||||
<td>${p.hasData ? numberWithCommas(p.totalCount) : '-'}</td>
|
||||
<td>${p.hasData ? numberWithCommas(p.successCount) : '-'}</td>
|
||||
<td>${p.hasData ? numberWithCommas(p.timeoutCount) : '-'}</td>
|
||||
<td>${p.hasData ? numberWithCommas(p.errorCount) : '-'}</td>
|
||||
</tr>`);
|
||||
});
|
||||
} else {
|
||||
tbody.html('<tr class="empty-row"><td colspan="5">데이터가 없습니다.</td></tr>');
|
||||
tfoot.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Update Download Link
|
||||
function updateDownloadLink() {
|
||||
const startDate = $('#startDate').val();
|
||||
const endDate = $('#endDate').val();
|
||||
let downloadUrl = `/statistics/api/download?startDate=${startDate}&endDate=${endDate}`;
|
||||
if (selectedClientId) {
|
||||
downloadUrl += `&clientId=${selectedClientId}`;
|
||||
}
|
||||
$('#btnDownload').attr('href', downloadUrl);
|
||||
}
|
||||
|
||||
// Number formatting
|
||||
function numberWithCommas(x) {
|
||||
if (x === null || x === undefined) return '0';
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
// 데이터 없음(해당 total 0)일 때: 건수는 0(그대로) 표기, 비율(%)만 '-' 로 표기
|
||||
function dashRate(total, val) {
|
||||
return (!total || total == 0) ? '-' : val;
|
||||
}
|
||||
|
||||
// 세션 기반 CSRF: 쿠키 대신 <meta name="_csrf">에서 토큰을 읽는다.
|
||||
function getCsrfToken() {
|
||||
var meta = document.querySelector('meta[name="_csrf"]');
|
||||
return meta ? meta.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
// Validate date range (max 40 days)
|
||||
function validateDateRange(startDate, endDate) {
|
||||
if (!startDate || !endDate) {
|
||||
return { valid: false, message: '시작일과 종료일을 입력해주세요.' };
|
||||
}
|
||||
var start = new Date(startDate);
|
||||
var end = new Date(endDate);
|
||||
if (start > end) {
|
||||
return { valid: false, message: '시작일이 종료일보다 늦을 수 없습니다.' };
|
||||
}
|
||||
if (!startDate || !endDate) return { valid: false, message: '시작일과 종료일을 입력해주세요.' };
|
||||
var start = new Date(startDate), end = new Date(endDate);
|
||||
if (start > end) return { valid: false, message: '시작일이 종료일보다 늦을 수 없습니다.' };
|
||||
var diffDays = Math.floor((end - start) / (1000 * 60 * 60 * 24));
|
||||
if (diffDays > MAX_DATE_RANGE_DAYS) {
|
||||
return { valid: false, message: '조회 기간은 최대 ' + MAX_DATE_RANGE_DAYS + '일까지 가능합니다.' };
|
||||
}
|
||||
if (diffDays > MAX_DATE_RANGE_DAYS) return { valid: false, message: '조회 기간은 최대 ' + MAX_DATE_RANGE_DAYS + '일까지 가능합니다.' };
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// Search Statistics
|
||||
function searchStatistics() {
|
||||
const startDate = $('#startDate').val();
|
||||
const endDate = $('#endDate').val();
|
||||
function buildPayload() {
|
||||
return {
|
||||
mode: currentMode,
|
||||
startDate: $('#startDate').val(),
|
||||
endDate: $('#endDate').val(),
|
||||
month: $('#month').val() || null,
|
||||
clientId: selectedClientId || null
|
||||
};
|
||||
}
|
||||
|
||||
// Client-side validation
|
||||
var validation = validateDateRange(startDate, endDate);
|
||||
if (!validation.valid) {
|
||||
alert(validation.message);
|
||||
return;
|
||||
function searchStatistics() {
|
||||
if (currentMode === 'MONTHLY') {
|
||||
if (!$('#month').val()) { alert('조회할 월을 선택해주세요.'); return; }
|
||||
} else {
|
||||
var v = validateDateRange($('#startDate').val(), $('#endDate').val());
|
||||
if (!v.valid) { alert(v.message); return; }
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/statistics/api/search',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader('X-XSRF-TOKEN', getCsrfToken());
|
||||
},
|
||||
data: JSON.stringify({
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
clientId: selectedClientId || null
|
||||
}),
|
||||
beforeSend: function(xhr) { xhr.setRequestHeader('X-XSRF-TOKEN', getCsrfToken()); },
|
||||
data: JSON.stringify(buildPayload()),
|
||||
success: function(data) {
|
||||
currentData = data;
|
||||
updateSummaryDisplay();
|
||||
updateChart();
|
||||
updateTable();
|
||||
updateDownloadLink();
|
||||
updatePeriodTable();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Search failed:', error);
|
||||
error: function(xhr) {
|
||||
var errorMsg = '통계 조회에 실패했습니다.';
|
||||
if (xhr.responseJSON && xhr.responseJSON.error) {
|
||||
errorMsg = xhr.responseJSON.error;
|
||||
}
|
||||
if (xhr.responseJSON && xhr.responseJSON.error) errorMsg = xhr.responseJSON.error;
|
||||
alert(errorMsg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event Handlers
|
||||
$('#btnSearch').on('click', function() {
|
||||
searchStatistics();
|
||||
});
|
||||
|
||||
// App selection change
|
||||
$('#appSelect').on('change', function() {
|
||||
selectedClientId = $(this).val();
|
||||
searchStatistics();
|
||||
});
|
||||
|
||||
// Enter key search
|
||||
$('#startDate, #endDate').on('keypress', function(e) {
|
||||
if (e.which === 13) {
|
||||
searchStatistics();
|
||||
// ── 모드 토글 ──
|
||||
$('.mode-tab').on('click', function() {
|
||||
currentMode = $(this).data('mode');
|
||||
$('.mode-tab').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
// 하단 기간표는 월별 조회에서도 '일별'로 표기하므로 라벨은 항상 일자 유지
|
||||
if (currentMode === 'MONTHLY') {
|
||||
$('#dailyFilter').hide();
|
||||
$('#monthlyFilter').show();
|
||||
} else {
|
||||
$('#monthlyFilter').hide();
|
||||
$('#dailyFilter').show();
|
||||
}
|
||||
});
|
||||
|
||||
// Date change auto search
|
||||
$('#startDate, #endDate').on('change', function() {
|
||||
searchStatistics();
|
||||
});
|
||||
|
||||
$('#btnSearch').on('click', searchStatistics);
|
||||
$('#appSelect').on('change', function() { selectedClientId = $(this).val(); searchStatistics(); });
|
||||
$('#startDate, #endDate').on('change', searchStatistics);
|
||||
$('#month').on('change', searchStatistics);
|
||||
$('#startDate, #endDate').on('keypress', function(e) { if (e.which === 13) searchStatistics(); });
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<option>DJBank 모바일뱅킹</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="footer-contact">고객센터 1588-3388</p>
|
||||
<p class="footer-contact" th:text="'고객센터 ' + ${customerCenterContact}">고객센터 1588-3388</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user