Merge branch 'jenkins_with_weblogic' of ssh://git@192.168.240.178:18081/eapim/eapim-admin.git into jenkins_with_weblogic
This commit is contained in:
+2
-1
@@ -243,4 +243,5 @@ gradle-gf63.sh
|
||||
|
||||
eapim-admin-kjb/
|
||||
.claude
|
||||
*.report.md
|
||||
*.report.md
|
||||
*.claude.md
|
||||
@@ -1,6 +1,17 @@
|
||||
# CLAUDE.md
|
||||
# AI-Assisted Development Guide
|
||||
|
||||
이 파일은 Claude Code (claude.ai/code)가 이 저장소의 코드를 작업할 때 참고할 가이드를 제공합니다.
|
||||
이 문서는 AI 어시스턴트가 이 저장소의 코드를 이해하고 기여하는 데 필요한 가이드를 제공합니다.
|
||||
|
||||
## 프로젝트 주요 특징
|
||||
|
||||
AI 어시스턴트가 작업을 시작하기 전에 반드시 이해해야 할 핵심적인 특징들입니다.
|
||||
|
||||
- **하이브리드 데이터 액세스**: 이 프로젝트는 **JPA(Hibernate)와 iBATIS를 함께 사용**합니다.
|
||||
- **신규 기능**: 항상 **JPA**와 Spring Data 리포지토리를 사용해야 합니다.
|
||||
- **레거시 코드**: 기존 iBATIS (`*.xml` 매퍼) 코드를 수정할 때는 해당 패턴을 따라야 합니다.
|
||||
- **멀티 모듈 Gradle 프로젝트**: `settings.gradle`에 정의된 것처럼 여러 하위 모듈(`elink-online-*`, `kjb-*` 등)로 구성되어 있습니다. 특정 기능은 다른 모듈에 위치할 수 있습니다.
|
||||
- **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `kjb-gradle.sh compileJava`를 먼저 실행하여 코드를 생성해야 합니다.
|
||||
- **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다.
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
@@ -27,48 +38,86 @@ eLink EMS (eLink Management System)는 eLink의 웹 기반 관리 서비스로,
|
||||
## 빌드 및 개발 명령어
|
||||
|
||||
### 빌드 명령어
|
||||
|
||||
**⚠️ 중요**: 환경변수 문제로 인해 `gradle` 대신 `kjb-gradle.sh` 스크립트를 사용해야 합니다.
|
||||
- 스크립트 위치: `/c/eactive/workspaces/shell-scripts/kjb-gradle.sh`
|
||||
- PATH에 등록되어 있으므로 바로 `kjb-gradle.sh` 명령어 사용 가능
|
||||
|
||||
```bash
|
||||
# 표준 빌드
|
||||
gradle build
|
||||
kjb-gradle.sh build
|
||||
|
||||
# Weblogic 배포용 빌드 (테스트 제외)
|
||||
gradle build -x test -Pprofile=weblogic
|
||||
kjb-gradle.sh build -x test -Pprofile=weblogic
|
||||
|
||||
# WAR 파일 빌드
|
||||
gradle war
|
||||
kjb-gradle.sh war
|
||||
|
||||
# 클린 빌드
|
||||
gradle clean build
|
||||
kjb-gradle.sh clean build
|
||||
```
|
||||
|
||||
### 테스트 실행
|
||||
```bash
|
||||
# 모든 테스트 실행
|
||||
gradle test
|
||||
kjb-gradle.sh test
|
||||
|
||||
# 특정 테스트 클래스 실행
|
||||
gradle test --tests "com.example.ClassName"
|
||||
kjb-gradle.sh test --tests "com.example.ClassName"
|
||||
|
||||
# 특정 테스트 패키지 실행 (JUnit 플랫폼 사용)
|
||||
gradle test --tests "com.eactive.eai.rms.*"
|
||||
kjb-gradle.sh test --tests "com.eactive.eai.rms.*"
|
||||
```
|
||||
|
||||
### 개발 태스크
|
||||
|
||||
```bash
|
||||
# 패키징 없이 클래스만 컴파일
|
||||
gradle classes
|
||||
kjb-gradle.sh classes
|
||||
|
||||
# QueryDSL Q-classes 및 기타 애노테이션 생성
|
||||
gradle compileJava
|
||||
kjb-gradle.sh compileJava
|
||||
|
||||
# 의존성 트리 보기
|
||||
gradle dependencies
|
||||
kjb-gradle.sh dependencies
|
||||
|
||||
# 사용 가능한 모든 태스크 목록
|
||||
gradle tasks --all
|
||||
kjb-gradle.sh tasks --all
|
||||
```
|
||||
|
||||
## 로컬 개발 빠른 시작
|
||||
|
||||
새로운 환경에서 프로젝트를 처음 설정하고 실행하는 권장 절차입니다.
|
||||
|
||||
1. **IDE 설정 파일 생성 (최초 1회)**
|
||||
```bash
|
||||
# IntelliJ IDEA 사용 시
|
||||
kjb-gradle.sh idea
|
||||
|
||||
# Eclipse 사용 시
|
||||
kjb-gradle.sh eclipse
|
||||
```
|
||||
|
||||
2. **QueryDSL 등 소스 코드 생성**
|
||||
```bash
|
||||
kjb-gradle.sh compileJava
|
||||
```
|
||||
|
||||
3. **전체 빌드 및 테스트 실행**
|
||||
```bash
|
||||
kjb-gradle.sh build
|
||||
```
|
||||
|
||||
4. **IDE에서 프로젝트 열기**
|
||||
- `kjb-gradle.sh idea` 또는 `kjb-gradle.sh eclipse` 실행 후, IDE에서 프로젝트를 엽니다.
|
||||
|
||||
5. **실행 구성(Run Configuration) 설정**
|
||||
- 사용하는 WAS(Tomcat 등)에 `eapim-admin` 프로젝트를 배포하도록 설정합니다.
|
||||
- **VM Options**에 위에 명시된 "필수 환경 변수 (Tomcat)"를 모두 추가합니다.
|
||||
|
||||
6. **서버 실행**
|
||||
- IDE에서 설정한 실행 구성을 시작하여 서버를 실행합니다.
|
||||
|
||||
## 아키텍처 및 모듈 의존성
|
||||
|
||||
### 멀티 모듈 구조
|
||||
@@ -111,7 +160,7 @@ eapim-admin (root project)
|
||||
|
||||
이 프로젝트는 **Gradle과 Eclipse 애노테이션 프로세서 모두를 사용**하므로, Q-classes가 **두 위치에 생성**됩니다:
|
||||
|
||||
1. **`build/generated/java/`** - `gradle compileJava` 또는 `gradle build` 실행 시 Gradle이 생성
|
||||
1. **`build/generated/java/`** - `kjb-gradle.sh compileJava` 또는 `kjb-gradle.sh build` 실행 시 Gradle이 생성
|
||||
2. **`WebContent/generated/`** - Eclipse IDE에서 빌드 시 Eclipse APT가 생성
|
||||
|
||||
**이는 예상된 동작이며 의도적입니다.** 두 디렉토리 모두 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다.
|
||||
@@ -120,8 +169,8 @@ eapim-admin (root project)
|
||||
|
||||
- **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다
|
||||
- **Eclipse IDE**: `WebContent/generated/`의 Q-classes가 자동으로 classpath에 포함됩니다
|
||||
- **업데이트 pull 후**: `gradle compileJava`를 실행하거나 Eclipse 프로젝트를 새로고침하여 Q-classes를 재생성합니다
|
||||
- **Q-classes IDE 오류 시**: `gradle clean compileJava` 또는 Eclipse → Project → Clean으로 재생성합니다
|
||||
- **업데이트 pull 후**: `kjb-gradle.sh compileJava`를 실행하거나 Eclipse 프로젝트를 새로고침하여 Q-classes를 재생성합니다
|
||||
- **Q-classes IDE 오류 시**: `kjb-gradle.sh clean compileJava` 또는 Eclipse → Project → Clean으로 재생성합니다
|
||||
|
||||
중복이 문제를 일으키지 않는 이유:
|
||||
- 두 도구 모두 동일한 JPA 엔티티에서 동일한 Q-classes를 생성합니다
|
||||
@@ -132,6 +181,9 @@ eapim-admin (root project)
|
||||
|
||||
### 필수 환경 변수 (Tomcat)
|
||||
|
||||
다음은 로컬 Tomcat 환경에서 개발 시 필요한 시스템 프로퍼티(-D)입니다.
|
||||
**설정 방법**: IntelliJ, Eclipse 등 IDE의 실행 구성(Run Configuration)에서 'VM options'에 추가하거나, 사용하는 Tomcat의 `bin` 폴더에 `setenv.bat` (Windows) 또는 `setenv.sh` (Linux/macOS) 파일을 생성하여 아래 내용들을 `CATALINA_OPTS` 또는 `JAVA_OPTS` 환경 변수에 추가합니다.
|
||||
|
||||
```
|
||||
-Deai.datasource.type=DEV
|
||||
-Dinst.Name=emsSvr99
|
||||
@@ -202,7 +254,7 @@ eapim-admin (root project)
|
||||
WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다:
|
||||
|
||||
```bash
|
||||
gradle build -x test -Pprofile=weblogic
|
||||
kjb-gradle.sh build -x test -Pprofile=weblogic
|
||||
```
|
||||
|
||||
## 기술 스택
|
||||
@@ -279,7 +331,7 @@ com.eactive.eai
|
||||
### Eclipse
|
||||
|
||||
```bash
|
||||
gradle eclipse
|
||||
kjb-gradle.sh eclipse
|
||||
```
|
||||
|
||||
다음을 생성합니다:
|
||||
@@ -291,7 +343,7 @@ gradle eclipse
|
||||
### IntelliJ IDEA
|
||||
|
||||
```bash
|
||||
gradle idea
|
||||
kjb-gradle.sh idea
|
||||
```
|
||||
|
||||
또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# GEMINI.md
|
||||
|
||||
이 프로젝트에 대한 주요 정보 및 지침은 `CLAUDE.md` 파일을 참조하십시오.
|
||||
(For key information and guidelines regarding this project, please refer to the `CLAUDE.md` file.)
|
||||
@@ -201,4 +201,7 @@
|
||||
|
||||
<!-- 로그추축 -->
|
||||
<sqlMap resource="com/eactive/eai/apim/portalstatistics/EAILog-oracle.xml" />
|
||||
|
||||
<!-- ObpGwMetric 시간별 거래량 지표 -->
|
||||
<sqlMap resource="com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml" />
|
||||
</sqlMapConfig>
|
||||
@@ -362,4 +362,209 @@
|
||||
console.error('Download failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스타일링된 Alert 모달
|
||||
* @param {string} message - 표시할 메시지
|
||||
* @param {object} options - 옵션 (type: 'info'|'warning'|'error'|'success', title: string, onClose: function)
|
||||
*/
|
||||
function showAlert(message, options) {
|
||||
options = options || {};
|
||||
var type = options.type || 'info';
|
||||
var title = options.title || getAlertTitle(type);
|
||||
var onClose = options.onClose || function(){};
|
||||
|
||||
// 기존 모달 제거
|
||||
$('#commonAlertModal').remove();
|
||||
|
||||
var iconHtml = getAlertIcon(type);
|
||||
var colorClass = 'alert-' + type;
|
||||
|
||||
var modalHtml =
|
||||
'<div id="commonAlertModal" class="common-alert-overlay">' +
|
||||
'<div class="common-alert-modal ' + colorClass + '">' +
|
||||
'<div class="common-alert-header">' +
|
||||
'<span class="common-alert-icon">' + iconHtml + '</span>' +
|
||||
'<span class="common-alert-title">' + title + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="common-alert-body">' +
|
||||
'<p class="common-alert-message">' + message + '</p>' +
|
||||
'</div>' +
|
||||
'<div class="common-alert-footer">' +
|
||||
'<button type="button" class="common-alert-btn" id="commonAlertOkBtn">확인</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
$('body').append(modalHtml);
|
||||
|
||||
// 애니메이션 효과
|
||||
setTimeout(function() {
|
||||
$('#commonAlertModal').addClass('show');
|
||||
}, 10);
|
||||
|
||||
// 닫기 이벤트
|
||||
$('#commonAlertOkBtn').on('click', function() {
|
||||
closeCommonAlert(onClose);
|
||||
});
|
||||
|
||||
// ESC 키로 닫기
|
||||
$(document).on('keydown.commonAlert', function(e) {
|
||||
if (e.keyCode === 27) {
|
||||
closeCommonAlert(onClose);
|
||||
}
|
||||
});
|
||||
|
||||
// Enter 키로 확인
|
||||
$(document).on('keydown.commonAlertEnter', function(e) {
|
||||
if (e.keyCode === 13) {
|
||||
closeCommonAlert(onClose);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeCommonAlert(callback) {
|
||||
$('#commonAlertModal').removeClass('show');
|
||||
setTimeout(function() {
|
||||
$('#commonAlertModal').remove();
|
||||
$(document).off('keydown.commonAlert');
|
||||
$(document).off('keydown.commonAlertEnter');
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function getAlertTitle(type) {
|
||||
switch(type) {
|
||||
case 'error': return '오류';
|
||||
case 'warning': return '경고';
|
||||
case 'success': return '완료';
|
||||
default: return '알림';
|
||||
}
|
||||
}
|
||||
|
||||
function getAlertIcon(type) {
|
||||
switch(type) {
|
||||
case 'error': return '<i class="material-icons">error</i>';
|
||||
case 'warning': return '<i class="material-icons">warning</i>';
|
||||
case 'success': return '<i class="material-icons">check_circle</i>';
|
||||
default: return '<i class="material-icons">info</i>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스타일링된 Confirm 모달
|
||||
* @param {string} message - 표시할 메시지
|
||||
* @param {object} options - 옵션 (type, title, confirmText, cancelText, onConfirm, onCancel)
|
||||
*/
|
||||
function showConfirm(message, options) {
|
||||
options = options || {};
|
||||
var type = options.type || 'info';
|
||||
var title = options.title || '확인';
|
||||
var confirmText = options.confirmText || '확인';
|
||||
var cancelText = options.cancelText || '취소';
|
||||
var onConfirm = options.onConfirm || function(){};
|
||||
var onCancel = options.onCancel || function(){};
|
||||
|
||||
// 기존 모달 제거
|
||||
$('#commonConfirmModal').remove();
|
||||
|
||||
var iconHtml = getAlertIcon(type);
|
||||
var colorClass = 'alert-' + type;
|
||||
|
||||
var modalHtml =
|
||||
'<div id="commonConfirmModal" class="common-alert-overlay">' +
|
||||
'<div class="common-alert-modal ' + colorClass + '">' +
|
||||
'<div class="common-alert-header">' +
|
||||
'<span class="common-alert-icon">' + iconHtml + '</span>' +
|
||||
'<span class="common-alert-title">' + title + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="common-alert-body">' +
|
||||
'<p class="common-alert-message">' + message + '</p>' +
|
||||
'</div>' +
|
||||
'<div class="common-alert-footer">' +
|
||||
'<button type="button" class="common-confirm-cancel-btn" id="commonConfirmCancelBtn">' + cancelText + '</button>' +
|
||||
'<button type="button" class="common-alert-btn" id="commonConfirmOkBtn">' + confirmText + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
$('body').append(modalHtml);
|
||||
|
||||
setTimeout(function() {
|
||||
$('#commonConfirmModal').addClass('show');
|
||||
}, 10);
|
||||
|
||||
// 확인 버튼
|
||||
$('#commonConfirmOkBtn').on('click', function() {
|
||||
closeCommonConfirm(onConfirm);
|
||||
});
|
||||
|
||||
// 취소 버튼
|
||||
$('#commonConfirmCancelBtn').on('click', function() {
|
||||
closeCommonConfirm(onCancel);
|
||||
});
|
||||
|
||||
// ESC 키로 취소
|
||||
$(document).on('keydown.commonConfirm', function(e) {
|
||||
if (e.keyCode === 27) {
|
||||
closeCommonConfirm(onCancel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeCommonConfirm(callback) {
|
||||
$('#commonConfirmModal').removeClass('show');
|
||||
setTimeout(function() {
|
||||
$('#commonConfirmModal').remove();
|
||||
$(document).off('keydown.commonConfirm');
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 공용 Alert 모달 스타일 -->
|
||||
<style>
|
||||
.common-alert-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:99999; justify-content:center; align-items:center; }
|
||||
.common-alert-overlay.show { display:flex; animation:alertFadeIn 0.2s ease; }
|
||||
@keyframes alertFadeIn { from { opacity:0; } to { opacity:1; } }
|
||||
.common-alert-modal { background:#fff; border-radius:8px; box-shadow:0 8px 32px rgba(0,0,0,0.3); width:380px; max-width:90%; overflow:hidden; animation:alertSlideIn 0.2s ease; }
|
||||
@keyframes alertSlideIn { from { transform:translateY(-20px); opacity:0; } to { transform:translateY(0); opacity:1; } }
|
||||
.common-alert-header { display:flex; align-items:center; gap:10px; padding:16px 20px; border-bottom:1px solid #eee; }
|
||||
.common-alert-icon { display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:50%; }
|
||||
.common-alert-icon i { font-size:20px; }
|
||||
.common-alert-title { font-size:15px; font-weight:600; color:#333; }
|
||||
.common-alert-body { padding:20px; }
|
||||
.common-alert-message { margin:0; font-size:13px; line-height:1.6; color:#555; word-break:keep-all; }
|
||||
.common-alert-footer { display:flex; justify-content:flex-end; padding:12px 20px; background:#fafafa; border-top:1px solid #eee; }
|
||||
.common-alert-btn { padding:8px 24px; font-size:13px; font-weight:500; border:none; border-radius:4px; cursor:pointer; transition:all 0.2s; }
|
||||
.common-confirm-cancel-btn { padding:8px 24px; font-size:13px; font-weight:500; border:1px solid #ddd; border-radius:4px; cursor:pointer; transition:all 0.2s; background:#fff; color:#666; margin-right:8px; }
|
||||
.common-confirm-cancel-btn:hover { background:#f5f5f5; border-color:#ccc; }
|
||||
|
||||
/* Info 타입 (기본) */
|
||||
.alert-info .common-alert-icon { background:rgba(0,128,128,0.1); }
|
||||
.alert-info .common-alert-icon i { color:rgba(0,128,128,1); }
|
||||
.alert-info .common-alert-btn { background:rgba(0,128,128,0.85); color:#fff; }
|
||||
.alert-info .common-alert-btn:hover { background:rgba(0,128,128,1); }
|
||||
|
||||
/* Warning 타입 */
|
||||
.alert-warning .common-alert-icon { background:rgba(255,152,0,0.1); }
|
||||
.alert-warning .common-alert-icon i { color:#ff9800; }
|
||||
.alert-warning .common-alert-btn { background:#ff9800; color:#fff; }
|
||||
.alert-warning .common-alert-btn:hover { background:#f57c00; }
|
||||
|
||||
/* Error 타입 */
|
||||
.alert-error .common-alert-icon { background:rgba(244,67,54,0.1); }
|
||||
.alert-error .common-alert-icon i { color:#f44336; }
|
||||
.alert-error .common-alert-btn { background:#f44336; color:#fff; }
|
||||
.alert-error .common-alert-btn:hover { background:#d32f2f; }
|
||||
|
||||
/* Success 타입 */
|
||||
.alert-success .common-alert-icon { background:rgba(76,175,80,0.1); }
|
||||
.alert-success .common-alert-icon i { color:#4caf50; }
|
||||
.alert-success .common-alert-btn { background:#4caf50; color:#fff; }
|
||||
.alert-success .common-alert-btn:hover { background:#388e3c; }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<!-- 유량제어 그룹 상세 페이지 스타일 -->
|
||||
<style>
|
||||
/* 폼 테이블 - 기존 table_row 스타일 기반 */
|
||||
.form-table { width:100%; border:0; border-top:1px solid #a3a9b1; border-left:1px solid #a3a9b1; font-size:12px; color:#333; margin-bottom:15px; }
|
||||
.form-table th { padding:8px 15px; border-right:1px solid #ebebec; border-bottom:1px solid #a3a9b1; text-align:left; background:rgb(255, 247, 231); font-weight:bold; white-space:nowrap; }
|
||||
.form-table td { padding:8px 15px; border-right:1px solid #a3a9b1; border-bottom:1px solid #a3a9b1; }
|
||||
.form-table input[type="text"], .form-table input[type="number"] { border:1px solid #ebebec; padding:4px 8px; }
|
||||
.form-table input[type="number"] { width:100px; text-align:right; }
|
||||
.form-table input:disabled { background:#ebebec; color:#999; }
|
||||
.form-table .select-style { display:inline-block; overflow:hidden; border:1px solid #ebebec; background:#fff; }
|
||||
.form-table .select-style select { border:none; box-shadow:none; background:transparent; padding:4px 8px; min-width:80px; }
|
||||
|
||||
/* iOS 스타일 토글 */
|
||||
.toggle-wrap { display:inline-flex; align-items:center; gap:10px; }
|
||||
.toggle-label { font-size:12px; color:#333; min-width:40px; }
|
||||
.ios-toggle { position:relative; display:inline-block; width:44px; height:24px; }
|
||||
.ios-toggle input { display:none; }
|
||||
.ios-toggle-slider { position:absolute; top:0; left:0; right:0; bottom:0; background:#ccc; border-radius:24px; cursor:pointer; transition:all 0.3s ease; }
|
||||
.ios-toggle-slider::before { content:''; position:absolute; top:2px; left:2px; width:20px; height:20px; background:#fff; border-radius:50%; box-shadow:0 1px 3px rgba(0,0,0,0.2); transition:all 0.3s ease; }
|
||||
.ios-toggle input:checked + .ios-toggle-slider { background:rgba(0,128,128,0.85); }
|
||||
.ios-toggle input:checked + .ios-toggle-slider::before { transform:translateX(20px); }
|
||||
|
||||
/* 임계치 입력 그룹 */
|
||||
.threshold-group { display:inline-flex; align-items:center; gap:8px; }
|
||||
.threshold-group .unit { font-size:11px; color:#666; min-width:60px; }
|
||||
|
||||
/* 스타일드 셀렉트 */
|
||||
.styled-select { appearance:none; -webkit-appearance:none; -moz-appearance:none; padding:6px 30px 6px 12px; font-size:12px; border:1px solid #ddd; border-radius:4px; background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E") no-repeat right 10px center; cursor:pointer; min-width:120px; transition:all 0.2s; color:#333; }
|
||||
.styled-select:hover { border-color:rgba(0,128,128,0.5); }
|
||||
.styled-select:focus { outline:none; border-color:rgba(0,128,128,0.8); box-shadow:0 0 0 2px rgba(0,128,128,0.1); }
|
||||
|
||||
/* 힌트 아이콘 */
|
||||
.hint-icon { display:inline-flex; align-items:center; cursor:pointer; color:#999; position:relative; margin-left:4px; vertical-align:middle; }
|
||||
.hint-icon:hover { color:rgba(0,128,128,1); }
|
||||
.hint-icon i { font-size:14px; }
|
||||
.hint-icon .hint-bubble { display:none; position:absolute; bottom:calc(100% + 8px); left:0; background:#333; color:#fff; padding:10px 12px; border-radius:4px; font-size:11px; width:220px; line-height:1.5; z-index:1000; box-shadow:0 2px 8px rgba(0,0,0,0.2); white-space:normal; word-break:keep-all; }
|
||||
.hint-icon .hint-bubble::after { content:''; position:absolute; top:100%; left:14px; border:6px solid transparent; border-top-color:#333; }
|
||||
.hint-icon:hover .hint-bubble { display:block; }
|
||||
|
||||
/* 섹션 구분 라벨 */
|
||||
.section-label { display:block; font-size:13px; color:#333; font-weight:bold; margin:20px 0 10px 0; padding-bottom:5px; border-bottom:2px solid rgba(0,128,128,0.3); }
|
||||
|
||||
/* 타이틀 래퍼 */
|
||||
.title-wrap { display:flex; align-items:center; gap:12px; margin-bottom:10px; }
|
||||
.title-wrap .title { margin-bottom:0; }
|
||||
|
||||
/* 수정일시 뱃지 */
|
||||
.modified-badge { display:inline-flex; align-items:center; gap:4px; padding:4px 10px; background:linear-gradient(135deg, #f0f7f7 0%, #e8f4f4 100%); border:1px solid rgba(0,128,128,0.2); border-radius:20px; font-size:11px; color:#555; }
|
||||
.modified-badge i { font-size:13px; color:rgba(0,128,128,0.7); }
|
||||
|
||||
/* 인터페이스 리스트 헤더 */
|
||||
.interface-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
|
||||
.interface-count { font-size:12px; color:#666; }
|
||||
|
||||
/* 인터페이스 리스트 테이블 */
|
||||
.interface-list-wrap { border:1px solid #a3a9b1; border-radius:3px; max-height:250px; overflow-y:auto; margin-bottom:15px; }
|
||||
.interface-list-table { width:100%; border-collapse:collapse; font-size:12px; }
|
||||
.interface-list-table th { padding:8px 12px; background:rgb(255, 247, 231); border-bottom:1px solid #a3a9b1; text-align:left; font-weight:bold; position:sticky; top:0; }
|
||||
.interface-list-table td { padding:8px 12px; border-bottom:1px solid #ebebec; }
|
||||
.interface-list-table tr:last-child td { border-bottom:none; }
|
||||
.interface-list-table tr:hover { background:#f9f9f9; }
|
||||
.interface-list-table .col-id { width:200px; font-family:Consolas, monospace; color:rgb(65,125,200); }
|
||||
.interface-list-table .col-action { width:80px; text-align:center; white-space:nowrap; }
|
||||
|
||||
/* 미니 토글 - teal 컬러 */
|
||||
.mini-toggle { position:relative; display:inline-block; width:32px; height:18px; vertical-align:middle; margin-right:8px; }
|
||||
.mini-toggle input { display:none; }
|
||||
.mini-toggle-slider { position:absolute; top:0; left:0; right:0; bottom:0; background:#ccc; border-radius:18px; cursor:pointer; transition:all 0.2s; }
|
||||
.mini-toggle-slider::before { content:''; position:absolute; top:2px; left:2px; width:14px; height:14px; background:#fff; border-radius:50%; transition:all 0.2s; }
|
||||
.mini-toggle input:checked + .mini-toggle-slider { background:rgba(0,128,128,0.8); }
|
||||
.mini-toggle input:checked + .mini-toggle-slider::before { transform:translateX(14px); }
|
||||
|
||||
/* 인라인 삭제 버튼 */
|
||||
.btn-icon { background:none; border:none; cursor:pointer; color:#999; padding:2px; transition:color 0.2s; vertical-align:middle; }
|
||||
.btn-icon:hover { color:#ff4d4f; }
|
||||
.btn-icon i { font-size:16px; }
|
||||
|
||||
/* 빈 상태 메시지 */
|
||||
.interface-empty { padding:30px 20px; text-align:center; color:#999; font-size:12px; }
|
||||
.interface-empty i { vertical-align:middle; margin-right:4px; font-size:18px; }
|
||||
|
||||
/* 탭 스타일 */
|
||||
.detail-tabs { display:flex; gap:0; border-bottom:2px solid rgba(0,128,128,0.3); margin-bottom:20px; }
|
||||
.detail-tab { padding:10px 24px; font-size:13px; font-weight:bold; color:#666; cursor:pointer; border:none; background:transparent; border-bottom:2px solid transparent; margin-bottom:-2px; transition:all 0.2s; }
|
||||
.detail-tab:hover { color:rgba(0,128,128,0.8); background:rgba(0,128,128,0.05); }
|
||||
.detail-tab.active { color:rgba(0,128,128,1); border-bottom-color:rgba(0,128,128,0.8); background:rgba(0,128,128,0.05); }
|
||||
.detail-tab i { vertical-align:middle; margin-right:4px; font-size:18px; }
|
||||
.tab-content { display:none; }
|
||||
.tab-content.active { display:block; }
|
||||
|
||||
/* 버킷 현황 스타일 */
|
||||
.bucket-status-wrap { margin-top:10px; }
|
||||
.bucket-refresh-bar { display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; }
|
||||
.bucket-refresh-bar .last-refresh { font-size:11px; color:#999; }
|
||||
.bucket-server-list { display:flex; flex-wrap:wrap; gap:15px; }
|
||||
.bucket-server-card { flex:0 0 calc(33.333% - 10px); min-width:280px; border:1px solid #ddd; border-radius:5px; overflow:hidden; }
|
||||
.bucket-server-header { display:flex; justify-content:space-between; align-items:center; padding:10px 15px; background:#f9f9f9; border-bottom:1px solid #eee; }
|
||||
.bucket-server-name { font-weight:bold; font-size:13px; }
|
||||
.bucket-server-ip { font-size:11px; color:#888; font-family:Consolas, monospace; }
|
||||
.bucket-server-status { font-size:11px; padding:3px 8px; border-radius:10px; }
|
||||
.bucket-server-status.online { background:#e8f5e9; color:#2e7d32; }
|
||||
.bucket-server-status.offline { background:#ffebee; color:#c62828; }
|
||||
.bucket-server-status.no_data { background:#fff3e0; color:#ef6c00; }
|
||||
.bucket-server-body { padding:15px; }
|
||||
.bucket-info-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(280px, 1fr)); gap:15px; }
|
||||
.bucket-card { background:#fafafa; border:1px solid #eee; border-radius:4px; padding:12px 15px; }
|
||||
.bucket-card-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; }
|
||||
.bucket-type { font-size:12px; font-weight:bold; color:#333; }
|
||||
.bucket-type-badge { font-size:10px; padding:2px 6px; border-radius:3px; background:#e3f2fd; color:#1565c0; }
|
||||
.bucket-progress-wrap { margin-bottom:8px; }
|
||||
.bucket-progress-bar { height:8px; background:#e0e0e0; border-radius:4px; overflow:hidden; }
|
||||
.bucket-progress-fill { height:100%; border-radius:4px; transition:width 0.3s ease; }
|
||||
.bucket-progress-fill.low { background:linear-gradient(90deg, #4caf50, #66bb6a); }
|
||||
.bucket-progress-fill.medium { background:linear-gradient(90deg, #ff9800, #ffa726); }
|
||||
.bucket-progress-fill.high { background:linear-gradient(90deg, #f44336, #ef5350); }
|
||||
.bucket-stats { display:flex; justify-content:space-between; font-size:11px; color:#666; }
|
||||
.bucket-stats-value { font-family:Consolas, monospace; }
|
||||
.bucket-empty-msg { padding:30px; text-align:center; color:#999; font-size:12px; }
|
||||
.bucket-empty-msg i { font-size:36px; display:block; margin-bottom:10px; color:#ddd; }
|
||||
.bucket-error-msg { color:#c62828; font-size:12px; }
|
||||
|
||||
/* 전체 합산 요약 */
|
||||
.bucket-summary { background:linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border:1px solid #dee2e6; border-radius:5px; padding:15px 20px; margin-bottom:20px; }
|
||||
.bucket-summary-title { font-size:13px; font-weight:bold; color:#333; margin-bottom:12px; display:flex; align-items:center; gap:6px; }
|
||||
.bucket-summary-title i { font-size:18px; color:rgba(0,128,128,0.8); }
|
||||
.bucket-summary-grid { display:flex; gap:30px; flex-wrap:wrap; }
|
||||
.bucket-summary-item { display:flex; flex-direction:column; }
|
||||
.bucket-summary-label { font-size:11px; color:#666; margin-bottom:4px; }
|
||||
.bucket-summary-value { font-size:18px; font-weight:bold; color:#333; font-family:Consolas, monospace; }
|
||||
.bucket-summary-value .unit { font-size:12px; font-weight:normal; color:#888; margin-left:4px; }
|
||||
.bucket-summary-sub { font-size:11px; color:#888; margin-top:2px; }
|
||||
|
||||
/* 도움말 모달 */
|
||||
.help-modal-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.4); z-index:9999; }
|
||||
.help-modal { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:#fff; border-radius:5px; box-shadow:0 4px 20px rgba(0,0,0,0.3); width:700px; max-height:80vh; overflow-y:auto; }
|
||||
.help-modal-header { display:flex; justify-content:space-between; align-items:center; padding:12px 20px; border-bottom:1px solid #ddd; background:rgb(255, 247, 231); }
|
||||
.help-modal-header h3 { margin:0; font-size:14px; font-weight:bold; color:#333; }
|
||||
.help-modal-close { background:none; border:none; font-size:20px; cursor:pointer; color:#666; }
|
||||
.help-modal-close:hover { color:#333; }
|
||||
.help-modal-body { padding:20px; font-size:12px; line-height:1.6; }
|
||||
.help-modal-body h4 { margin:15px 0 8px 0; font-size:13px; color:#333; border-left:3px solid rgba(0,128,128,0.6); padding-left:10px; }
|
||||
.help-modal-body h4:first-child { margin-top:0; }
|
||||
.help-modal-body p { margin:6px 0; color:#555; }
|
||||
.help-modal-body ul { margin:6px 0; padding-left:18px; }
|
||||
.help-modal-body li { margin:4px 0; }
|
||||
.help-modal-body .example-box { background:#f9f9f9; border:1px solid #ddd; border-radius:3px; padding:12px; margin:8px 0; }
|
||||
.help-modal-body .example-title { font-weight:bold; color:#333; margin-bottom:6px; }
|
||||
.help-modal-body table { width:100%; border-collapse:collapse; margin:8px 0; }
|
||||
.help-modal-body table th, .help-modal-body table td { border:1px solid #ddd; padding:6px 10px; text-align:left; }
|
||||
.help-modal-body table th { background:rgb(255, 247, 231); }
|
||||
.help-modal-body .note { background:#fffde7; border:1px solid #fff176; border-radius:3px; padding:10px 12px; margin:12px 0; }
|
||||
|
||||
/* 인터페이스 선택 모달 */
|
||||
.if-modal-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.4); z-index:9998; }
|
||||
.if-modal { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:#fff; border-radius:5px; box-shadow:0 4px 20px rgba(0,0,0,0.3); width:850px; }
|
||||
.if-modal-header { display:flex; justify-content:space-between; align-items:center; padding:12px 20px; border-bottom:1px solid #ddd; background:rgb(255, 247, 231); border-radius:5px 5px 0 0; }
|
||||
.if-modal-header h3 { margin:0; font-size:14px; font-weight:bold; color:#333; }
|
||||
.if-modal-close { background:none; border:none; font-size:20px; cursor:pointer; color:#666; }
|
||||
.if-modal-close:hover { color:#333; }
|
||||
.if-modal-body { padding:15px 20px; }
|
||||
|
||||
/* 검색 영역 */
|
||||
.if-search-box { display:flex; gap:8px; margin-bottom:12px; }
|
||||
.if-search-box input { flex:1; border:1px solid #ebebec; padding:6px 10px; font-size:12px; }
|
||||
.if-search-box button { padding:6px 12px; font-size:12px; }
|
||||
|
||||
/* 테이블 */
|
||||
.if-table-wrap { border:1px solid #a3a9b1; border-radius:3px; max-height:400px; overflow-y:auto; }
|
||||
.if-table { width:100%; border-collapse:collapse; font-size:12px; }
|
||||
.if-table th { padding:8px 12px; background:rgb(255, 247, 231); border-bottom:1px solid #a3a9b1; text-align:left; font-weight:bold; position:sticky; top:0; }
|
||||
.if-table td { padding:8px 12px; border-bottom:1px solid #ebebec; cursor:pointer; }
|
||||
.if-table tr:last-child td { border-bottom:none; }
|
||||
.if-table tbody tr:hover { background:#e8f4f4; }
|
||||
.if-table tbody tr.selected { background:rgba(0,128,128,0.15); }
|
||||
.if-table .col-id { width:180px; font-family:Consolas, monospace; color:rgb(65,125,200); }
|
||||
.if-table-empty { padding:40px 20px; text-align:center; color:#999; }
|
||||
|
||||
/* 페이지네이션 */
|
||||
.if-pagination { display:flex; justify-content:space-between; align-items:center; margin-top:12px; font-size:12px; }
|
||||
.if-page-info { color:#666; }
|
||||
.if-page-nav { display:flex; align-items:center; gap:4px; }
|
||||
.if-page-btn { min-width:28px; height:28px; padding:0 6px; border:1px solid #ddd; background:#fff; cursor:pointer; font-size:11px; border-radius:3px; }
|
||||
.if-page-btn:hover:not(:disabled) { background:#f0f0f0; border-color:#bbb; }
|
||||
.if-page-btn:disabled { color:#ccc; cursor:default; }
|
||||
.if-page-btn.active { background:rgba(0,128,128,0.8); color:#fff; border-color:rgba(0,128,128,0.8); }
|
||||
.if-page-num { display:flex; gap:2px; }
|
||||
|
||||
/* 하단 버튼 */
|
||||
.if-modal-footer { display:flex; justify-content:flex-end; gap:8px; padding:12px 20px; border-top:1px solid #ddd; background:#fafafa; border-radius:0 0 5px 5px; }
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<!-- 유량제어 그룹 도움말 모달 -->
|
||||
<div class="help-modal-overlay" id="helpModal">
|
||||
<div class="help-modal">
|
||||
<div class="help-modal-header">
|
||||
<h3>유량제어 임계치 설명</h3>
|
||||
<button type="button" class="help-modal-close">×</button>
|
||||
</div>
|
||||
<div class="help-modal-body">
|
||||
<h4>개요</h4>
|
||||
<p>유량제어는 <strong>Token Bucket 알고리즘</strong> 기반으로 동작합니다.
|
||||
설정된 임계치만큼 토큰이 <strong>균등하게 리필</strong>되며, 요청 1건당 토큰 1개를 소비합니다.
|
||||
토큰이 부족하면 요청이 거부됩니다.</p>
|
||||
|
||||
<h4>토큰 리필 방식 - Greedy (균등 리필)</h4>
|
||||
<p>토큰은 설정된 시간이 지난 후 한꺼번에 리필되는 것이 아니라, <strong>매 순간 균등하게 리필</strong>됩니다.</p>
|
||||
<div class="note" style="background:#e3f2fd; border-color:#2196f3; margin-bottom:12px;">
|
||||
<strong>왜 균등 리필인가?</strong><br/>
|
||||
한꺼번에 리필하면 리필 직후 트래픽이 몰려 백엔드에 부하가 집중됩니다.
|
||||
균등 리필은 트래픽을 <strong>시간축에 고르게 분산</strong>시켜 시스템 안정성을 높입니다.
|
||||
AWS, Google Cloud, Nginx 등 대부분의 Rate Limiter가 이 방식을 채택합니다.
|
||||
</div>
|
||||
<div class="example-box">
|
||||
<div class="example-title">예시: 분당 120건 설정 시 리필 속도</div>
|
||||
<ul>
|
||||
<li>120건 ÷ 60초 = <strong>초당 2건씩</strong> 균등 리필</li>
|
||||
<li>1초마다 2개의 토큰이 자동으로 채워짐</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="example-box">
|
||||
<div class="example-title">동작 예시: 분당 120건, 10건 요청 처리 후</div>
|
||||
<table style="width:100%; margin-top:8px;">
|
||||
<tr><th style="width:100px;">시간</th><th>상황</th><th style="width:80px;">토큰 수</th></tr>
|
||||
<tr><td>10:00:00</td><td>초기 상태 (만땅)</td><td style="text-align:center;"><strong>120개</strong></td></tr>
|
||||
<tr><td>10:00:01</td><td>요청 10건 처리</td><td style="text-align:center;">110개</td></tr>
|
||||
<tr><td>10:00:02</td><td>1초 경과 → 2개 리필</td><td style="text-align:center;">112개</td></tr>
|
||||
<tr><td>10:00:03</td><td>1초 경과 → 2개 리필</td><td style="text-align:center;">114개</td></tr>
|
||||
<tr><td>10:00:04</td><td>1초 경과 → 2개 리필</td><td style="text-align:center;">116개</td></tr>
|
||||
<tr><td>10:00:05</td><td>1초 경과 → 2개 리필</td><td style="text-align:center;">118개</td></tr>
|
||||
<tr><td>10:00:06</td><td>1초 경과 → 2개 리필 (복구 완료)</td><td style="text-align:center;"><strong>120개</strong></td></tr>
|
||||
</table>
|
||||
<p style="margin-top:10px; color:#666; font-size:11px;">
|
||||
※ 10건 소비 후 5초만에 복구됨 (10건 ÷ 초당2건 = 5초)
|
||||
</p>
|
||||
</div>
|
||||
<div class="note" style="background:#fff3e0; border-color:#ffb74d;">
|
||||
<strong>주의:</strong> "분당 120건"은 "1분 후에 120개가 한꺼번에 채워진다"는 의미가 아닙니다.
|
||||
매 초마다 2개씩 균등하게 리필되어, 결과적으로 1분 동안 최대 120건을 처리할 수 있다는 의미입니다.
|
||||
</div>
|
||||
|
||||
<h4>초당 임계치 (thresholdPerSecond)</h4>
|
||||
<p>1초 단위의 순간적인 트래픽 급증(spike)을 방어합니다.</p>
|
||||
<div class="example-box">
|
||||
<div class="example-title">예시: 초당 임계치 = 100</div>
|
||||
<ul>
|
||||
<li>매 1초마다 토큰 100개 리필</li>
|
||||
<li>1초 내 100건까지 처리 가능</li>
|
||||
<li>101번째 요청부터 차단</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4>추가 임계치 (threshold) + 시간단위</h4>
|
||||
<p>장시간 누적 트래픽을 제어합니다. 시간단위에 따라 리필 속도가 결정됩니다.</p>
|
||||
<table>
|
||||
<tr><th>시간단위</th><th>설명</th><th>리필 속도 예시 (120건 설정 시)</th></tr>
|
||||
<tr><td>MIN</td><td>분</td><td>초당 2건 (120÷60)</td></tr>
|
||||
<tr><td>HOU</td><td>시</td><td>초당 0.033건 (120÷3600)</td></tr>
|
||||
<tr><td>DAY</td><td>일</td><td>초당 0.00139건 (120÷86400)</td></tr>
|
||||
<tr><td>MON</td><td>월</td><td>초당 0.0000463건 (120÷2592000)</td></tr>
|
||||
</table>
|
||||
|
||||
<h4>두 임계치 조합 (권장)</h4>
|
||||
<p>두 조건을 함께 설정하면 <strong>이중 보호</strong>가 적용됩니다.</p>
|
||||
<div class="example-box">
|
||||
<div class="example-title">예시: 초당 100건 + 시간당 50,000건</div>
|
||||
<ul>
|
||||
<li><strong>초당 임계치</strong>: 순간 폭주 방지 → 1초에 101건 이상 요청 시 차단</li>
|
||||
<li><strong>추가 임계치</strong>: 장기 누적 제한 → 시간당 50,001건 이상 요청 시 차단</li>
|
||||
<li>두 조건 중 하나라도 초과 시 차단 (둘 다 충족해야 통과)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>참고:</strong> 그룹에 매핑된 인터페이스들은 개별 유량제어 대신 그룹 유량제어가 적용됩니다.
|
||||
그룹 내 모든 인터페이스의 호출이 합산되어 임계치와 비교됩니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<!-- 인터페이스 선택 모달 -->
|
||||
<div class="if-modal-overlay" id="ifModal">
|
||||
<div class="if-modal">
|
||||
<div class="if-modal-header">
|
||||
<h3>API 선택</h3>
|
||||
<button type="button" class="if-modal-close">×</button>
|
||||
</div>
|
||||
<div class="if-modal-body">
|
||||
<div class="if-search-box">
|
||||
<input type="text" id="ifSearchInput" placeholder="API ID 또는 설명 검색" />
|
||||
<button type="button" class="cssbtn" id="ifSearchBtn"><i class="material-icons">search</i> 검색</button>
|
||||
</div>
|
||||
<div class="if-table-wrap">
|
||||
<table class="if-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-id">API ID</th>
|
||||
<th>설명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ifTableBody">
|
||||
<tr><td colspan="2" class="if-table-empty">검색 결과가 없습니다</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="if-pagination">
|
||||
<span class="if-page-info">총 <strong id="ifTotalCount">0</strong>건</span>
|
||||
<div class="if-page-nav">
|
||||
<button type="button" class="if-page-btn" id="ifPrevBtn" disabled><</button>
|
||||
<div class="if-page-num" id="ifPageNum"></div>
|
||||
<button type="button" class="if-page-btn" id="ifNextBtn" disabled>></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="if-modal-footer">
|
||||
<button type="button" class="cssbtn" id="ifCancelBtn">취소</button>
|
||||
<button type="button" class="cssbtn" id="ifSelectBtn"><i class="material-icons">check</i> 선택</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,201 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<%@ page import="java.io.*"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<style>
|
||||
.badge-use { display:inline-block; padding:3px 10px; border-radius:12px; font-size:11px; font-weight:500; }
|
||||
.badge-use.active { background:rgba(0,128,128,0.15); color:rgba(0,128,128,1); border:1px solid rgba(0,128,128,0.3); }
|
||||
.badge-use.inactive { background:#f5f5f5; color:#999; border:1px solid #ddd; }
|
||||
.threshold-display { font-size:12px; color:#666; }
|
||||
.threshold-display .num { color:rgba(0,128,128,1); font-weight:600; }
|
||||
.threshold-display .unit { color:#555; }
|
||||
.threshold-display .code { background:#e8f4f4; color:rgba(0,128,128,0.9); padding:1px 5px; border-radius:3px; font-family:Consolas,monospace; font-size:10px; margin-left:4px; }
|
||||
.datetime-display { font-size:11px; color:#666; }
|
||||
.datetime-display .date { color:#333; }
|
||||
.datetime-display .time { color:rgba(0,128,128,0.9); font-weight:500; }
|
||||
.group-id-display { font-family:Consolas, monospace; font-size:11px; color:#666; }
|
||||
</style>
|
||||
<script language="javascript" >
|
||||
var url ='<c:url value="/onl/admin/inflow/inflowGroupControlMan.json" />';
|
||||
var url_view ='<c:url value="/onl/admin/inflow/inflowGroupControlMan.view" />';
|
||||
|
||||
function useYnBadgeFormatter(cellValue, options, rowObject) {
|
||||
if (cellValue === '1') {
|
||||
return '<span class="badge-use active">사용</span>';
|
||||
} else {
|
||||
return '<span class="badge-use inactive">미사용</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function perSecondFormatter(cellValue, options, rowObject) {
|
||||
if (!cellValue) return '<span style="color:#ccc;">-</span>';
|
||||
return '<span class="threshold-display"><span class="num">' + Number(cellValue).toLocaleString() + '</span>건 / <span class="unit">초</span></span>';
|
||||
}
|
||||
|
||||
function dateTimeFormatter(cellValue, options, rowObject) {
|
||||
if (!cellValue) return '<span style="color:#ccc;">-</span>';
|
||||
// 2025-12-10 16:34:16 형식 파싱
|
||||
var parts = cellValue.split(' ');
|
||||
if (parts.length < 2) return cellValue;
|
||||
var datePart = parts[0].split('-');
|
||||
var timePart = parts[1].substring(0, 5); // HH:mm만
|
||||
if (datePart.length < 3) return cellValue;
|
||||
var year = datePart[0];
|
||||
var month = parseInt(datePart[1], 10);
|
||||
var day = parseInt(datePart[2], 10);
|
||||
var currentYear = new Date().getFullYear().toString();
|
||||
var dateStr = (year === currentYear) ? month + '월 ' + day + '일' : year.substring(2) + '년 ' + month + '월 ' + day + '일';
|
||||
return '<span class="datetime-display"><span class="date">' + dateStr + '</span> <span class="time">' + timePart + '</span></span>';
|
||||
}
|
||||
|
||||
function thresholdFormatter(cellValue, options, rowObject) {
|
||||
var threshold = rowObject.THRESHOLD;
|
||||
var timeUnit = rowObject.THRESHOLDTIMEUNIT;
|
||||
|
||||
if (!threshold || !timeUnit) return '<span style="color:#ccc;">-</span>';
|
||||
|
||||
var unitLabels = {
|
||||
'SEC': '초',
|
||||
'MIN': '분',
|
||||
'HOU': '시간',
|
||||
'DAY': '일',
|
||||
'MON': '월'
|
||||
};
|
||||
var label = unitLabels[timeUnit] || timeUnit;
|
||||
return '<span class="threshold-display"><span class="num">' + Number(threshold).toLocaleString() + '</span>건 / <span class="unit">' + label + '</span><span class="code">' + timeUnit + '</span></span>';
|
||||
}
|
||||
|
||||
function groupIdFormatter(cellValue, options, rowObject) {
|
||||
if (!cellValue) return '<span style="color:#ccc;">-</span>';
|
||||
return '<span class="group-id-display">' + cellValue + '</span>';
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#grid').jqGrid({
|
||||
datatype:"json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData : { cmd : 'LIST', searchGroupName: $('input[name=searchGroupName]').val()},
|
||||
colNames:['그룹 ID',
|
||||
'그룹명',
|
||||
'초당 임계치',
|
||||
'추가 임계치',
|
||||
'사용여부',
|
||||
'수정일시'
|
||||
],
|
||||
colModel:[
|
||||
{ name : 'GROUPID' , align:'left' , width:'200' , sortable:false },
|
||||
{ name : 'GROUPNAME' , align:'left' , width:'280'},
|
||||
{ name : 'THRESHOLDPERSECOND' , align:'center' , width:'100' , formatter:perSecondFormatter },
|
||||
{ name : 'THRESHOLD' , align:'center' , width:'120' , formatter:thresholdFormatter },
|
||||
{ name : 'USEYN' , align:'center' , width:'70' , formatter:useYnBadgeFormatter },
|
||||
{ name : 'MODIFIEDAT' , align:'center' , width:'100' , formatter:dateTimeFormatter }
|
||||
],
|
||||
jsonReader: {
|
||||
repeatitems:false
|
||||
},
|
||||
pager : $('#pager'),
|
||||
page : '${param.page}',
|
||||
rowNum : '${rmsDefaultRowNum}',
|
||||
autoheight: true,
|
||||
height: $("#container").height(),
|
||||
autowidth: true,
|
||||
viewrecords: true,
|
||||
rowList : eval('[${rmsDefaultRowList}]'),
|
||||
ondblClickRow: function(rowId) {
|
||||
var rowData = $(this).getRowData(rowId);
|
||||
var groupId = rowData['GROUPID'];
|
||||
var url2 = url_view;
|
||||
url2 += '?cmd=DETAIL';
|
||||
url2 += '&page='+$(this).getGridParam("page");
|
||||
url2 += '&returnUrl='+getReturnUrl();
|
||||
url2 += '&menuId='+'${param.menuId}';
|
||||
//검색값
|
||||
url2 += '&searchGroupName='+$("input[name=searchGroupName]").val();
|
||||
//key값
|
||||
url2 += '&groupId='+groupId;
|
||||
goNav(url2);
|
||||
|
||||
},
|
||||
gridComplete:function (d){
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
for(var i = 0 ; i< colModel.length; i++){
|
||||
$(this).setColProp(colModel[i].name, {sortable : false});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
|
||||
$("#btn_search").click(function(){
|
||||
$("#grid").setGridParam({ postData: { searchGroupName: $('input[name=searchGroupName]').val()}, page:1 }).trigger("reloadGrid");
|
||||
});
|
||||
$("#btn_new").click(function(){
|
||||
var url2 = url_view;
|
||||
url2 += '?cmd=DETAIL';
|
||||
url2 += '&page='+$("#grid").getGridParam("page");
|
||||
url2 += '&returnUrl='+getReturnUrl();
|
||||
url2 += '&menuId='+'${param.menuId}';
|
||||
//검색값
|
||||
url2 += '&searchGroupName=';
|
||||
|
||||
goNav(url2);
|
||||
});
|
||||
|
||||
$("input[name^=search]").keydown(function(key){
|
||||
if (key.keyCode == 13){
|
||||
$("#btn_search").click();
|
||||
}
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path">
|
||||
<li><a href="#">${rmsMenuPath}</a></li>
|
||||
</ul>
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> <%= localeMessage.getString("button.new") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||
</div>
|
||||
<div class="title">유량제어 그룹 관리<span class="tooltip">유량제어 그룹 관리</span></div>
|
||||
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:180px;">그룹명</th>
|
||||
<td><input type="text" name="searchGroupName" value="${param.searchGroupName}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table id="grid" ></table>
|
||||
<div id="pager"></div>
|
||||
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,932 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<%@ page import="java.io.*"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
|
||||
<!-- 페이지 커스텀 스타일 (fragment) -->
|
||||
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
|
||||
|
||||
<script language="javascript">
|
||||
var url ='<c:url value="/onl/admin/inflow/inflowGroupControlMan.json" />';
|
||||
var url_view ='<c:url value="/onl/admin/inflow/inflowGroupControlMan.view" />';
|
||||
var isDetail = false;
|
||||
var interfaceList = [];
|
||||
|
||||
function isValid() {
|
||||
if ($('input[name=groupName]').val().trim() == "") {
|
||||
alert("그룹명을 입력하여 주십시요.");
|
||||
$('input[name=groupName]').focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateThresholdLabel() {
|
||||
var timeUnit = $('#thresholdTimeUnit').val();
|
||||
var $input = $('#thresholdInput');
|
||||
var $unit = $('#thresholdUnit');
|
||||
|
||||
var unitLabels = {
|
||||
'': '',
|
||||
'SEC': 'req/sec',
|
||||
'MIN': 'req/min',
|
||||
'HOU': 'req/hour',
|
||||
'DAY': 'req/day',
|
||||
'MON': 'req/month'
|
||||
};
|
||||
|
||||
$unit.text(unitLabels[timeUnit] || 'requests');
|
||||
|
||||
if (timeUnit === '') {
|
||||
$input.prop('disabled', true).val('');
|
||||
$unit.css('visibility', 'hidden');
|
||||
} else {
|
||||
$input.prop('disabled', false);
|
||||
$unit.css('visibility', 'visible');
|
||||
}
|
||||
}
|
||||
|
||||
function init(url, key, callback) {
|
||||
// useYn 초기값 설정 (기본: 사용)
|
||||
$('#useYn').val('1');
|
||||
updateThresholdLabel();
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(url, key);
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedTimeUnitText() {
|
||||
return $('#thresholdTimeUnit option:selected').text();
|
||||
}
|
||||
|
||||
function detail(url, key) {
|
||||
if (!isDetail) return;
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
dataType: "json",
|
||||
data: {cmd: 'DETAIL', groupId: key},
|
||||
success: function(json) {
|
||||
var groupId = json.GROUPID || json.groupId;
|
||||
$("input[name=groupId]").val(groupId);
|
||||
$("input[name=groupName]").val(json.GROUPNAME || json.groupName);
|
||||
$("input[name=thresholdPerSecond]").val(json.THRESHOLDPERSECOND || json.thresholdPerSecond);
|
||||
$("input[name=threshold]").val(json.THRESHOLD || json.threshold);
|
||||
$("select[name=thresholdTimeUnit]").val(json.THRESHOLDTIMEUNIT || json.thresholdTimeUnit || '');
|
||||
var useYnVal = json.USEYN || json.useYn || '1';
|
||||
$('#useYn').val(useYnVal);
|
||||
$('#useYnToggle').prop('checked', useYnVal === '1');
|
||||
$('#useYnLabel').text(useYnVal === '1' ? '사용' : '미사용');
|
||||
updateThresholdLabel();
|
||||
|
||||
// 그룹 ID 표시 (수정 화면에서만)
|
||||
if (groupId) {
|
||||
$('#groupIdDisplay').text(groupId);
|
||||
$('#groupIdRow').show();
|
||||
}
|
||||
|
||||
// 수정일시 표시
|
||||
var modifiedAt = json.MODIFIEDAT || json.modifiedAt;
|
||||
if (modifiedAt) {
|
||||
$('#modifiedAtText').text(modifiedAt);
|
||||
$('#modifiedAtInfo').show();
|
||||
}
|
||||
|
||||
interfaceList = [];
|
||||
if (json.interfaceList && json.interfaceList.length > 0) {
|
||||
$.each(json.interfaceList, function(i, item) {
|
||||
interfaceList.push({
|
||||
interfaceId: item.interfaceId || item.INTERFACEID,
|
||||
interfaceDesc: item.interfaceDesc || item.INTERFACEDESC,
|
||||
useYn: item.useYn || item.USEYN || '1'
|
||||
});
|
||||
});
|
||||
}
|
||||
renderInterfaceList();
|
||||
},
|
||||
error: function(e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderInterfaceList() {
|
||||
var container = $('#interfaceListContainer');
|
||||
container.empty();
|
||||
|
||||
var activeCount = interfaceList.filter(function(item) {
|
||||
return item.useYn === '1' || item.useYn === undefined;
|
||||
}).length;
|
||||
$('#interfaceCountNum').text(interfaceList.length);
|
||||
if (interfaceList.length > 0 && activeCount === interfaceList.length) {
|
||||
$('#interfaceActiveLabel').html('<strong>전체 활성</strong>');
|
||||
} else {
|
||||
$('#interfaceActiveLabel').html('활성 <strong id="interfaceActiveNum">' + activeCount + '</strong>개');
|
||||
}
|
||||
|
||||
if (interfaceList.length === 0) {
|
||||
container.html('<tr class="interface-empty-row" id="interfaceEmpty"><td colspan="3" class="interface-empty"><i class="material-icons">inbox</i> 매핑된 API가 없습니다</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
$.each(interfaceList, function(i, item) {
|
||||
var useYn = item.useYn !== undefined ? item.useYn : '1';
|
||||
var checked = useYn === '1' ? 'checked' : '';
|
||||
var row = '<tr data-id="' + item.interfaceId + '">' +
|
||||
'<td class="col-id">' + item.interfaceId + '</td>' +
|
||||
'<td class="col-desc">' + (item.interfaceDesc || '-') + '</td>' +
|
||||
'<td class="col-action">' +
|
||||
'<label class="mini-toggle" title="사용여부">' +
|
||||
'<input type="checkbox" class="interface-use-toggle" data-id="' + item.interfaceId + '" ' + checked + ' />' +
|
||||
'<span class="mini-toggle-slider"></span>' +
|
||||
'</label>' +
|
||||
'<button type="button" class="btn-icon btn-delete-item" data-id="' + item.interfaceId + '" title="삭제">' +
|
||||
'<i class="material-icons">close</i>' +
|
||||
'</button>' +
|
||||
'</td>' +
|
||||
'</tr>';
|
||||
container.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
function getInterfaceListJson() {
|
||||
var gridData = [];
|
||||
$.each(interfaceList, function(i, item) {
|
||||
gridData.push({
|
||||
interfaceId: item.interfaceId,
|
||||
useYn: item.useYn !== undefined ? item.useYn : '1'
|
||||
});
|
||||
});
|
||||
return JSON.stringify(gridData);
|
||||
}
|
||||
|
||||
// 인터페이스 선택 모달 관련
|
||||
var ifModalState = {
|
||||
data: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
selectedItem: null
|
||||
};
|
||||
|
||||
function openIfModal() {
|
||||
ifModalState.page = 1;
|
||||
ifModalState.selectedItem = null;
|
||||
$('#ifSearchInput').val('');
|
||||
$('#ifModal').fadeIn(200, function() {
|
||||
$('#ifSearchInput').focus();
|
||||
});
|
||||
$(document.activeElement).blur();
|
||||
loadIfList();
|
||||
}
|
||||
|
||||
function closeIfModal() {
|
||||
$('#ifModal').fadeOut(200);
|
||||
}
|
||||
|
||||
function loadIfList() {
|
||||
var searchName = $('#ifSearchInput').val();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
dataType: "json",
|
||||
data: {
|
||||
cmd: 'INTERFACE_LIST',
|
||||
searchName: searchName,
|
||||
page: ifModalState.page,
|
||||
rows: ifModalState.pageSize
|
||||
},
|
||||
success: function(json) {
|
||||
ifModalState.data = json.rows || [];
|
||||
ifModalState.totalCount = json.records || 0;
|
||||
renderIfTable();
|
||||
renderIfPagination();
|
||||
},
|
||||
error: function(e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderIfTable() {
|
||||
var tbody = $('#ifTableBody');
|
||||
tbody.empty();
|
||||
|
||||
if (ifModalState.data.length === 0) {
|
||||
tbody.html('<tr><td colspan="2" class="if-table-empty">검색 결과가 없습니다</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
$.each(ifModalState.data, function(i, item) {
|
||||
var interfaceId = item.INTERFACEID || item.interfaceId;
|
||||
var interfaceDesc = item.INTERFACEDESC || item.interfaceDesc || '-';
|
||||
var row = $('<tr data-id="' + interfaceId + '" data-desc="' + interfaceDesc + '">' +
|
||||
'<td class="col-id">' + interfaceId + '</td>' +
|
||||
'<td>' + interfaceDesc + '</td>' +
|
||||
'</tr>');
|
||||
tbody.append(row);
|
||||
});
|
||||
|
||||
// 행 클릭 이벤트
|
||||
tbody.find('tr').on('click', function() {
|
||||
tbody.find('tr').removeClass('selected');
|
||||
$(this).addClass('selected');
|
||||
ifModalState.selectedItem = {
|
||||
interfaceId: $(this).data('id'),
|
||||
interfaceDesc: $(this).data('desc')
|
||||
};
|
||||
});
|
||||
|
||||
// 더블클릭으로 바로 선택
|
||||
tbody.find('tr').on('dblclick', function() {
|
||||
ifModalState.selectedItem = {
|
||||
interfaceId: $(this).data('id'),
|
||||
interfaceDesc: $(this).data('desc')
|
||||
};
|
||||
selectInterface();
|
||||
});
|
||||
}
|
||||
|
||||
function renderIfPagination() {
|
||||
var totalPages = Math.ceil(ifModalState.totalCount / ifModalState.pageSize);
|
||||
var currentPage = ifModalState.page;
|
||||
|
||||
$('#ifTotalCount').text(ifModalState.totalCount);
|
||||
|
||||
// 이전/다음 버튼
|
||||
$('#ifPrevBtn').prop('disabled', currentPage <= 1);
|
||||
$('#ifNextBtn').prop('disabled', currentPage >= totalPages);
|
||||
|
||||
// 페이지 번호
|
||||
var pageNumContainer = $('#ifPageNum');
|
||||
pageNumContainer.empty();
|
||||
|
||||
if (totalPages <= 0) return;
|
||||
|
||||
var startPage = Math.max(1, currentPage - 2);
|
||||
var endPage = Math.min(totalPages, startPage + 4);
|
||||
if (endPage - startPage < 4) {
|
||||
startPage = Math.max(1, endPage - 4);
|
||||
}
|
||||
|
||||
for (var i = startPage; i <= endPage; i++) {
|
||||
var activeClass = (i === currentPage) ? ' active' : '';
|
||||
var btn = $('<button type="button" class="if-page-btn' + activeClass + '">' + i + '</button>');
|
||||
btn.data('page', i);
|
||||
pageNumContainer.append(btn);
|
||||
}
|
||||
|
||||
// 페이지 번호 클릭
|
||||
pageNumContainer.find('.if-page-btn').on('click', function() {
|
||||
ifModalState.page = $(this).data('page');
|
||||
ifModalState.selectedItem = null;
|
||||
loadIfList();
|
||||
});
|
||||
}
|
||||
|
||||
// 탭 전환 기능
|
||||
function switchTab(tabName) {
|
||||
$('.detail-tab').removeClass('active');
|
||||
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
|
||||
$('.tab-content').removeClass('active');
|
||||
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
|
||||
|
||||
if (tabName === 'status' && isDetail) {
|
||||
loadBucketStatus();
|
||||
}
|
||||
}
|
||||
|
||||
// 버킷 현황 조회
|
||||
function loadBucketStatus() {
|
||||
var groupId = $('input[name=groupId]').val();
|
||||
if (!groupId) return;
|
||||
|
||||
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: { cmd: 'LIST_BUCKET_STATUS', groupId: groupId },
|
||||
dataType: 'json',
|
||||
success: function(result) {
|
||||
renderBucketStatus(result.servers || []);
|
||||
$('#lastRefreshTime').text(formatDateTime(new Date()));
|
||||
},
|
||||
error: function(e) {
|
||||
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatDateTime(date) {
|
||||
var h = date.getHours();
|
||||
var m = date.getMinutes();
|
||||
var s = date.getSeconds();
|
||||
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
|
||||
}
|
||||
|
||||
function renderBucketStatus(servers) {
|
||||
var container = $('#bucketServerList');
|
||||
container.empty();
|
||||
|
||||
if (servers.length === 0) {
|
||||
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
|
||||
$('#bucketSummary').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// 전체 합산 계산
|
||||
var onlineCount = 0;
|
||||
var perSecondCapacity = 0;
|
||||
var thresholdCapacity = 0;
|
||||
var thresholdTimeUnit = '';
|
||||
|
||||
$.each(servers, function(i, server) {
|
||||
if (server.status === 'online' && server.data && server.data.buckets) {
|
||||
onlineCount++;
|
||||
$.each(server.data.buckets, function(j, bucket) {
|
||||
if (bucket.type === 'perSecond') {
|
||||
perSecondCapacity += bucket.capacity || 0;
|
||||
} else if (bucket.type === 'threshold') {
|
||||
thresholdCapacity += bucket.capacity || 0;
|
||||
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 요약 영역 표시
|
||||
if (onlineCount > 0) {
|
||||
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
|
||||
|
||||
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
|
||||
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
|
||||
|
||||
if (configPerSecond > 0) {
|
||||
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
|
||||
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
|
||||
$('#summaryPerSecondWrap').show();
|
||||
} else {
|
||||
$('#summaryPerSecondWrap').hide();
|
||||
}
|
||||
|
||||
if (configThreshold > 0) {
|
||||
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
|
||||
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
|
||||
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
|
||||
$('#summaryThresholdWrap').show();
|
||||
} else {
|
||||
$('#summaryThresholdWrap').hide();
|
||||
}
|
||||
|
||||
// 활성 API 정보 표시
|
||||
var activeApis = interfaceList.filter(function(item) {
|
||||
return item.useYn === '1' || item.useYn === undefined;
|
||||
});
|
||||
$('#summaryActiveApi').html(activeApis.length + '<span class="unit">개</span>');
|
||||
if (activeApis.length > 0) {
|
||||
var apiIds = activeApis.map(function(item) { return item.interfaceId; });
|
||||
var displayText = apiIds.length <= 3 ? apiIds.join(', ') : apiIds.slice(0, 3).join(', ') + ' 외 ' + (apiIds.length - 3) + '개';
|
||||
$('#summaryActiveApiList').text(displayText);
|
||||
} else {
|
||||
$('#summaryActiveApiList').text('');
|
||||
}
|
||||
|
||||
$('#bucketSummary').show();
|
||||
} else {
|
||||
$('#bucketSummary').hide();
|
||||
}
|
||||
|
||||
$.each(servers, function(i, server) {
|
||||
var statusClass = server.status || 'offline';
|
||||
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
|
||||
|
||||
var card = '<div class="bucket-server-card">' +
|
||||
'<div class="bucket-server-header">' +
|
||||
'<div>' +
|
||||
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
|
||||
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
|
||||
'</div>' +
|
||||
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
|
||||
'</div>';
|
||||
container.append(card);
|
||||
});
|
||||
}
|
||||
|
||||
function renderBucketBody(server) {
|
||||
if (server.status === 'offline') {
|
||||
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
|
||||
}
|
||||
if (server.status === 'no_data') {
|
||||
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '그룹이 로드되지 않음') + '</div>';
|
||||
}
|
||||
|
||||
var data = server.data;
|
||||
if (!data || !data.buckets || data.buckets.length === 0) {
|
||||
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
|
||||
}
|
||||
|
||||
var html = '<div class="bucket-info-grid">';
|
||||
$.each(data.buckets, function(j, bucket) {
|
||||
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
|
||||
var timeUnitText = getTimeUnitText(bucket.timeUnit);
|
||||
var usagePercent = bucket.usagePercent || 0;
|
||||
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
|
||||
|
||||
html += '<div class="bucket-card">' +
|
||||
'<div class="bucket-card-header">' +
|
||||
'<span class="bucket-type">' + typeLabel + '</span>' +
|
||||
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="bucket-progress-wrap">' +
|
||||
'<div class="bucket-progress-bar">' +
|
||||
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="bucket-stats">' +
|
||||
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
|
||||
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function getTimeUnitText(timeUnit) {
|
||||
var units = { SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월' };
|
||||
return units[timeUnit] || timeUnit || '-';
|
||||
}
|
||||
|
||||
function formatNumber(num) {
|
||||
if (num === undefined || num === null) return '-';
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
||||
function selectInterface() {
|
||||
if (!ifModalState.selectedItem) {
|
||||
alert('API를 선택하여 주십시오.');
|
||||
return;
|
||||
}
|
||||
|
||||
var newId = String(ifModalState.selectedItem.interfaceId);
|
||||
|
||||
// 현재 화면 내 중복 체크
|
||||
var exists = interfaceList.some(function(item) {
|
||||
return String(item.interfaceId) === newId;
|
||||
});
|
||||
|
||||
if (exists) {
|
||||
alert('이미 추가된 API입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 서버에 다른 그룹 등록 여부 체크
|
||||
var groupId = $('input[name=groupId]').val();
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: { cmd: 'LIST_CHECK_DUPLICATE', interfaceId: newId, groupId: groupId },
|
||||
dataType: 'json',
|
||||
async: false,
|
||||
success: function(result) {
|
||||
if (result.duplicate) {
|
||||
showAlert('<strong>' + newId + '</strong> API는<br/><strong>[' + result.groupName + ']</strong> 그룹에 이미 등록되어 있습니다.', {
|
||||
type: 'warning',
|
||||
title: '중복 등록 불가'
|
||||
});
|
||||
} else {
|
||||
interfaceList.push(ifModalState.selectedItem);
|
||||
renderInterfaceList();
|
||||
closeIfModal();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showAlert('중복 체크 중 오류가 발생했습니다.', { type: 'error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
var key = "${param.groupId}";
|
||||
if (key != "" && key != "null") {
|
||||
isDetail = true;
|
||||
}
|
||||
|
||||
init(url, key, detail);
|
||||
|
||||
$('#thresholdTimeUnit').on('change', function() {
|
||||
updateThresholdLabel();
|
||||
});
|
||||
|
||||
// 사용여부 토글 변경 시 hidden 값 및 라벨 업데이트
|
||||
$('#useYnToggle').on('change', function() {
|
||||
var isChecked = $(this).is(':checked');
|
||||
$('#useYn').val(isChecked ? '1' : '0');
|
||||
$('#useYnLabel').text(isChecked ? '사용' : '미사용');
|
||||
});
|
||||
|
||||
// 인터페이스 삭제 버튼 (동적 요소이므로 위임)
|
||||
$(document).on('click', '.btn-delete-item', function() {
|
||||
var id = $(this).data('id');
|
||||
interfaceList = interfaceList.filter(function(item) {
|
||||
return item.interfaceId !== id;
|
||||
});
|
||||
renderInterfaceList();
|
||||
});
|
||||
|
||||
// 인터페이스 사용여부 토글 (동적 요소이므로 위임)
|
||||
$(document).on('change', '.interface-use-toggle', function() {
|
||||
var id = $(this).data('id');
|
||||
var useYn = $(this).is(':checked') ? '1' : '0';
|
||||
$.each(interfaceList, function(i, item) {
|
||||
if (item.interfaceId === id) {
|
||||
item.useYn = useYn;
|
||||
}
|
||||
});
|
||||
// 활성 건수 업데이트
|
||||
var activeCount = interfaceList.filter(function(item) {
|
||||
return item.useYn === '1' || item.useYn === undefined;
|
||||
}).length;
|
||||
if (interfaceList.length > 0 && activeCount === interfaceList.length) {
|
||||
$('#interfaceActiveLabel').html('<strong>전체 활성</strong>');
|
||||
} else {
|
||||
$('#interfaceActiveLabel').html('활성 <strong id="interfaceActiveNum">' + activeCount + '</strong>개');
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_modify").click(function() {
|
||||
$(this).blur();
|
||||
if (!isValid()) return;
|
||||
|
||||
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 그룹을 등록하시겠습니까?';
|
||||
showConfirm(confirmMsg, {
|
||||
title: '저장 확인',
|
||||
confirmText: '저장',
|
||||
onConfirm: function() {
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
if (isDetail) {
|
||||
postData.push({name: "cmd", value: "UPDATE"});
|
||||
} else {
|
||||
postData.push({name: "cmd", value: "INSERT"});
|
||||
}
|
||||
|
||||
postData.push({name: "interfaceListJson", value: getInterfaceListJson()});
|
||||
|
||||
var isInsert = !isDetail;
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: postData,
|
||||
success: function(args) {
|
||||
if (isInsert) {
|
||||
// 신규 등록 시 목록으로 이동
|
||||
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
|
||||
type: 'success',
|
||||
title: '저장 완료',
|
||||
onClose: function() {
|
||||
goNav(returnUrl);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 수정 시 현재 화면 유지
|
||||
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
|
||||
type: 'success',
|
||||
title: '저장 완료'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(e) {
|
||||
showAlert(e.responseText, { type: 'error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#btn_delete").click(function() {
|
||||
$(this).blur();
|
||||
var groupName = $('input[name=groupName]').val();
|
||||
showConfirm('<strong>[' + groupName + ']</strong> 그룹을 삭제하시겠습니까?<br/><span style="color:#999;font-size:11px;">매핑된 API 정보도 함께 삭제됩니다.</span>', {
|
||||
type: 'error',
|
||||
title: '삭제 확인',
|
||||
confirmText: '삭제',
|
||||
onConfirm: function() {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {cmd: 'DELETE', groupId: $('input[name=groupId]').val()},
|
||||
success: function(args) {
|
||||
showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
|
||||
type: 'success',
|
||||
title: '삭제 완료',
|
||||
onClose: function() {
|
||||
goNav(returnUrl);
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function(e) {
|
||||
showAlert(e.responseText, { type: 'error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#btn_previous").click(function() {
|
||||
goNav(returnUrl);
|
||||
});
|
||||
|
||||
// 인터페이스 추가 버튼 → 모달 열기
|
||||
$("#btn_popup_interface").click(function() {
|
||||
openIfModal();
|
||||
});
|
||||
|
||||
// 인터페이스 선택 모달 이벤트
|
||||
$(".if-modal-close, #ifCancelBtn").click(function() {
|
||||
closeIfModal();
|
||||
});
|
||||
$("#ifModal").click(function(e) {
|
||||
if (e.target === this) closeIfModal();
|
||||
});
|
||||
$("#ifSearchBtn").click(function() {
|
||||
ifModalState.page = 1;
|
||||
ifModalState.selectedItem = null;
|
||||
loadIfList();
|
||||
});
|
||||
$("#ifSearchInput").keydown(function(e) {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
$("#ifSearchBtn").click();
|
||||
}
|
||||
});
|
||||
$("#ifSelectBtn").click(function() {
|
||||
selectInterface();
|
||||
});
|
||||
$("#ifPrevBtn").click(function() {
|
||||
if (ifModalState.page > 1) {
|
||||
ifModalState.page--;
|
||||
ifModalState.selectedItem = null;
|
||||
loadIfList();
|
||||
}
|
||||
});
|
||||
$("#ifNextBtn").click(function() {
|
||||
var totalPages = Math.ceil(ifModalState.totalCount / ifModalState.pageSize);
|
||||
if (ifModalState.page < totalPages) {
|
||||
ifModalState.page++;
|
||||
ifModalState.selectedItem = null;
|
||||
loadIfList();
|
||||
}
|
||||
});
|
||||
|
||||
buttonControl(isDetail);
|
||||
titleControl(isDetail);
|
||||
|
||||
// 탭 전환 이벤트
|
||||
$('.detail-tab').click(function() {
|
||||
var tabName = $(this).data('tab');
|
||||
switchTab(tabName);
|
||||
});
|
||||
|
||||
// 현황 탭 표시 (상세 조회 시에만)
|
||||
if (isDetail) {
|
||||
$('#tabStatus').show();
|
||||
}
|
||||
|
||||
// 버킷 새로고침 버튼
|
||||
$('#btn_refresh_bucket').click(function() {
|
||||
loadBucketStatus();
|
||||
});
|
||||
|
||||
// 도움말 모달
|
||||
$("#btn_help").click(function() {
|
||||
$(document.activeElement).blur();
|
||||
$("#helpModal").fadeIn(200);
|
||||
});
|
||||
$(".help-modal-close, .help-modal-overlay").click(function(e) {
|
||||
if (e.target === this) {
|
||||
$("#helpModal").fadeOut(200);
|
||||
}
|
||||
});
|
||||
$(document).keydown(function(e) {
|
||||
if (e.keyCode === 27) {
|
||||
$("#helpModal").fadeOut(200);
|
||||
$("#ifModal").fadeOut(200);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path">
|
||||
<li><a href="#">${rmsMenuPath}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_help"><i class="material-icons">help_outline</i> 도움말</button>
|
||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_previous"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||
</div>
|
||||
<div class="title-wrap">
|
||||
<div class="title">유량제어 그룹 관리<span class="tooltip">유량제어 그룹 관리</span></div>
|
||||
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
|
||||
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 탭 네비게이션 -->
|
||||
<div class="detail-tabs" id="detailTabs">
|
||||
<button type="button" class="detail-tab active" data-tab="config">
|
||||
<i class="material-icons">settings</i> 설정
|
||||
</button>
|
||||
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
|
||||
<i class="material-icons">analytics</i> 현황
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 설정 탭 -->
|
||||
<div class="tab-content active" id="tabConfigContent">
|
||||
<form id="ajaxForm">
|
||||
<input type="hidden" name="groupId" />
|
||||
<input type="hidden" name="useYn" id="useYn" value="1" />
|
||||
|
||||
<!-- 기본 정보 -->
|
||||
<table class="form-table">
|
||||
<colgroup>
|
||||
<col style="width:120px" />
|
||||
<col />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr id="groupIdRow" style="display:none;">
|
||||
<th>그룹 ID</th>
|
||||
<td><span id="groupIdDisplay" style="font-family:Consolas, monospace; font-size:12px; color:#666;"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>그룹명 <span style="color:red">*</span></th>
|
||||
<td><input type="text" name="groupName" style="width:300px" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>사용여부</th>
|
||||
<td>
|
||||
<div class="toggle-wrap">
|
||||
<label class="ios-toggle">
|
||||
<input type="checkbox" id="useYnToggle" checked />
|
||||
<span class="ios-toggle-slider"></span>
|
||||
</label>
|
||||
<span class="toggle-label" id="useYnLabel">사용</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 유량제어 설정 -->
|
||||
<span class="section-label">유량제어 설정</span>
|
||||
<table class="form-table">
|
||||
<colgroup>
|
||||
<col style="width:120px" />
|
||||
<col />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
초당 임계치
|
||||
<span class="hint-icon"><i class="material-icons">help_outline</i>
|
||||
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
|
||||
</span>
|
||||
</th>
|
||||
<td>
|
||||
<div class="threshold-group">
|
||||
<input type="number" name="thresholdPerSecond" placeholder="0" />
|
||||
<span class="unit">req/sec</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
추가 임계치
|
||||
<span class="hint-icon"><i class="material-icons">help_outline</i>
|
||||
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
|
||||
</span>
|
||||
</th>
|
||||
<td>
|
||||
<div class="threshold-group">
|
||||
<select name="thresholdTimeUnit" id="thresholdTimeUnit" class="styled-select">
|
||||
<option value="">미사용</option>
|
||||
<option value="MIN">분 (MIN)</option>
|
||||
<option value="HOU">시간 (HOU)</option>
|
||||
<option value="DAY">일 (DAY)</option>
|
||||
<option value="MON">월 (MON)</option>
|
||||
</select>
|
||||
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
|
||||
<span class="unit" id="thresholdUnit">requests</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- API 매핑 -->
|
||||
<span class="section-label">API 매핑</span>
|
||||
<div class="interface-header">
|
||||
<span class="interface-count">매핑된 API <strong id="interfaceCountNum">0</strong>개 (<span id="interfaceActiveLabel">활성 <strong id="interfaceActiveNum">0</strong>개</span>)</span>
|
||||
<button type="button" class="cssbtn" id="btn_popup_interface"><i class="material-icons">add</i> 추가</button>
|
||||
</div>
|
||||
<div class="interface-list-wrap">
|
||||
<table class="interface-list-table">
|
||||
<colgroup>
|
||||
<col class="col-id" />
|
||||
<col class="col-desc" />
|
||||
<col class="col-action" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>API ID</th>
|
||||
<th>설명</th>
|
||||
<th>관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="interfaceListContainer">
|
||||
<tr class="interface-empty-row" id="interfaceEmpty">
|
||||
<td colspan="3" class="interface-empty"><i class="material-icons">inbox</i> 매핑된 API가 없습니다</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- /tabConfigContent -->
|
||||
|
||||
<!-- 현황 탭 -->
|
||||
<div class="tab-content" id="tabStatusContent">
|
||||
<div class="bucket-status-wrap">
|
||||
<div class="bucket-refresh-bar">
|
||||
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
|
||||
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
|
||||
</div>
|
||||
|
||||
<!-- 전체 합산 요약 -->
|
||||
<div class="bucket-summary" id="bucketSummary" style="display:none;">
|
||||
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
|
||||
<div class="bucket-summary-grid">
|
||||
<div class="bucket-summary-item">
|
||||
<span class="bucket-summary-label">활성 인스턴스</span>
|
||||
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
|
||||
</div>
|
||||
<div class="bucket-summary-item" id="summaryPerSecondWrap">
|
||||
<span class="bucket-summary-label">초당 임계치 (전체)</span>
|
||||
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
|
||||
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
|
||||
</div>
|
||||
<div class="bucket-summary-item" id="summaryThresholdWrap">
|
||||
<span class="bucket-summary-label">추가 임계치 (전체)</span>
|
||||
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
|
||||
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
|
||||
</div>
|
||||
<div class="bucket-summary-item">
|
||||
<span class="bucket-summary-label">적용 API</span>
|
||||
<span class="bucket-summary-value" id="summaryActiveApi">0<span class="unit">개</span></span>
|
||||
<span class="bucket-summary-sub" id="summaryActiveApiList"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bucket-server-list" id="bucketServerList">
|
||||
<div class="bucket-empty-msg">
|
||||
<i class="material-icons">hourglass_empty</i>
|
||||
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /tabStatusContent -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 인터페이스 선택 모달 (fragment) -->
|
||||
<jsp:include page="fragments/inflowGroupInterfaceModal.jsp"/>
|
||||
|
||||
<!-- 도움말 모달 (fragment) -->
|
||||
<jsp:include page="fragments/inflowGroupHelpModal.jsp"/>
|
||||
</body>
|
||||
</html>
|
||||
+9
-2
@@ -41,10 +41,17 @@ checkstyle {
|
||||
toolVersion = '8.45.1'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDirs generatedJavaDir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs = ['-parameters']
|
||||
sourceSets.main.java { srcDir generatedJavaDir }
|
||||
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
|
||||
|
||||
aptOptions {
|
||||
@@ -70,7 +77,7 @@ war {
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs = ['-parameters']
|
||||
options.compilerArgs += ['-parameters', "-Xlint:unchecked", "-Xlint:deprecation"]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# ObpGwMetricHourJob
|
||||
- Job명 : ObpGwMetricHourJob
|
||||
- 설명 : GW 시간당 거래 지표 - OBP 과금기초데이터
|
||||
- com.eactive.eai.rms.onl.common.service.ObpGwMetricHourJob
|
||||
- aggregation.hour.range : 집계 범위(최대 24로 제한)
|
||||
@@ -668,7 +668,4 @@ insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Mon
|
||||
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
|
||||
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
|
||||
commit;
|
||||
```
|
||||
## 라이선스
|
||||
|
||||
© 광주은행 (Kwangju Bank)
|
||||
```
|
||||
+11
-5
@@ -2,6 +2,7 @@ package com.eactive.eai.rms.data.entity.man.monitoringProperty.service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -14,13 +15,14 @@ import com.querydsl.core.types.dsl.BooleanExpression;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class MonitoringPropertyService
|
||||
extends AbstractDataService<MonitoringProperty, MonitoringPropertyId, MonitoringPropertyRepository> {
|
||||
|
||||
public String findPrpty2ValById(String prptyGroupName, String key) {
|
||||
MonitoringPropertyId id = new MonitoringPropertyId();
|
||||
id.setPrptyGroupName(prptyGroupName);
|
||||
id.setPrptyGroupName(key);
|
||||
id.setPrptyName(key);
|
||||
|
||||
Optional<MonitoringProperty> property = repository.findById(id);
|
||||
return property.map(MonitoringProperty::getPrpty2Val).orElse(null);
|
||||
@@ -41,12 +43,16 @@ public class MonitoringPropertyService
|
||||
id.setPrptyName(prptyName);
|
||||
|
||||
Optional<MonitoringProperty> property = repository.findById(id);
|
||||
if (property.isPresent() && !property.get().getPrpty2Val().isEmpty()) {
|
||||
return property.get().getPrpty2Val();
|
||||
} else {
|
||||
return defaultValue;
|
||||
if (property.isPresent()) {
|
||||
String value = property.get().getPrpty2Val();
|
||||
if (value != null && !value.isEmpty()) {
|
||||
log.debug("getPropertyValue - {} : {}", prptyName, value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("getPropertyValue - {} : {} (defaultValue)", prptyName, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public Iterable<MonitoringProperty> findAllByIdPrptyName(String prptyName) {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.apim.obp;
|
||||
|
||||
import com.eactive.eai.rms.common.base.SqlMapClientTemplateDao;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* ObpGwMetric DAO - iBATIS를 사용한 로그 테이블 집계
|
||||
*/
|
||||
@Repository
|
||||
public class ObpGwMetricDAO extends SqlMapClientTemplateDao {
|
||||
|
||||
/**
|
||||
* EAIBZWKDSTCD 코드 목록 조회
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> selectAllBzwkCodes(String schemaId) {
|
||||
HashMap<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("schemaId", schemaId);
|
||||
return this.template.queryForList("ObpGwMetric.selectAllBzwkCodes", paramMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* EAIBZWKDSTCD별 시간대 로그 집계
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<ObpGwMetricVO> selectHourlyMetricsByBzwkCode(HashMap<String, Object> paramMap) {
|
||||
return this.template.queryForList("ObpGwMetric.selectHourlyMetricsByBzwkCode", paramMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* API 이름 조회 (TSEAIHE01)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, String> selectApiNames(String schemaId, Set<String> apiIds) {
|
||||
if (apiIds == null || apiIds.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
HashMap<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("schemaId", schemaId);
|
||||
paramMap.put("apiIds", new ArrayList<>(apiIds)); // Set → List 변환 (iBATIS iterate 호환)
|
||||
|
||||
List<Map<String, String>> resultList = this.template.queryForList("ObpGwMetric.selectApiNames", paramMap);
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (Map<String, String> m : resultList) {
|
||||
String apiId = m.get("API_ID");
|
||||
String apiName = m.get("API_NAME");
|
||||
if (apiId != null && apiName != null) {
|
||||
result.put(apiId, apiName);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* URI 조회 (TSEAIHS04)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, String> selectUris(String schemaId, Set<String> apiIds) {
|
||||
if (apiIds == null || apiIds.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
HashMap<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("schemaId", schemaId);
|
||||
paramMap.put("apiIds", new ArrayList<>(apiIds)); // Set → List 변환 (iBATIS iterate 호환)
|
||||
|
||||
List<Map<String, String>> resultList = this.template.queryForList("ObpGwMetric.selectUris", paramMap);
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (Map<String, String> m : resultList) {
|
||||
String apiId = m.get("API_ID");
|
||||
String uri = m.get("URI");
|
||||
if (apiId != null && uri != null) {
|
||||
result.put(apiId, uri);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AppId 조회 (ptl_app_request) - org_id로 조회
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, String> selectAppIdsByOrgIds(Set<String> orgIds) {
|
||||
if (orgIds == null || orgIds.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
HashMap<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("orgIds", new ArrayList<>(orgIds)); // Set → List 변환 (iBATIS iterate 호환)
|
||||
|
||||
List<Map<String, String>> resultList = this.template.queryForList("ObpGwMetric.selectAppIdsByOrgIds", paramMap);
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (Map<String, String> m : resultList) {
|
||||
String orgId = m.get("ORG_ID");
|
||||
String appId = m.get("APP_ID");
|
||||
if (orgId != null && appId != null) {
|
||||
result.put(orgId, appId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.apim.obp;
|
||||
|
||||
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
import com.eactive.eai.rms.data.EMSDataSource;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* ObpGwMetric Repository (eapim-admin 위치)
|
||||
* API Gateway 시간별 거래량 지표 조회/저장을 위한 Spring Data JPA Repository
|
||||
*/
|
||||
@EMSDataSource
|
||||
public interface ObpGwMetricRepository extends BaseRepository<ObpGwMetric, String> {
|
||||
|
||||
/**
|
||||
* UPSERT 판단을 위한 기존 데이터 조회
|
||||
* 동일한 timeslice + apiId + clientId + hostname + orgId 조합이 존재하는지 확인
|
||||
*/
|
||||
Optional<ObpGwMetric> findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId(
|
||||
LocalDateTime timeslice,
|
||||
String apiId,
|
||||
String clientId,
|
||||
String hostname,
|
||||
String orgId
|
||||
);
|
||||
|
||||
/**
|
||||
* 특정 시간대의 모든 GwMetric 데이터 조회
|
||||
*/
|
||||
List<ObpGwMetric> findByTimeslice(LocalDateTime timeslice);
|
||||
|
||||
/**
|
||||
* 특정 시간 범위의 GwMetric 데이터 조회
|
||||
*/
|
||||
List<ObpGwMetric> findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.apim.obp;
|
||||
|
||||
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* ObpGwMetric Service - 엔티티 CRUD 서비스
|
||||
*/
|
||||
@Service
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
public class ObpGwMetricService {
|
||||
|
||||
private final ObpGwMetricRepository repository;
|
||||
|
||||
public ObpGwMetricService(ObpGwMetricRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* UPSERT 판단을 위한 기존 데이터 조회
|
||||
*/
|
||||
public Optional<ObpGwMetric> findByUniqueKey(LocalDateTime timeslice, String apiId, String clientId, String hostname, String orgId) {
|
||||
return repository.findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId(
|
||||
timeslice, apiId, clientId, hostname, orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 시간대의 모든 GwMetric 데이터 조회
|
||||
*/
|
||||
public List<ObpGwMetric> findByTimeslice(LocalDateTime timeslice) {
|
||||
return repository.findByTimeslice(timeslice);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 시간 범위의 GwMetric 데이터 조회
|
||||
*/
|
||||
public List<ObpGwMetric> findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return repository.findByTimesliceBetween(startTime, endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 엔티티 저장 (INSERT 또는 UPDATE)
|
||||
*/
|
||||
public ObpGwMetric save(ObpGwMetric entity) {
|
||||
return repository.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 엔티티 목록 저장
|
||||
*/
|
||||
public List<ObpGwMetric> saveAll(List<ObpGwMetric> entities) {
|
||||
return repository.saveAll(entities);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.apim.obp;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* ObpGwMetric VO - 로그 집계 결과 매핑용
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ObpGwMetricVO {
|
||||
|
||||
private static final DateTimeFormatter TIMESLICE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHH0000000");
|
||||
|
||||
/** API ID (EAISVCNAME) */
|
||||
private String apiId;
|
||||
|
||||
/** 클라이언트 ID (CLIENTID) */
|
||||
private String clientId;
|
||||
|
||||
/** 호스트 이름 (EAISEVRINSTNCNAME) */
|
||||
private String hostname;
|
||||
|
||||
/** 기관 ID (ORGID) */
|
||||
private String orgId;
|
||||
|
||||
/** 타임슬라이스 문자열 (yyyyMMddHH0000000) */
|
||||
private String timeslice;
|
||||
|
||||
/** 시도 횟수 (LOGPRCSSSERNO='100' 건수) */
|
||||
private Long attemptedCount;
|
||||
|
||||
/** 완료 횟수 (LOGPRCSSSERNO='400' AND 성공 조건 건수) */
|
||||
private Long completedCount;
|
||||
|
||||
/**
|
||||
* timeslice 문자열을 LocalDateTime으로 변환
|
||||
*/
|
||||
public LocalDateTime getTimesliceAsLocalDateTime() {
|
||||
if (timeslice == null || timeslice.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
// "yyyyMMddHH0000000" 형식에서 앞 10자리(yyyyMMddHH)만 사용
|
||||
String dateTimeStr = timeslice.substring(0, 10) + "0000";
|
||||
return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 집계 키 생성 (apiId + clientId + hostname + orgId)
|
||||
*/
|
||||
public String getAggregationKey() {
|
||||
return String.join("|",
|
||||
nullSafe(apiId),
|
||||
nullSafe(clientId),
|
||||
nullSafe(hostname),
|
||||
nullSafe(orgId)
|
||||
);
|
||||
}
|
||||
|
||||
private String nullSafe(String value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.inflow;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroupMapping;
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
|
||||
public interface InflowControlGroupMappingRepository extends BaseRepository<InflowControlGroupMapping, String> {
|
||||
|
||||
List<InflowControlGroupMapping> findByGroupId(String groupId);
|
||||
|
||||
@Modifying
|
||||
@Query("DELETE FROM InflowControlGroupMapping m WHERE m.groupId = :groupId")
|
||||
void deleteByGroupId(@Param("groupId") String groupId);
|
||||
|
||||
/**
|
||||
* 인터페이스 ID로 매핑 조회 (중복 체크용)
|
||||
*/
|
||||
Optional<InflowControlGroupMapping> findByInterfaceId(String interfaceId);
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.eactive.eai.rms.data.entity.onl.inflow;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroup;
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
|
||||
public interface InflowControlGroupRepository extends BaseRepository<InflowControlGroup, String> {
|
||||
|
||||
}
|
||||
+1
-1
@@ -28,7 +28,7 @@ public abstract class CredentialUIMapper implements GenericMapper<CredentialUI,
|
||||
|
||||
@InheritInverseConfiguration
|
||||
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
@Mapping(target = "apiList", expression = "java(apiSpecUIMapper.mapList(credentialUI.getApiList()))")
|
||||
@Mapping(target = "apiList", source = "apiList")
|
||||
public abstract void updateToEntity(CredentialUI credentialUI, @MappingTarget Credential credential);
|
||||
|
||||
@Mapping(source = "clientid", target = "clientId")
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.eactive.eai.rms.onl.apim.obp;
|
||||
|
||||
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ObpGwMetric REST Controller
|
||||
* API Gateway 시간별 거래량 지표 조회 API
|
||||
*
|
||||
* <p>세션 체크를 우회하기 위해 InterceptorSkipController 인터페이스 구현</p>
|
||||
*/
|
||||
@Controller
|
||||
public class ObpGwMetricController implements InterceptorSkipController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ObpGwMetricController.class);
|
||||
|
||||
private static final DateTimeFormatter PATH_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss");
|
||||
|
||||
private final ObpGwMetricManService obpGwMetricManService;
|
||||
|
||||
public ObpGwMetricController(ObpGwMetricManService obpGwMetricManService) {
|
||||
this.obpGwMetricManService = obpGwMetricManService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 시간대의 GwMetric 데이터 조회
|
||||
*
|
||||
* @param datetime 조회 시간대 (형식: yyyy-MM-dd_HH:mm:ss)
|
||||
* @return JSON Array
|
||||
*/
|
||||
@RequestMapping(
|
||||
value = "/kjb/gw-metrics/{datetime}",
|
||||
produces = "application/json;charset=utf-8"
|
||||
)
|
||||
@ResponseBody
|
||||
public String getGwMetrics(@PathVariable("datetime") String datetime) {
|
||||
log.debug("GwMetric 조회 요청: datetime={}", datetime);
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// Path 파라미터 파싱
|
||||
LocalDateTime targetTime = LocalDateTime.parse(datetime, PATH_FORMATTER);
|
||||
LocalDateTime timeslice = targetTime.withMinute(0).withSecond(0).withNano(0);
|
||||
|
||||
// 조회
|
||||
List<ObpGwMetricDTO> metrics = obpGwMetricManService.findByTimeslice(timeslice);
|
||||
|
||||
// JSON 변환 (GSON)
|
||||
Gson gson = new GsonBuilder()
|
||||
.serializeNulls()
|
||||
.create();
|
||||
|
||||
String json = gson.toJson(metrics);
|
||||
|
||||
// 응답 크기 계산 (UTF-8 기준)
|
||||
int jsonBytes = json.getBytes(StandardCharsets.UTF_8).length;
|
||||
String jsonSize = formatByteSize(jsonBytes);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
log.info("GwMetric 조회 완료: datetime={}, count={}, size={}, elapsed={}ms",
|
||||
datetime, metrics.size(), jsonSize, elapsed);
|
||||
|
||||
return json;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("GwMetric 조회 실패: datetime={}", datetime, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 바이트 크기를 읽기 쉬운 형식으로 변환
|
||||
* 예: 1024 -> "1.0 KB", 1048576 -> "1.0 MB"
|
||||
*/
|
||||
private String formatByteSize(long bytes) {
|
||||
if (bytes < 1024) {
|
||||
return bytes + " B";
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return String.format("%.1f KB", bytes / 1024.0);
|
||||
} else {
|
||||
return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.eactive.eai.rms.onl.apim.obp;
|
||||
|
||||
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* ObpGwMetric JSON 응답 DTO
|
||||
* Entity 주석에 명시된 JSON Key 사용
|
||||
*/
|
||||
@Data
|
||||
public class ObpGwMetricDTO {
|
||||
|
||||
private static final DateTimeFormatter ISO8601_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'+0000'");
|
||||
|
||||
@SerializedName("appKey")
|
||||
private String clientId;
|
||||
|
||||
@SerializedName("clusterHostname")
|
||||
private String hostname;
|
||||
|
||||
@SerializedName("timeslice")
|
||||
private String timeslice;
|
||||
|
||||
@SerializedName("apiName")
|
||||
private String apiName;
|
||||
|
||||
@SerializedName("apiRoutingUri")
|
||||
private String uri;
|
||||
|
||||
@SerializedName("attemptedCount")
|
||||
private Long attemptedCount;
|
||||
|
||||
@SerializedName("completedCount")
|
||||
private Long completedCount;
|
||||
|
||||
// EAPIM ID
|
||||
@SerializedName("apiIdByEapim")
|
||||
private String apiId;
|
||||
|
||||
@SerializedName("appIdByEapim")
|
||||
private String appId;
|
||||
|
||||
@SerializedName("orgIdByEapim")
|
||||
private String orgId;
|
||||
|
||||
// AS-IS 호환용 필드
|
||||
@SerializedName("appId")
|
||||
private String appIdByCaPortal;
|
||||
|
||||
@SerializedName("portalOrgId")
|
||||
private String orgIdByCaPortal;
|
||||
|
||||
@SerializedName("portalApiId")
|
||||
private String apiIdByCaPortal;
|
||||
|
||||
@SerializedName("partnerCode")
|
||||
private String partnerCode;
|
||||
|
||||
@SerializedName("apiId")
|
||||
private String apiIdByCa;
|
||||
|
||||
/**
|
||||
* Entity → DTO 변환
|
||||
*/
|
||||
public static ObpGwMetricDTO from(ObpGwMetric entity) {
|
||||
ObpGwMetricDTO dto = new ObpGwMetricDTO();
|
||||
dto.setClientId(entity.getClientId());
|
||||
dto.setHostname(entity.getHostname());
|
||||
dto.setTimeslice(formatTimeslice(entity.getTimeslice()));
|
||||
dto.setApiName(entity.getApiName());
|
||||
dto.setUri(entity.getUri());
|
||||
dto.setAttemptedCount(entity.getAttemptedCount());
|
||||
dto.setCompletedCount(entity.getCompletedCount());
|
||||
dto.setApiId(entity.getApiId());
|
||||
dto.setAppId(entity.getAppId());
|
||||
dto.setOrgId(entity.getOrgId());
|
||||
dto.setAppIdByCaPortal(entity.getAppIdByCaPortal());
|
||||
dto.setOrgIdByCaPortal(entity.getOrgIdByCaPortal());
|
||||
dto.setApiIdByCaPortal(entity.getApiIdByCaPortal());
|
||||
dto.setPartnerCode(entity.getPartnerCode());
|
||||
dto.setApiIdByCa(entity.getApiIdByCa());
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* timeslice를 ISO8601 형식 + UTC 시간대로 변환
|
||||
* 예: 2025-12-02T01:00:00+0000
|
||||
*/
|
||||
private static String formatTimeslice(LocalDateTime timeslice) {
|
||||
if (timeslice == null) return null;
|
||||
return timeslice.format(ISO8601_FORMATTER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package com.eactive.eai.rms.onl.apim.obp;
|
||||
|
||||
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
|
||||
import com.eactive.eai.rms.common.base.BaseService;
|
||||
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
||||
import com.eactive.eai.rms.common.datasource.DataSourceType;
|
||||
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
||||
import com.eactive.eai.rms.common.util.CommonUtil;
|
||||
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricDAO;
|
||||
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricService;
|
||||
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricVO;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* ObpGwMetric Man Service - 비즈니스 로직
|
||||
* API Gateway 로그 데이터를 집계하여 ObpGwMetric 엔티티로 저장
|
||||
*/
|
||||
@Service
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
public class ObpGwMetricManService extends BaseService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ObpGwMetricManService.class);
|
||||
|
||||
/**
|
||||
* 기본 집계 범위 시간 (단위: 시간)
|
||||
* - 1: 직전 1시간만 집계
|
||||
* - 5: 직전 5시간까지 각각 1시간 단위로 집계
|
||||
*
|
||||
* <p>Job 파라미터로 오버라이드 가능 (aggregation.hour.range)</p>
|
||||
*/
|
||||
public static final int DEFAULT_AGGREGATION_HOUR_RANGE = 1;
|
||||
|
||||
private static final DateTimeFormatter TIMESLICE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHH0000000");
|
||||
private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
|
||||
private static final DateTimeFormatter YYYYMMDD_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
private final ObpGwMetricDAO obpGwMetricDAO;
|
||||
private final ObpGwMetricService obpGwMetricService;
|
||||
|
||||
public ObpGwMetricManService(ObpGwMetricDAO obpGwMetricDAO, ObpGwMetricService obpGwMetricService) {
|
||||
this.obpGwMetricDAO = obpGwMetricDAO;
|
||||
this.obpGwMetricService = obpGwMetricService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 다중 시간대 집계 실행 (Job에서 호출) - 기본 집계 범위 사용
|
||||
*/
|
||||
public void executeHourlyAggregation() {
|
||||
executeHourlyAggregation(DEFAULT_AGGREGATION_HOUR_RANGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 다중 시간대 집계 실행 (Job에서 호출)
|
||||
*
|
||||
* @param aggregationHourRange 집계 범위 시간 (단위: 시간)
|
||||
* 예: 1=직전 1시간, 3=직전 3시간 각각 집계
|
||||
*/
|
||||
public void executeHourlyAggregation(int aggregationHourRange) {
|
||||
long totalStartTime = System.currentTimeMillis();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime baseHour = now.truncatedTo(ChronoUnit.HOURS); // 현재 시간의 정각
|
||||
|
||||
log.info("========== ObpGwMetric 집계 시작 ==========");
|
||||
log.info("실행 시간: {}, aggregationHourRange: {}", now, aggregationHourRange);
|
||||
|
||||
for (int i = 1; i <= aggregationHourRange; i++) {
|
||||
LocalDateTime targetHour = baseHour.minusHours(i);
|
||||
log.info("집계 대상 시간대: {}", targetHour);
|
||||
createHourlyData(targetHour);
|
||||
}
|
||||
|
||||
long totalElapsed = System.currentTimeMillis() - totalStartTime;
|
||||
log.info("========== ObpGwMetric 집계 완료 ==========");
|
||||
log.info("전체 집계 소요시간: {}ms ({}초)", totalElapsed, String.format("%.2f", totalElapsed / 1000.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 1시간 단위 ObpGwMetric 데이터 생성 (UPSERT)
|
||||
*/
|
||||
public void createHourlyData(LocalDateTime targetHour) {
|
||||
long jobStartTime = System.currentTimeMillis();
|
||||
log.info("ObpGwMetric 집계 시작: targetHour={}", targetHour);
|
||||
|
||||
try {
|
||||
String timeslice = targetHour.format(TIMESLICE_FORMATTER);
|
||||
String yyyymmdd = targetHour.format(YYYYMMDD_FORMATTER);
|
||||
String tableName = CommonUtil.getLogTable(yyyymmdd, true);
|
||||
String startTime = targetHour.format(TIMESTAMP_FORMATTER);
|
||||
String endTime = targetHour.plusHours(1).minusNanos(1).format(TIMESTAMP_FORMATTER);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 1단계: GW DB에서 EAIBZWKDSTCD 코드 목록 조회
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
long step1Start = System.currentTimeMillis();
|
||||
DataSourceType apigwDataSourceType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
|
||||
DataSourceContextHolder.setDataSourceType(apigwDataSourceType);
|
||||
|
||||
List<String> bzwkCodes = obpGwMetricDAO.selectAllBzwkCodes(apigwDataSourceType.getSchema());
|
||||
log.info("[1단계] EAIBZWKDSTCD 코드 조회 완료: count={}, elapsed={}ms",
|
||||
bzwkCodes.size(), System.currentTimeMillis() - step1Start);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 2단계: 코드별 개별 쿼리 실행 후 결과 병합
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
long step2Start = System.currentTimeMillis();
|
||||
Map<String, ObpGwMetricVO> aggregatedMap = new HashMap<>();
|
||||
int totalLogCount = 0;
|
||||
|
||||
for (String bzwkCode : bzwkCodes) {
|
||||
long codeStart = System.currentTimeMillis();
|
||||
|
||||
HashMap<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("schemaId", apigwDataSourceType.getSchema());
|
||||
paramMap.put("tableName", tableName);
|
||||
paramMap.put("bzwkCode", bzwkCode);
|
||||
paramMap.put("startTime", startTime);
|
||||
paramMap.put("endTime", endTime);
|
||||
paramMap.put("timeslice", timeslice);
|
||||
|
||||
List<ObpGwMetricVO> partialResult = obpGwMetricDAO.selectHourlyMetricsByBzwkCode(paramMap);
|
||||
|
||||
for (ObpGwMetricVO vo : partialResult) {
|
||||
String key = vo.getAggregationKey();
|
||||
aggregatedMap.merge(key, vo, this::mergeMetricVO);
|
||||
}
|
||||
|
||||
if (!partialResult.isEmpty()) {
|
||||
log.debug("[2단계] bzwkCode={}, 집계건수={}, elapsed={}ms",
|
||||
bzwkCode, partialResult.size(), System.currentTimeMillis() - codeStart);
|
||||
}
|
||||
totalLogCount += partialResult.size();
|
||||
}
|
||||
|
||||
List<ObpGwMetricVO> aggregatedList = new ArrayList<>(aggregatedMap.values());
|
||||
log.info("[2단계] 로그 집계 완료: 원본건수={}, 병합후건수={}, elapsed={}ms",
|
||||
totalLogCount, aggregatedList.size(), System.currentTimeMillis() - step2Start);
|
||||
|
||||
if (aggregatedList.isEmpty()) {
|
||||
log.warn("집계 대상 데이터 없음: targetHour={}", targetHour);
|
||||
return;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 3단계: 부가 정보 조회 (apiName, uri) - GW DB
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
long step3Start = System.currentTimeMillis();
|
||||
Set<String> apiIds = aggregatedList.stream()
|
||||
.map(ObpGwMetricVO::getApiId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<String, String> apiNameMap = obpGwMetricDAO.selectApiNames(apigwDataSourceType.getSchema(), apiIds);
|
||||
Map<String, String> uriMap = obpGwMetricDAO.selectUris(apigwDataSourceType.getSchema(), apiIds);
|
||||
log.info("[3단계] 부가정보 조회 완료: apiCount={}, elapsed={}ms",
|
||||
apiIds.size(), System.currentTimeMillis() - step3Start);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 4단계: 지표 DB로 전환 후 appId 조회 및 UPSERT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
long step4Start = System.currentTimeMillis();
|
||||
DataSourceContextHolder.setDataSourceType(
|
||||
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.MONITORING));
|
||||
|
||||
Set<String> orgIds = aggregatedList.stream()
|
||||
.map(ObpGwMetricVO::getOrgId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
Map<String, String> appIdMap = obpGwMetricDAO.selectAppIdsByOrgIds(orgIds);
|
||||
|
||||
int insertCount = 0;
|
||||
int updateCount = 0;
|
||||
|
||||
for (ObpGwMetricVO vo : aggregatedList) {
|
||||
LocalDateTime timesliceDateTime = vo.getTimesliceAsLocalDateTime();
|
||||
|
||||
Optional<ObpGwMetric> existing = obpGwMetricService.findByUniqueKey(
|
||||
timesliceDateTime,
|
||||
vo.getApiId(),
|
||||
vo.getClientId(),
|
||||
vo.getHostname(),
|
||||
vo.getOrgId()
|
||||
);
|
||||
|
||||
if (existing.isPresent()) {
|
||||
// UPDATE: 기존 데이터 갱신
|
||||
ObpGwMetric entity = existing.get();
|
||||
entity.setAttemptedCount(vo.getAttemptedCount());
|
||||
entity.setCompletedCount(vo.getCompletedCount());
|
||||
entity.setApiName(apiNameMap.get(vo.getApiId()));
|
||||
entity.setUri(uriMap.get(vo.getApiId()));
|
||||
entity.setAppId(appIdMap.get(vo.getOrgId()));
|
||||
entity.setUpdateCount(entity.getUpdateCount() + 1); // 보정 횟수 증가
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
entity.setUpdatedBy("GwMetricHourJob");
|
||||
obpGwMetricService.save(entity);
|
||||
updateCount++;
|
||||
} else {
|
||||
// INSERT: 신규 데이터 생성
|
||||
ObpGwMetric entity = convertToEntity(vo, apiNameMap, uriMap, appIdMap);
|
||||
obpGwMetricService.save(entity);
|
||||
insertCount++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[4단계] UPSERT 완료: INSERT={}, UPDATE={}, elapsed={}ms",
|
||||
insertCount, updateCount, System.currentTimeMillis() - step4Start);
|
||||
|
||||
long totalElapsed = System.currentTimeMillis() - jobStartTime;
|
||||
log.info("ObpGwMetric 집계 완료: targetHour={}, 총 소요시간={}ms ({}초)",
|
||||
targetHour, totalElapsed, totalElapsed / 1000.0);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("ObpGwMetric 집계 실패: targetHour={}", targetHour, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ObpGwMetricVO 병합 (count 누적)
|
||||
*/
|
||||
private ObpGwMetricVO mergeMetricVO(ObpGwMetricVO existing, ObpGwMetricVO incoming) {
|
||||
existing.setAttemptedCount(
|
||||
(existing.getAttemptedCount() != null ? existing.getAttemptedCount() : 0L) +
|
||||
(incoming.getAttemptedCount() != null ? incoming.getAttemptedCount() : 0L)
|
||||
);
|
||||
existing.setCompletedCount(
|
||||
(existing.getCompletedCount() != null ? existing.getCompletedCount() : 0L) +
|
||||
(incoming.getCompletedCount() != null ? incoming.getCompletedCount() : 0L)
|
||||
);
|
||||
return existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* VO → Entity 변환
|
||||
*/
|
||||
private ObpGwMetric convertToEntity(ObpGwMetricVO vo, Map<String, String> apiNameMap,
|
||||
Map<String, String> uriMap, Map<String, String> appIdMap) {
|
||||
ObpGwMetric entity = new ObpGwMetric();
|
||||
entity.setClientId(vo.getClientId());
|
||||
entity.setHostname(vo.getHostname());
|
||||
entity.setTimeslice(vo.getTimesliceAsLocalDateTime());
|
||||
entity.setApiId(vo.getApiId());
|
||||
entity.setOrgId(vo.getOrgId());
|
||||
entity.setAttemptedCount(vo.getAttemptedCount());
|
||||
entity.setCompletedCount(vo.getCompletedCount());
|
||||
entity.setApiName(apiNameMap.get(vo.getApiId()));
|
||||
entity.setUri(uriMap.get(vo.getApiId()));
|
||||
entity.setAppId(appIdMap.get(vo.getOrgId()));
|
||||
entity.setUpdateCount(0);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
entity.setCreatedBy("GwMetricHourJob");
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 시간대의 GwMetric 데이터 조회 (REST API용)
|
||||
*/
|
||||
public List<ObpGwMetricDTO> findByTimeslice(LocalDateTime timeslice) {
|
||||
DataSourceContextHolder.setDataSourceType(
|
||||
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.MONITORING));
|
||||
|
||||
List<ObpGwMetric> entities = obpGwMetricService.findByTimeslice(timeslice);
|
||||
return entities.stream()
|
||||
.map(ObpGwMetricDTO::from)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import com.eactive.eai.rms.onl.audit.adapter.ui.AuditUI;
|
||||
@Mapper(config = BaseMapperConfig.class, uses = {})
|
||||
public interface AuditUIMapper {
|
||||
|
||||
@Mapping(target = "revTstmp", expression = "java(mapLongToString(entity.getRevTstmp()))")
|
||||
@Mapping(target = "revTstmp", source = "revTstmp")
|
||||
@Mapping(target = "revNo", source = "rev")
|
||||
@Mapping(target = "revUserId", source = "userId")
|
||||
void map(CustomRevisionEntity entity, @MappingTarget AuditUI auditUI);
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.eactive.eai.rms.onl.common.service;
|
||||
|
||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||
import com.eactive.eai.rms.common.util.CommonUtil;
|
||||
import com.eactive.eai.rms.onl.apim.obp.ObpGwMetricManService;
|
||||
import com.eactive.eai.rms.onl.common.util.DateUtil;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
/**
|
||||
* ObpGwMetric Hour Job - Quartz Job
|
||||
* API Gateway 로그 데이터를 1시간 단위로 집계하여 ObpGwMetric 테이블에 저장
|
||||
*
|
||||
* <p>Cron Schedule 권장:</p>
|
||||
* <ul>
|
||||
* <li>기본: 0 5 * * * ? (매시 05분 1회 실행)</li>
|
||||
* <li>보정 (15분): 0 5,20,35,50 * * * ? (15분 간격 4회 실행)</li>
|
||||
* <li>보정 (10분): 0 5,15,25,35,45,55 * * * ? (10분 간격 6회 실행)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Job 파라미터 (JobDataMap):</p>
|
||||
* <ul>
|
||||
* <li><b>aggregation.hour.range</b>: 집계 범위 시간 (단위: 시간, 기본값: 1)
|
||||
* <ul>
|
||||
* <li>1: 직전 1시간만 집계</li>
|
||||
* <li>3: 직전 3시간 각각 집계</li>
|
||||
* <li>5: 직전 5시간 각각 집계</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>예시 (Quartz 스케줄 등록 시):</p>
|
||||
* <pre>
|
||||
* JobDataMap jobDataMap = new JobDataMap();
|
||||
* jobDataMap.put("aggregation.hour.range", "3");
|
||||
* </pre>
|
||||
*/
|
||||
public class ObpGwMetricHourJob implements Job {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ObpGwMetricHourJob.class);
|
||||
|
||||
/** Job 파라미터 키: 집계 범위 시간 */
|
||||
public static final String PARAM_AGGREGATION_HOUR_RANGE = "aggregation.hour.range";
|
||||
|
||||
/** 기본 집계 범위 시간 (시간 단위) */
|
||||
public static final int DEFAULT_AGGREGATION_HOUR_RANGE = 1;
|
||||
|
||||
private transient MonitoringContext monitoringContext;
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||
log.info("*** START ObpGwMetricHourJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
||||
|
||||
// Job 파라미터 로깅
|
||||
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
|
||||
logJobParameters(jobDataMap);
|
||||
|
||||
ApplicationContext appContext;
|
||||
try {
|
||||
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
|
||||
} catch (SchedulerException e) {
|
||||
log.error("applicationContext get module error", e);
|
||||
return;
|
||||
}
|
||||
|
||||
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
|
||||
ObpGwMetricManService obpGwMetricManService = appContext.getBean(ObpGwMetricManService.class);
|
||||
|
||||
// 집계 범위 시간 파라미터 파싱
|
||||
int aggregationHourRange = parseAggregationHourRange(jobDataMap);
|
||||
|
||||
try {
|
||||
obpGwMetricManService.executeHourlyAggregation(aggregationHourRange);
|
||||
} catch (Exception e) {
|
||||
log.error("ObpGwMetricHourJob execution failed", e);
|
||||
throw new JobExecutionException(e);
|
||||
}
|
||||
|
||||
log.info("*** END ObpGwMetricHourJob run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Job 파라미터 로깅
|
||||
*/
|
||||
private void logJobParameters(JobDataMap jobDataMap) {
|
||||
if (jobDataMap == null || jobDataMap.isEmpty()) {
|
||||
log.debug("Job 파라미터 없음 (기본값 사용)");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder("Job 파라미터: ");
|
||||
for (String key : jobDataMap.getKeys()) {
|
||||
sb.append(key).append("=").append(jobDataMap.getString(key)).append(", ");
|
||||
}
|
||||
log.info(sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 집계 범위 시간 파라미터 파싱
|
||||
*
|
||||
* @param jobDataMap Job 파라미터 맵
|
||||
* @return 집계 범위 시간 (파싱 실패 시 기본값 반환)
|
||||
*/
|
||||
private int parseAggregationHourRange(JobDataMap jobDataMap) {
|
||||
if (jobDataMap == null) {
|
||||
return DEFAULT_AGGREGATION_HOUR_RANGE;
|
||||
}
|
||||
|
||||
try {
|
||||
String value = jobDataMap.getString(PARAM_AGGREGATION_HOUR_RANGE);
|
||||
if (value != null && !value.trim().isEmpty()) {
|
||||
int parsedValue = Integer.parseInt(value.trim());
|
||||
if (parsedValue < 1) {
|
||||
log.warn("aggregation.hour.range 값이 1 미만입니다. 기본값({}) 사용: input={}",
|
||||
DEFAULT_AGGREGATION_HOUR_RANGE, parsedValue);
|
||||
return DEFAULT_AGGREGATION_HOUR_RANGE;
|
||||
}
|
||||
// 개발 모드(D)가 아닌 경우에만 24시간 제한 적용
|
||||
if (parsedValue > 24 && !isDevMode()) {
|
||||
log.warn("aggregation.hour.range 값이 24 초과입니다. 24로 제한: input={}", parsedValue);
|
||||
return 24;
|
||||
}
|
||||
return parsedValue;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("aggregation.hour.range 파싱 실패, 기본값({}) 사용: {}",
|
||||
DEFAULT_AGGREGATION_HOUR_RANGE, e.getMessage());
|
||||
}
|
||||
|
||||
return DEFAULT_AGGREGATION_HOUR_RANGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 개발 모드 여부 확인
|
||||
* @return eai.systemmode=D 이면 true
|
||||
*/
|
||||
private boolean isDevMode() {
|
||||
String systemMode = System.getProperty("eai.systemmode", "");
|
||||
return "D".equalsIgnoreCase(systemMode);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.eactive.eai.rms.onl.common.util.FileSystemUtil;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.apache.poi.hssf.usermodel.HSSFCell;
|
||||
@@ -75,7 +76,7 @@ public class TransactionReportHourJob implements Job {
|
||||
}
|
||||
|
||||
String trTime = monitoringContext.getStringProperty(MonitoringContext.RMS_TRANSACTION_LOG_TIME, "");
|
||||
if( trTime.indexOf("EVERY_HOUR") > -1 ) {
|
||||
if (trTime.contains("EVERY_HOUR")) {
|
||||
// go on
|
||||
} else if (!StringUtils.contains(trTime, hour)) {
|
||||
return;
|
||||
@@ -93,9 +94,11 @@ public class TransactionReportHourJob implements Job {
|
||||
+ CommonUtil.getToday("yyyy-MM-dd HH:mm") + ")");
|
||||
}
|
||||
|
||||
String pathDir = monitoringContext.getStringProperty( MonitoringContext.RMS_TRANSACTION_LOG_PATH,"/fslog/eai/")
|
||||
+ monitoringContext.getStringProperty( Keys.SERVER_KEY)
|
||||
+ "/trlog";
|
||||
String pathDir = FileSystemUtil.pathNormalize(
|
||||
monitoringContext.getStringProperty( MonitoringContext.RMS_TRANSACTION_LOG_PATH,"/fslog/eai/")
|
||||
, monitoringContext.getStringProperty( Keys.SERVER_KEY)
|
||||
, "trlog"
|
||||
);
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// 로컬테스트용
|
||||
|
||||
@@ -17,13 +17,14 @@ import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -46,6 +47,7 @@ public class UmsDispatchService {
|
||||
private KjbUmsService kjbUmsService;
|
||||
private KjbEmailSendModule kjbEmailSendModule;
|
||||
private final KjbPropertyInjector kjbPropertyInjector;
|
||||
private KjbProperty kjbProperty;
|
||||
|
||||
|
||||
@Autowired
|
||||
@@ -55,11 +57,7 @@ public class UmsDispatchService {
|
||||
MessageRequestRepository messageRequestRepository
|
||||
) {
|
||||
this.kjbPropertyInjector = new KjbPropertyInjector(monitoringPropertyService, PROP_GORUP_ID);
|
||||
|
||||
this.messageRequestRepository = messageRequestRepository;
|
||||
// this.kjbEmailService = kjbEmailService;
|
||||
// this.kjbSafedbWrapper = kjbSafedbWrapper;
|
||||
// this.monitoringPropertyService = monitoringPropertyService;
|
||||
|
||||
this.executorService = new ThreadPoolExecutor(
|
||||
CORE_POOL_SIZE,
|
||||
@@ -72,32 +70,51 @@ public class UmsDispatchService {
|
||||
this.transactionTemplate = new TransactionTemplate(transactionManager);
|
||||
log.debug("UmsDispatchService initialized with thread pool - core: {}, max: {}, queue: {}",
|
||||
CORE_POOL_SIZE, MAX_POOL_SIZE, QUEUE_CAPACITY);
|
||||
}
|
||||
|
||||
|
||||
KjbProperty prop = new KjbProperty();
|
||||
// prop.setEaiBatchUrl(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.eai_batch.url"));
|
||||
// prop.setUmsEaiBatchSendDir(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.eai_batch.send_dir"));
|
||||
// prop.setUmsEaiBatchBackupDir(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.eai_batch.backup_dir"));
|
||||
// prop.setUmsEaiBatchInterfaceId(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.eai_batch.interface_id"));
|
||||
// prop.setUmsHostUrl(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.host_url"));
|
||||
// prop.setUmsApiKey(monitoringPropertyService.getPrpty2ValById(PROP_GORUP_ID, "kjb.ums.api_key"));
|
||||
prop = kjbPropertyInjector.inject(prop);
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.kjbProperty = kjbPropertyInjector.inject(new KjbProperty());
|
||||
|
||||
try {
|
||||
this.kjbUmsService = new KjbUmsService(prop);
|
||||
|
||||
this.kjbEmailSendModule = KjbEmailSendModule.getInstance(prop, messageRequestRepository);
|
||||
this.kjbUmsService = new KjbUmsService(kjbProperty);
|
||||
this.kjbEmailSendModule = KjbEmailSendModule.getInstance(kjbProperty, messageRequestRepository);
|
||||
log.info("KjbUmsService and KjbEmailSendModule initialized successfully");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to initialize KjbUmsService or KjbEmailSendModule: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 설정값이 비어있을 경우 DB에서 재로딩 후 서비스 재초기화
|
||||
*/
|
||||
private void reloadPropertyIfNeeded() {
|
||||
if (!kjbPropertyInjector.isUrlConfigured(kjbProperty)) {
|
||||
log.info("KjbProperty URL 값이 비어있어 재로딩 시도");
|
||||
KjbProperty reloaded = kjbPropertyInjector.reloadIfUrlEmpty(kjbProperty);
|
||||
|
||||
if (reloaded != kjbProperty && kjbPropertyInjector.isUrlConfigured(reloaded)) {
|
||||
this.kjbProperty = reloaded;
|
||||
try {
|
||||
this.kjbUmsService = new KjbUmsService(kjbProperty);
|
||||
this.kjbEmailSendModule = KjbEmailSendModule.getInstance(kjbProperty, messageRequestRepository);
|
||||
log.info("KjbProperty 재로딩 후 서비스 재초기화 완료");
|
||||
} catch (Exception e) {
|
||||
log.error("KjbProperty 재로딩 후 서비스 재초기화 실패: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
public void processMessages() {
|
||||
log.info("Starting to process pending messages");
|
||||
|
||||
// 상태가 PENDING인 메시지들을 조회하고 바로 PROCESSING으로 변경
|
||||
// URL 설정값이 비어있을 경우 DB에서 재로딩 시도
|
||||
reloadPropertyIfNeeded();
|
||||
|
||||
// 상태가 PENDING인 메시지들을 조회하고 바로 PROCESSING으로 변경
|
||||
List<MessageRequest> pendingMessages = entityManager.createQuery(
|
||||
"SELECT m FROM MessageRequest m WHERE m.requestStatus = :status " +
|
||||
"ORDER BY m.requestDate ASC")
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.eactive.eai.rms.onl.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public class FileSystemUtil {
|
||||
|
||||
/**
|
||||
* Normalizes a path by joining segments and applying the correct file separator for the current operating system.
|
||||
* It handles redundant separators between segments.
|
||||
*
|
||||
* <p>Examples for Linux/macOS:</p>
|
||||
* <ul>
|
||||
* <li>{@code pathNormalize("/asd1/asd2", "asd3")} returns {@code "/asd1/asd2/asd3"}</li>
|
||||
* <li>{@code pathNormalize("/asd1/asd2/", "/asd3")} returns {@code "/asd1/asd2/asd3"}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Examples for Windows:</p>
|
||||
* <ul>
|
||||
* <li>{@code pathNormalize("C:/Users/Test", "Documents")} returns {@code "C:\\Users\\Test\\Documents"}</li>
|
||||
* <li>{@code pathNormalize("C:/Users/Test/", "/Documents")} returns {@code "C:\\Users\\Test\\Documents"}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param args A variable number of path segments to join.
|
||||
* @return A normalized path string for the current operating system.
|
||||
*/
|
||||
public static String pathNormalize(String... args) {
|
||||
if (args == null || args.length == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringJoiner joiner = new StringJoiner(File.separator);
|
||||
for (String arg : args) {
|
||||
if (arg != null && !arg.isEmpty()) {
|
||||
String segment = arg;
|
||||
// Remove leading separators from the segment
|
||||
while (segment.startsWith("/") || segment.startsWith("\\")) {
|
||||
segment = segment.substring(1);
|
||||
}
|
||||
// Remove trailing separators from the segment
|
||||
while (segment.endsWith("/") || segment.endsWith("\\")) {
|
||||
segment = segment.substring(0, segment.length() - 1);
|
||||
}
|
||||
if (!segment.isEmpty()) {
|
||||
joiner.add(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String joinedPath = joiner.toString();
|
||||
|
||||
// Handle the root path for the first argument
|
||||
String firstArg = args[0];
|
||||
if (firstArg != null && !firstArg.isEmpty()) {
|
||||
if (firstArg.startsWith("/")) {
|
||||
joinedPath = File.separator + joinedPath;
|
||||
} else if (firstArg.matches("^[a-zA-Z]:\\\\") || firstArg.matches("^[a-zA-Z]:/.*")) { // Handle Windows drive letters, e.g., "C:\" or "C:/"
|
||||
String drive = firstArg.substring(0, 2);
|
||||
return drive + File.separator + joinedPath.substring(joinedPath.indexOf(File.separator) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Final normalization for Windows if the path starts with a separator (e.g. from a UNC path)
|
||||
if (System.getProperty("os.name").toLowerCase().contains("win")) {
|
||||
if (args[0] != null && args[0].startsWith("//") || args[0].startsWith("\\\\")) {
|
||||
return "\\\\" + joinedPath;
|
||||
}
|
||||
return joinedPath.replace("/", "\\");
|
||||
}
|
||||
|
||||
return joinedPath.replace("\\", "/");
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package com.eactive.eai.rms.onl.manage.inflow.group;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import com.eactive.eai.agent.command.CommonCommand;
|
||||
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
|
||||
import com.eactive.eai.rms.common.combo.ComboService;
|
||||
import com.eactive.eai.rms.common.combo.ComboVo;
|
||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@Controller
|
||||
public class InflowGroupControlManController extends OnlBaseAnnotationController {
|
||||
|
||||
@Autowired
|
||||
private InflowGroupControlManService service;
|
||||
|
||||
@Autowired
|
||||
private ComboService comboService;
|
||||
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// ========== View Mappings ==========
|
||||
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.view")
|
||||
public String viewList() {
|
||||
return "/onl/admin/inflow/inflowGroupControlMan";
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.view", params = "cmd=DETAIL")
|
||||
public String viewDetail() {
|
||||
return "/onl/admin/inflow/inflowGroupControlManDetail";
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.view", params = "cmd=INTERFACE_POPUP")
|
||||
public String viewInterfacePopup() {
|
||||
return "/onl/admin/inflow/inflowGroupInterfacePopup";
|
||||
}
|
||||
|
||||
// ========== JSON API Mappings ==========
|
||||
|
||||
/**
|
||||
* 그룹 목록 조회
|
||||
*/
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST")
|
||||
public ResponseEntity<GridResponse<InflowGroupControlManUI>> selectList(HttpServletRequest request,
|
||||
Pageable pageVo, String searchGroupName) {
|
||||
Page<InflowGroupControlManUI> uiPage = service.selectGroupList(pageVo, searchGroupName);
|
||||
return ResponseEntity.ok(new GridResponse<>(uiPage));
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹 상세 조회
|
||||
*/
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=DETAIL")
|
||||
public ResponseEntity<InflowGroupControlManUI> selectDetail(HttpServletRequest request,
|
||||
HttpServletResponse response, String groupId) {
|
||||
InflowGroupControlManUI ui = service.selectGroupDetail(groupId);
|
||||
return ResponseEntity.ok(ui);
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹 저장 (INSERT)
|
||||
*/
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=INSERT")
|
||||
public String insert(HttpServletRequest request, HttpServletResponse response, InflowGroupControlManUI ui,
|
||||
@RequestParam(value = "interfaceListJson", required = false) String interfaceListJson) throws Exception {
|
||||
parseInterfaceList(ui, interfaceListJson);
|
||||
String modifiedBy = getSessionUserId(request);
|
||||
service.mergeGroup(ui, modifiedBy);
|
||||
|
||||
// Agent Command 전송
|
||||
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.ReloadInflowGroupControlCommand",
|
||||
ui.getGroupId());
|
||||
agentUtilService.broadcast(command);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹 저장 (UPDATE)
|
||||
*/
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=UPDATE")
|
||||
public String update(HttpServletRequest request, HttpServletResponse response, InflowGroupControlManUI ui,
|
||||
@RequestParam(value = "interfaceListJson", required = false) String interfaceListJson) throws Exception {
|
||||
parseInterfaceList(ui, interfaceListJson);
|
||||
String modifiedBy = getSessionUserId(request);
|
||||
service.mergeGroup(ui, modifiedBy);
|
||||
|
||||
// Agent Command 전송
|
||||
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.ReloadInflowGroupControlCommand",
|
||||
ui.getGroupId());
|
||||
agentUtilService.broadcast(command);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹 삭제
|
||||
*/
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=DELETE")
|
||||
public String delete(HttpServletRequest request, HttpServletResponse response, String groupId) throws Exception {
|
||||
service.deleteGroup(groupId);
|
||||
|
||||
// Agent Command 전송
|
||||
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.RemoveInflowGroupControlCommand",
|
||||
groupId);
|
||||
agentUtilService.broadcast(command);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 콤보박스 초기화 데이터
|
||||
*/
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST_INIT_COMBO")
|
||||
public ModelAndView initCombo(HttpServletRequest request, HttpServletResponse response) {
|
||||
List<ComboVo> useYnRows = comboService.getFromCode("USE_YN");
|
||||
List<ComboVo> timeUnitRows = comboService.getFromCode("INFLOW_CTRL_TIME_UNIT");
|
||||
|
||||
Map<String, List<ComboVo>> resultMap = new HashMap<>();
|
||||
resultMap.put("useYnRows", useYnRows);
|
||||
resultMap.put("timeUnitRows", timeUnitRows);
|
||||
|
||||
return new ModelAndView("jsonView", resultMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인터페이스 목록 조회 (팝업용)
|
||||
*/
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=INTERFACE_LIST")
|
||||
public ResponseEntity<GridResponse<InflowGroupMappingUI>> selectInterfaceList(HttpServletRequest request,
|
||||
Pageable pageVo, String searchName) {
|
||||
Page<InflowGroupMappingUI> uiPage = service.selectInterfaceListForPopup(pageVo, searchName);
|
||||
return ResponseEntity.ok(new GridResponse<>(uiPage));
|
||||
}
|
||||
|
||||
/**
|
||||
* 인터페이스 중복 등록 체크
|
||||
*/
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST_CHECK_DUPLICATE")
|
||||
public ModelAndView checkDuplicate(HttpServletRequest request, HttpServletResponse response,
|
||||
@RequestParam("interfaceId") String interfaceId,
|
||||
@RequestParam(value = "groupId", required = false) String groupId) {
|
||||
String existingGroupName = service.checkInterfaceDuplicate(interfaceId, groupId);
|
||||
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("duplicate", existingGroupName != null);
|
||||
resultMap.put("groupName", existingGroupName);
|
||||
|
||||
return new ModelAndView("jsonView", resultMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹 버킷 상태 조회 (GW 서버별 현황)
|
||||
*/
|
||||
@RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=LIST_BUCKET_STATUS")
|
||||
public ModelAndView getBucketStatus(HttpServletRequest request, HttpServletResponse response,
|
||||
@RequestParam("groupId") String groupId) {
|
||||
List<Map<String, Object>> statusList = service.getGroupBucketStatus(groupId);
|
||||
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("servers", statusList);
|
||||
|
||||
return new ModelAndView("jsonView", resultMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열을 인터페이스 목록으로 변환
|
||||
*/
|
||||
private void parseInterfaceList(InflowGroupControlManUI ui, String interfaceListJson) throws Exception {
|
||||
if (interfaceListJson != null && !interfaceListJson.isEmpty()) {
|
||||
List<InflowGroupMappingUI> interfaceList = objectMapper.readValue(interfaceListJson,
|
||||
new TypeReference<List<InflowGroupMappingUI>>() {
|
||||
});
|
||||
ui.setInterfaceList(interfaceList);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션에서 사용자 ID 조회
|
||||
*/
|
||||
private String getSessionUserId(HttpServletRequest request) {
|
||||
Object userId = request.getSession().getAttribute("userId");
|
||||
return userId != null ? userId.toString() : "SYSTEM";
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.eactive.eai.rms.onl.manage.inflow.group;
|
||||
|
||||
import org.mapstruct.BeanMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.NullValuePropertyMappingStrategy;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroup;
|
||||
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroupMapping;
|
||||
|
||||
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public interface InflowGroupControlManMapper {
|
||||
|
||||
InflowGroupControlManUI toVo(InflowControlGroup entity);
|
||||
|
||||
InflowControlGroup toEntity(InflowGroupControlManUI vo);
|
||||
|
||||
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
void updateToEntity(InflowGroupControlManUI vo, @MappingTarget InflowControlGroup entity);
|
||||
|
||||
// Mapping Entity <-> UI
|
||||
InflowGroupMappingUI toMappingVo(InflowControlGroupMapping entity);
|
||||
|
||||
InflowControlGroupMapping toMappingEntity(InflowGroupMappingUI vo);
|
||||
|
||||
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
void updateToMappingEntity(InflowGroupMappingUI vo, @MappingTarget InflowControlGroupMapping entity);
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
package com.eactive.eai.rms.onl.manage.inflow.group;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroup;
|
||||
import com.eactive.eai.data.entity.onl.inflow.InflowControlGroupMapping;
|
||||
import com.eactive.eai.data.entity.onl.inflow.QInflowControlGroup;
|
||||
import com.eactive.eai.data.entity.onl.inflow.QInflowControlGroupMapping;
|
||||
import com.eactive.eai.data.entity.onl.message.QEAIMessageEntity;
|
||||
import com.eactive.eai.rms.common.base.BaseService;
|
||||
import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlGroupMappingRepository;
|
||||
import com.eactive.eai.rms.data.entity.onl.inflow.InflowControlGroupRepository;
|
||||
import com.eactive.eai.rms.onl.common.service.EaiServerInfoService;
|
||||
import com.querydsl.core.BooleanBuilder;
|
||||
import com.querydsl.core.Tuple;
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||
|
||||
@Service
|
||||
public class InflowGroupControlManService extends BaseService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(InflowGroupControlManService.class);
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager entityManager;
|
||||
|
||||
@Autowired
|
||||
private InflowControlGroupRepository groupRepository;
|
||||
|
||||
@Autowired
|
||||
private InflowControlGroupMappingRepository mappingRepository;
|
||||
|
||||
@Autowired
|
||||
private InflowGroupControlManMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private EaiServerInfoService eaiServerInfoService;
|
||||
|
||||
private RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
/**
|
||||
* 그룹 목록 조회
|
||||
*/
|
||||
public Page<InflowGroupControlManUI> selectGroupList(Pageable pageable, String searchGroupName) {
|
||||
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
|
||||
QInflowControlGroup qGroup = QInflowControlGroup.inflowControlGroup;
|
||||
|
||||
BooleanBuilder boolBuilder = new BooleanBuilder();
|
||||
if (!StringUtils.isEmpty(searchGroupName)) {
|
||||
boolBuilder.and(qGroup.groupName.contains(searchGroupName));
|
||||
}
|
||||
|
||||
List<InflowControlGroup> groupList = jpaQueryFactory
|
||||
.selectFrom(qGroup)
|
||||
.where(boolBuilder)
|
||||
.orderBy(qGroup.groupId.asc())
|
||||
.offset(pageable.getOffset())
|
||||
.limit(pageable.getPageSize())
|
||||
.fetch();
|
||||
|
||||
List<InflowGroupControlManUI> uiList = groupList.stream()
|
||||
.map(mapper::toVo)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long totalCount = jpaQueryFactory
|
||||
.select(qGroup.groupId.count())
|
||||
.from(qGroup)
|
||||
.where(boolBuilder)
|
||||
.fetchOne();
|
||||
|
||||
return new PageImpl<>(uiList, pageable, totalCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹 상세 조회 (매핑된 인터페이스 포함)
|
||||
*/
|
||||
public InflowGroupControlManUI selectGroupDetail(String groupId) {
|
||||
Optional<InflowControlGroup> groupOpt = groupRepository.findById(groupId);
|
||||
if (!groupOpt.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InflowGroupControlManUI ui = mapper.toVo(groupOpt.get());
|
||||
|
||||
// 매핑된 인터페이스 목록 조회 (인터페이스 설명 포함)
|
||||
List<InflowGroupMappingUI> mappingList = selectMappingListWithDesc(groupId);
|
||||
ui.setInterfaceList(mappingList);
|
||||
|
||||
return ui;
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹에 매핑된 인터페이스 목록 조회 (인터페이스 설명 포함)
|
||||
*/
|
||||
private List<InflowGroupMappingUI> selectMappingListWithDesc(String groupId) {
|
||||
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
|
||||
QInflowControlGroupMapping qMapping = QInflowControlGroupMapping.inflowControlGroupMapping;
|
||||
QEAIMessageEntity qMessage = QEAIMessageEntity.eAIMessageEntity;
|
||||
|
||||
List<Tuple> tupleList = jpaQueryFactory
|
||||
.select(qMapping, qMessage.eaisvcdesc)
|
||||
.from(qMapping)
|
||||
.leftJoin(qMessage).on(qMapping.interfaceId.eq(qMessage.eaisvcname))
|
||||
.where(qMapping.groupId.eq(groupId))
|
||||
.orderBy(qMapping.interfaceId.asc())
|
||||
.fetch();
|
||||
|
||||
return tupleList.stream()
|
||||
.map(tuple -> {
|
||||
InflowGroupMappingUI mappingUI = mapper.toMappingVo(tuple.get(qMapping));
|
||||
mappingUI.setInterfaceDesc(tuple.get(qMessage.eaisvcdesc));
|
||||
return mappingUI;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹 저장 (INSERT/UPDATE)
|
||||
*/
|
||||
@Transactional
|
||||
public void mergeGroup(InflowGroupControlManUI ui, String modifiedBy) {
|
||||
String groupId = ui.getGroupId();
|
||||
boolean isNew = StringUtils.isEmpty(groupId);
|
||||
|
||||
InflowControlGroup entity;
|
||||
if (!isNew) {
|
||||
Optional<InflowControlGroup> groupOpt = groupRepository.findById(groupId);
|
||||
if (groupOpt.isPresent()) {
|
||||
entity = groupOpt.get();
|
||||
mapper.updateToEntity(ui, entity);
|
||||
} else {
|
||||
entity = mapper.toEntity(ui);
|
||||
}
|
||||
} else {
|
||||
entity = mapper.toEntity(ui);
|
||||
}
|
||||
entity.setModifiedBy(modifiedBy);
|
||||
entity.setModifiedAt(LocalDateTime.now());
|
||||
InflowControlGroup savedEntity = groupRepository.saveAndFlush(entity);
|
||||
|
||||
// 저장된 엔티티의 groupId 사용 (신규 생성 시 시퀀스에서 생성된 ID)
|
||||
String savedGroupId = savedEntity.getGroupId();
|
||||
|
||||
// 매핑 정보 저장
|
||||
saveMappings(savedGroupId, ui.getInterfaceList(), modifiedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹-인터페이스 매핑 저장
|
||||
*/
|
||||
@Transactional
|
||||
public void saveMappings(String groupId, List<InflowGroupMappingUI> interfaceList, String modifiedBy) {
|
||||
// 기존 매핑 삭제
|
||||
mappingRepository.deleteByGroupId(groupId);
|
||||
|
||||
// 새로운 매핑 저장
|
||||
if (interfaceList != null && !interfaceList.isEmpty()) {
|
||||
for (InflowGroupMappingUI mappingUI : interfaceList) {
|
||||
InflowControlGroupMapping mapping = new InflowControlGroupMapping();
|
||||
mapping.setInterfaceId(mappingUI.getInterfaceId());
|
||||
mapping.setGroupId(groupId);
|
||||
// 화면에서 전달된 useYn 사용, 없으면 기본값 "1"
|
||||
String useYn = StringUtils.isNotEmpty(mappingUI.getUseYn()) ? mappingUI.getUseYn() : "1";
|
||||
mapping.setUseYn(useYn);
|
||||
mapping.setModifiedBy(modifiedBy);
|
||||
mapping.setModifiedAt(LocalDateTime.now());
|
||||
mappingRepository.save(mapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹 삭제
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteGroup(String groupId) {
|
||||
// 매핑 먼저 삭제
|
||||
mappingRepository.deleteByGroupId(groupId);
|
||||
// 그룹 삭제
|
||||
groupRepository.deleteById(groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인터페이스 중복 등록 체크 (다른 그룹에 등록 여부)
|
||||
* @param interfaceId 체크할 인터페이스 ID
|
||||
* @param currentGroupId 현재 그룹 ID (수정 시 자기 그룹 제외, 신규 시 null)
|
||||
* @return 등록된 그룹명 (미등록 시 null)
|
||||
*/
|
||||
public String checkInterfaceDuplicate(String interfaceId, String currentGroupId) {
|
||||
Optional<InflowControlGroupMapping> mappingOpt = mappingRepository.findByInterfaceId(interfaceId);
|
||||
if (!mappingOpt.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InflowControlGroupMapping mapping = mappingOpt.get();
|
||||
String existingGroupId = mapping.getGroupId();
|
||||
|
||||
// 현재 그룹과 동일하면 중복 아님 (수정 시)
|
||||
if (existingGroupId.equals(currentGroupId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 그룹명 조회
|
||||
Optional<InflowControlGroup> groupOpt = groupRepository.findById(existingGroupId);
|
||||
if (groupOpt.isPresent()) {
|
||||
return groupOpt.get().getGroupName();
|
||||
}
|
||||
return existingGroupId; // 그룹명 없으면 ID 반환
|
||||
}
|
||||
|
||||
/**
|
||||
* 인터페이스 목록 조회 (팝업용)
|
||||
*/
|
||||
public Page<InflowGroupMappingUI> selectInterfaceListForPopup(Pageable pageable, String searchName) {
|
||||
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
|
||||
QEAIMessageEntity qMessage = QEAIMessageEntity.eAIMessageEntity;
|
||||
|
||||
BooleanBuilder boolBuilder = new BooleanBuilder();
|
||||
if (!StringUtils.isEmpty(searchName)) {
|
||||
boolBuilder.and(qMessage.eaisvcname.containsIgnoreCase(searchName)
|
||||
.or(qMessage.eaisvcdesc.containsIgnoreCase(searchName)));
|
||||
}
|
||||
|
||||
List<Tuple> tupleList = jpaQueryFactory
|
||||
.select(qMessage.eaisvcname, qMessage.eaisvcdesc)
|
||||
.from(qMessage)
|
||||
.where(boolBuilder)
|
||||
.orderBy(qMessage.eaisvcname.asc())
|
||||
.offset(pageable.getOffset())
|
||||
.limit(pageable.getPageSize())
|
||||
.fetch();
|
||||
|
||||
List<InflowGroupMappingUI> uiList = tupleList.stream()
|
||||
.map(tuple -> {
|
||||
InflowGroupMappingUI ui = new InflowGroupMappingUI();
|
||||
ui.setInterfaceId(tuple.get(qMessage.eaisvcname));
|
||||
ui.setInterfaceDesc(tuple.get(qMessage.eaisvcdesc));
|
||||
return ui;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long totalCount = jpaQueryFactory
|
||||
.select(qMessage.eaisvcname.count())
|
||||
.from(qMessage)
|
||||
.where(boolBuilder)
|
||||
.fetchOne();
|
||||
|
||||
return new PageImpl<>(uiList, pageable, totalCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹 버킷 상태 조회 (모든 GW 서버에서 수집)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Map<String, Object>> getGroupBucketStatus(String groupId) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
List<Map<String, String>> serverList = eaiServerInfoService.getEaiServerIpList();
|
||||
|
||||
for (Map<String, String> server : serverList) {
|
||||
String serverName = server.get("EAISVRINSTNM");
|
||||
String serverIp = server.get("EAISVRIP");
|
||||
String serverPort = server.get("WEBSERVERPORT");
|
||||
|
||||
if (StringUtils.isEmpty(serverPort)) {
|
||||
serverPort = server.get("EAISVRLSNPORT");
|
||||
}
|
||||
|
||||
Map<String, Object> serverStatus = new HashMap<>();
|
||||
serverStatus.put("serverName", serverName);
|
||||
serverStatus.put("serverIp", serverIp);
|
||||
serverStatus.put("serverPort", serverPort);
|
||||
|
||||
try {
|
||||
String url = String.format("http://%s:%s/manage/inflow/group/%s/bucket-status",
|
||||
serverIp, serverPort, groupId);
|
||||
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
|
||||
|
||||
if (response.getBody() != null && Boolean.TRUE.equals(response.getBody().get("success"))) {
|
||||
serverStatus.put("status", "online");
|
||||
serverStatus.put("data", response.getBody().get("data"));
|
||||
} else {
|
||||
serverStatus.put("status", "no_data");
|
||||
serverStatus.put("message", "그룹이 로드되지 않음");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("버킷 상태 조회 실패 - server: {}, error: {}", serverName, e.getMessage());
|
||||
serverStatus.put("status", "offline");
|
||||
serverStatus.put("message", "서버 연결 실패");
|
||||
}
|
||||
|
||||
result.add(serverStatus);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.eactive.eai.rms.onl.manage.inflow.group;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class InflowGroupControlManUI {
|
||||
/**
|
||||
* 그룹 ID
|
||||
*/
|
||||
@JsonProperty("GROUPID")
|
||||
private String groupId;
|
||||
|
||||
/**
|
||||
* 그룹명
|
||||
*/
|
||||
@JsonProperty("GROUPNAME")
|
||||
private String groupName;
|
||||
|
||||
/**
|
||||
* 임계치
|
||||
*/
|
||||
@JsonProperty("THRESHOLD")
|
||||
private Integer threshold;
|
||||
|
||||
/**
|
||||
* 초당 임계치
|
||||
*/
|
||||
@JsonProperty("THRESHOLDPERSECOND")
|
||||
private Integer thresholdPerSecond;
|
||||
|
||||
/**
|
||||
* 임계치 TimeUnit
|
||||
*/
|
||||
@JsonProperty("THRESHOLDTIMEUNIT")
|
||||
private String thresholdTimeUnit;
|
||||
|
||||
/**
|
||||
* 사용여부
|
||||
*/
|
||||
@JsonProperty("USEYN")
|
||||
private String useYn;
|
||||
|
||||
/**
|
||||
* 수정일시
|
||||
*/
|
||||
@JsonProperty("MODIFIEDAT")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime modifiedAt;
|
||||
|
||||
/**
|
||||
* 매핑된 인터페이스 목록 (저장용)
|
||||
*/
|
||||
private List<InflowGroupMappingUI> interfaceList;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.eactive.eai.rms.onl.manage.inflow.group;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class InflowGroupMappingUI {
|
||||
/**
|
||||
* 인터페이스 ID
|
||||
*/
|
||||
@JsonProperty("INTERFACEID")
|
||||
@JsonAlias("interfaceId")
|
||||
private String interfaceId;
|
||||
|
||||
/**
|
||||
* 인터페이스 설명 (조회용)
|
||||
*/
|
||||
@JsonProperty("INTERFACEDESC")
|
||||
@JsonAlias("interfaceDesc")
|
||||
private String interfaceDesc;
|
||||
|
||||
/**
|
||||
* 그룹 ID
|
||||
*/
|
||||
@JsonProperty("GROUPID")
|
||||
@JsonAlias("groupId")
|
||||
private String groupId;
|
||||
|
||||
/**
|
||||
* 사용여부
|
||||
*/
|
||||
@JsonProperty("USEYN")
|
||||
@JsonAlias("useYn")
|
||||
private String useYn;
|
||||
}
|
||||
@@ -19,6 +19,48 @@ public class KjbPropertyInjector {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* KjbProperty의 URL 값들이 설정되어 있는지 확인
|
||||
* @param property 검사할 KjbProperty 객체
|
||||
* @return URL 값들이 모두 설정되어 있으면 true
|
||||
*/
|
||||
public boolean isUrlConfigured(KjbProperty property) {
|
||||
if (property == null) return false;
|
||||
|
||||
// 주요 URL 값들 중 하나라도 설정되어 있으면 true
|
||||
return StringUtils.isNotEmpty(property.getEaiBatchUrl())
|
||||
|| StringUtils.isNotEmpty(property.getUmsHostUrl())
|
||||
|| StringUtils.isNotEmpty(property.getObpUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 값이 비어있을 경우 DB에서 재로딩 시도
|
||||
* @param property 기존 KjbProperty 객체
|
||||
* @return 재로딩된 KjbProperty 객체 (재로딩 실패 시 기존 객체 반환)
|
||||
*/
|
||||
public KjbProperty reloadIfUrlEmpty(KjbProperty property) {
|
||||
if (property == null) {
|
||||
return inject(new KjbProperty());
|
||||
}
|
||||
|
||||
if (!isUrlConfigured(property)) {
|
||||
log.info("KjbProperty URL 값이 비어있어 DB에서 재로딩 시도");
|
||||
try {
|
||||
KjbProperty reloaded = inject(new KjbProperty());
|
||||
if (isUrlConfigured(reloaded)) {
|
||||
log.info("KjbProperty 재로딩 성공");
|
||||
return reloaded;
|
||||
} else {
|
||||
log.warn("KjbProperty 재로딩 후에도 URL 값이 비어있음");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("KjbProperty 재로딩 실패: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return property;
|
||||
}
|
||||
|
||||
public <T> T inject(T property) {
|
||||
Class<?> clazz = property.getClass();
|
||||
|
||||
@@ -30,6 +72,11 @@ public class KjbPropertyInjector {
|
||||
String rawValue = monitoringPropertyService.getPropertyValue(groupId, key, anno.defaultValue());
|
||||
Object convertedValue = convertValue(field.getType(), rawValue);
|
||||
|
||||
// null이면 필드의 기본값 유지 (primitive 타입에 null 할당 방지)
|
||||
if (convertedValue == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
field.set(property, convertedValue);
|
||||
@@ -46,16 +93,26 @@ public class KjbPropertyInjector {
|
||||
|
||||
private static Object convertValue(Class<?> type, String rawValue) {
|
||||
if (rawValue == null) return null;
|
||||
if (StringUtils.isEmpty(rawValue)) return "";
|
||||
|
||||
// 빈 문자열인 경우 null 반환 (필드의 기본값 유지)
|
||||
if (StringUtils.isEmpty(rawValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type == String.class) return rawValue;
|
||||
if (type == Integer.class || type == int.class) return Integer.valueOf(rawValue);
|
||||
if (type == Long.class || type == long.class) return Long.valueOf(rawValue);
|
||||
if (type == Double.class || type == double.class) return Double.valueOf(rawValue);
|
||||
if (type == Boolean.class || type == boolean.class) return Boolean.valueOf(rawValue);
|
||||
if (type == Float.class || type == float.class) return Float.valueOf(rawValue);
|
||||
if (type == Short.class || type == short.class) return Short.valueOf(rawValue);
|
||||
if (type == Byte.class || type == byte.class) return Byte.valueOf(rawValue);
|
||||
|
||||
try {
|
||||
if (type == Integer.class || type == int.class) return Integer.valueOf(rawValue);
|
||||
if (type == Long.class || type == long.class) return Long.valueOf(rawValue);
|
||||
if (type == Double.class || type == double.class) return Double.valueOf(rawValue);
|
||||
if (type == Boolean.class || type == boolean.class) return Boolean.valueOf(rawValue);
|
||||
if (type == Float.class || type == float.class) return Float.valueOf(rawValue);
|
||||
if (type == Short.class || type == short.class) return Short.valueOf(rawValue);
|
||||
if (type == Byte.class || type == byte.class) return Byte.valueOf(rawValue);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("Failed to convert value '{}' to type {}, returning null", rawValue, type.getSimpleName());
|
||||
return null;
|
||||
}
|
||||
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd">
|
||||
|
||||
<sqlMap namespace="ObpGwMetric">
|
||||
|
||||
<!-- EAIBZWKDSTCD 코드 목록 조회 (TSEAICM01) -->
|
||||
<statement id="selectAllBzwkCodes" parameterClass="java.util.HashMap" resultClass="java.lang.String">
|
||||
SELECT EAIBZWKDSTCD
|
||||
FROM $schemaId$.TSEAICM01
|
||||
ORDER BY EAIBZWKDSTCD
|
||||
</statement>
|
||||
|
||||
<!-- 로그 집계 결과 매핑 -->
|
||||
<resultMap id="obpGwMetricResult" class="com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricVO">
|
||||
<result property="apiId" column="API_ID"/>
|
||||
<result property="clientId" column="CLIENT_ID"/>
|
||||
<result property="hostname" column="HOSTNAME"/>
|
||||
<result property="orgId" column="ORG_ID"/>
|
||||
<result property="timeslice" column="TIMESLICE"/>
|
||||
<result property="attemptedCount" column="ATTEMPTED_COUNT"/>
|
||||
<result property="completedCount" column="COMPLETED_COUNT"/>
|
||||
</resultMap>
|
||||
|
||||
<!--
|
||||
EAIBZWKDSTCD별 시간대 로그 집계 (Index Range Scan 활용)
|
||||
|
||||
에러 판단 기준:
|
||||
- 성공 조건: RSPNSERRCDNAME IS NULL 또는 RSPNSERRCDNAME LIKE 'ESB%'
|
||||
- 에러 조건: RE%, RF%, REC%, RIB%, 기타 모든 에러 코드
|
||||
|
||||
성능 최적화:
|
||||
- PK 인덱스 (EAIBZWKDSTCD, MSGDPSTYMS, EAISVCSERNO, LOGPRCSSSERNO) 활용
|
||||
- EAIBZWKDSTCD를 고정하여 Index Range Scan 보장
|
||||
-->
|
||||
<statement id="selectHourlyMetricsByBzwkCode" parameterClass="java.util.HashMap" resultMap="obpGwMetricResult">
|
||||
SELECT
|
||||
EAISVCNAME AS API_ID,
|
||||
CLIENTID AS CLIENT_ID,
|
||||
EAISEVRINSTNCNAME AS HOSTNAME,
|
||||
ORGID AS ORG_ID,
|
||||
'$timeslice$' AS TIMESLICE,
|
||||
SUM(CASE WHEN LOGPRCSSSERNO = '100' THEN 1 ELSE 0 END) AS ATTEMPTED_COUNT,
|
||||
SUM(CASE WHEN LOGPRCSSSERNO = '400'
|
||||
AND (RSPNSERRCDNAME IS NULL OR RSPNSERRCDNAME LIKE 'ESB%')
|
||||
THEN 1 ELSE 0 END) AS COMPLETED_COUNT
|
||||
FROM $schemaId$.$tableName$
|
||||
WHERE EAIBZWKDSTCD = #bzwkCode#
|
||||
AND MSGDPSTYMS BETWEEN #startTime# AND #endTime#
|
||||
GROUP BY EAISVCNAME, CLIENTID, EAISEVRINSTNCNAME, ORGID
|
||||
HAVING SUM(CASE WHEN LOGPRCSSSERNO = '100' THEN 1 ELSE 0 END) > 0
|
||||
OR SUM(CASE WHEN LOGPRCSSSERNO = '400' THEN 1 ELSE 0 END) > 0
|
||||
</statement>
|
||||
|
||||
<!-- API 이름 조회 (TSEAIHE01) -->
|
||||
<statement id="selectApiNames" parameterClass="java.util.HashMap" resultClass="java.util.HashMap">
|
||||
SELECT
|
||||
EAISVCNAME AS API_ID,
|
||||
EAISVCDESC AS API_NAME
|
||||
FROM $schemaId$.TSEAIHE01
|
||||
WHERE EAISVCNAME IN
|
||||
<iterate property="apiIds" open="(" close=")" conjunction=",">
|
||||
#apiIds[]#
|
||||
</iterate>
|
||||
</statement>
|
||||
|
||||
<!-- URI 조회 (TSEAIHS04) - APIFULLPATH 사용 -->
|
||||
<statement id="selectUris" parameterClass="java.util.HashMap" resultClass="java.util.HashMap">
|
||||
SELECT
|
||||
EAISVCNAME AS API_ID,
|
||||
APIFULLPATH AS URI
|
||||
FROM $schemaId$.TSEAIHS04
|
||||
WHERE EAISVCNAME IN
|
||||
<iterate property="apiIds" open="(" close=")" conjunction=",">
|
||||
#apiIds[]#
|
||||
</iterate>
|
||||
</statement>
|
||||
|
||||
<!-- AppId 조회 (ptl_app_request) - org_id로 조회 -->
|
||||
<statement id="selectAppIdsByOrgIds" parameterClass="java.util.HashMap" resultClass="java.util.HashMap">
|
||||
SELECT
|
||||
ORG_ID,
|
||||
ID AS APP_ID
|
||||
FROM PTL_APP_REQUEST
|
||||
WHERE ORG_ID IN
|
||||
<iterate property="orgIds" open="(" close=")" conjunction=",">
|
||||
#orgIds[]#
|
||||
</iterate>
|
||||
</statement>
|
||||
|
||||
</sqlMap>
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.eactive.eai.rms.onl.common.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class FileSystemUtilTest {
|
||||
|
||||
@Test
|
||||
@EnabledOnOs(OS.LINUX)
|
||||
void testPathNormalize_Linux() {
|
||||
assertEquals("/asd1/asd2/asd3", FileSystemUtil.pathNormalize("/asd1/asd2", "asd3"));
|
||||
assertEquals("/asd1/asd2/asd3", FileSystemUtil.pathNormalize("/asd1/asd2/", "/asd3"));
|
||||
assertEquals("/asd1/asd2/asd3", FileSystemUtil.pathNormalize("/asd1/asd2", "/asd3"));
|
||||
assertEquals("/asd1/asd2/asd3", FileSystemUtil.pathNormalize("/asd1/asd2/", "asd3/"));
|
||||
assertEquals("/a/b/c", FileSystemUtil.pathNormalize("/a/", "/b/", "/c/"));
|
||||
assertEquals("a/b/c", FileSystemUtil.pathNormalize("a", "b", "c"));
|
||||
assertEquals("/a", FileSystemUtil.pathNormalize("/a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledOnOs(OS.WINDOWS)
|
||||
void testPathNormalize_Windows() {
|
||||
assertEquals("\\asd1\\asd2\\asd3", FileSystemUtil.pathNormalize("/asd1/asd2", "asd3"));
|
||||
assertEquals("\\asd1\\asd2\\asd3", FileSystemUtil.pathNormalize("/asd1/asd2/", "/asd3"));
|
||||
assertEquals("\\asd1\\asd2\\asd3", FileSystemUtil.pathNormalize("/asd1/asd2", "/asd3"));
|
||||
assertEquals("\\asd1\\asd2\\asd3", FileSystemUtil.pathNormalize("/asd1/asd2/", "asd3/"));
|
||||
assertEquals("C:\\Users\\Test", FileSystemUtil.pathNormalize("C:/Users", "Test"));
|
||||
assertEquals("C:\\Users\\Test", FileSystemUtil.pathNormalize("C:\\Users", "Test"));
|
||||
assertEquals("\\a\\b\\c", FileSystemUtil.pathNormalize("/a/", "/b/", "/c/"));
|
||||
assertEquals("a\\b\\c", FileSystemUtil.pathNormalize("a", "b", "c"));
|
||||
assertEquals("\\a", FileSystemUtil.pathNormalize("/a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPathNormalize_General() {
|
||||
String expected = "a" + File.separator + "b" + File.separator + "c";
|
||||
assertEquals(expected, FileSystemUtil.pathNormalize("a", "b", "c"));
|
||||
assertEquals("", FileSystemUtil.pathNormalize(""));
|
||||
assertEquals("", FileSystemUtil.pathNormalize(null));
|
||||
assertEquals("", FileSystemUtil.pathNormalize());
|
||||
assertEquals("a", FileSystemUtil.pathNormalize("a"));
|
||||
assertEquals("a", FileSystemUtil.pathNormalize("a/"));
|
||||
assertEquals("a", FileSystemUtil.pathNormalize("/a"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user