diff --git a/.gitignore b/.gitignore index 5214320..2adda4b 100644 --- a/.gitignore +++ b/.gitignore @@ -243,4 +243,5 @@ gradle-gf63.sh eapim-admin-kjb/ .claude -*.report.md \ No newline at end of file +*.report.md +*.claude.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 1d04763..881e98d 100644 --- a/CLAUDE.md +++ b/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"를 사용합니다. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..60e6004 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,4 @@ +# GEMINI.md + +이 프로젝트에 대한 주요 정보 및 지침은 `CLAUDE.md` 파일을 참조하십시오. +(For key information and guidelines regarding this project, please refer to the `CLAUDE.md` file.) \ No newline at end of file diff --git a/WebContent/WEB-INF/sqlmap-config/oracle/sqlMapConfigOfOnl.xml b/WebContent/WEB-INF/sqlmap-config/oracle/sqlMapConfigOfOnl.xml index 8e843a1..0355c0c 100644 --- a/WebContent/WEB-INF/sqlmap-config/oracle/sqlMapConfigOfOnl.xml +++ b/WebContent/WEB-INF/sqlmap-config/oracle/sqlMapConfigOfOnl.xml @@ -201,4 +201,7 @@ + + + \ No newline at end of file diff --git a/WebContent/jsp/common/include/script.jsp b/WebContent/jsp/common/include/script.jsp index 80ed3ec..873de95 100644 --- a/WebContent/jsp/common/include/script.jsp +++ b/WebContent/jsp/common/include/script.jsp @@ -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 = + '
' + + '
' + + '
' + + '' + iconHtml + '' + + '' + title + '' + + '
' + + '
' + + '

' + message + '

' + + '
' + + '' + + '
' + + '
'; + + $('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 'error'; + case 'warning': return 'warning'; + case 'success': return 'check_circle'; + default: return 'info'; + } + } + + /** + * 스타일링된 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 = + '
' + + '
' + + '
' + + '' + iconHtml + '' + + '' + title + '' + + '
' + + '
' + + '

' + message + '

' + + '
' + + '' + + '
' + + '
'; + + $('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); + } + + + diff --git a/WebContent/jsp/onl/admin/inflow/fragments/inflowGroupDetailStyle.jsp b/WebContent/jsp/onl/admin/inflow/fragments/inflowGroupDetailStyle.jsp new file mode 100644 index 0000000..ef69886 --- /dev/null +++ b/WebContent/jsp/onl/admin/inflow/fragments/inflowGroupDetailStyle.jsp @@ -0,0 +1,191 @@ +<%@ page language="java" contentType="text/html; charset=utf-8"%> + + diff --git a/WebContent/jsp/onl/admin/inflow/fragments/inflowGroupHelpModal.jsp b/WebContent/jsp/onl/admin/inflow/fragments/inflowGroupHelpModal.jsp new file mode 100644 index 0000000..e7415b3 --- /dev/null +++ b/WebContent/jsp/onl/admin/inflow/fragments/inflowGroupHelpModal.jsp @@ -0,0 +1,89 @@ +<%@ page language="java" contentType="text/html; charset=utf-8"%> + +
+
+
+

유량제어 임계치 설명

+ +
+
+

개요

+

유량제어는 Token Bucket 알고리즘 기반으로 동작합니다. + 설정된 임계치만큼 토큰이 균등하게 리필되며, 요청 1건당 토큰 1개를 소비합니다. + 토큰이 부족하면 요청이 거부됩니다.

+ +

토큰 리필 방식 - Greedy (균등 리필)

+

토큰은 설정된 시간이 지난 후 한꺼번에 리필되는 것이 아니라, 매 순간 균등하게 리필됩니다.

+
+ 왜 균등 리필인가?
+ 한꺼번에 리필하면 리필 직후 트래픽이 몰려 백엔드에 부하가 집중됩니다. + 균등 리필은 트래픽을 시간축에 고르게 분산시켜 시스템 안정성을 높입니다. + AWS, Google Cloud, Nginx 등 대부분의 Rate Limiter가 이 방식을 채택합니다. +
+
+
예시: 분당 120건 설정 시 리필 속도
+
    +
  • 120건 ÷ 60초 = 초당 2건씩 균등 리필
  • +
  • 1초마다 2개의 토큰이 자동으로 채워짐
  • +
+
+
+
동작 예시: 분당 120건, 10건 요청 처리 후
+ + + + + + + + + +
시간상황토큰 수
10:00:00초기 상태 (만땅)120개
10:00:01요청 10건 처리110개
10:00:021초 경과 → 2개 리필112개
10:00:031초 경과 → 2개 리필114개
10:00:041초 경과 → 2개 리필116개
10:00:051초 경과 → 2개 리필118개
10:00:061초 경과 → 2개 리필 (복구 완료)120개
+

+ ※ 10건 소비 후 5초만에 복구됨 (10건 ÷ 초당2건 = 5초) +

+
+
+ 주의: "분당 120건"은 "1분 후에 120개가 한꺼번에 채워진다"는 의미가 아닙니다. + 매 초마다 2개씩 균등하게 리필되어, 결과적으로 1분 동안 최대 120건을 처리할 수 있다는 의미입니다. +
+ +

초당 임계치 (thresholdPerSecond)

+

1초 단위의 순간적인 트래픽 급증(spike)을 방어합니다.

+
+
예시: 초당 임계치 = 100
+
    +
  • 매 1초마다 토큰 100개 리필
  • +
  • 1초 내 100건까지 처리 가능
  • +
  • 101번째 요청부터 차단
  • +
+
+ +

추가 임계치 (threshold) + 시간단위

+

장시간 누적 트래픽을 제어합니다. 시간단위에 따라 리필 속도가 결정됩니다.

+ + + + + + +
시간단위설명리필 속도 예시 (120건 설정 시)
MIN초당 2건 (120÷60)
HOU초당 0.033건 (120÷3600)
DAY초당 0.00139건 (120÷86400)
MON초당 0.0000463건 (120÷2592000)
+ +

두 임계치 조합 (권장)

+

두 조건을 함께 설정하면 이중 보호가 적용됩니다.

+
+
예시: 초당 100건 + 시간당 50,000건
+
    +
  • 초당 임계치: 순간 폭주 방지 → 1초에 101건 이상 요청 시 차단
  • +
  • 추가 임계치: 장기 누적 제한 → 시간당 50,001건 이상 요청 시 차단
  • +
  • 두 조건 중 하나라도 초과 시 차단 (둘 다 충족해야 통과)
  • +
+
+ +
+ 참고: 그룹에 매핑된 인터페이스들은 개별 유량제어 대신 그룹 유량제어가 적용됩니다. + 그룹 내 모든 인터페이스의 호출이 합산되어 임계치와 비교됩니다. +
+
+
+
diff --git a/WebContent/jsp/onl/admin/inflow/fragments/inflowGroupInterfaceModal.jsp b/WebContent/jsp/onl/admin/inflow/fragments/inflowGroupInterfaceModal.jsp new file mode 100644 index 0000000..98aea33 --- /dev/null +++ b/WebContent/jsp/onl/admin/inflow/fragments/inflowGroupInterfaceModal.jsp @@ -0,0 +1,41 @@ +<%@ page language="java" contentType="text/html; charset=utf-8"%> + +
+
+
+

API 선택

+ +
+
+ +
+ + + + + + + + + + +
API ID설명
검색 결과가 없습니다
+
+
+ 0 +
+ +
+ +
+
+
+ +
+
diff --git a/WebContent/jsp/onl/admin/inflow/inflowGroupControlMan.jsp b/WebContent/jsp/onl/admin/inflow/inflowGroupControlMan.jsp new file mode 100644 index 0000000..04bf391 --- /dev/null +++ b/WebContent/jsp/onl/admin/inflow/inflowGroupControlMan.jsp @@ -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"); + +%> + + + + + + + + + + +
+
+ +
+
+
+ + +
+
유량제어 그룹 관리유량제어 그룹 관리
+ + + + + + + + +
그룹명
+ +
+
+ +
+
+ + diff --git a/WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp b/WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp new file mode 100644 index 0000000..ea2ef28 --- /dev/null +++ b/WebContent/jsp/onl/admin/inflow/inflowGroupControlManDetail.jsp @@ -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"); +%> + + + + + + + + + + + + + + +
+
+ +
+
+
+ + + + +
+
+
유량제어 그룹 관리유량제어 그룹 관리
+ +
+ + +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
그룹명 *
사용여부 +
+ + 사용 +
+
+ + + + + + + + + + + + + + + + + + +
+ 초당 임계치 + help_outline + 1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다. + + +
+ + req/sec +
+
+ 추가 임계치 + help_outline + 장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다. + + +
+ + + requests +
+
+ + + +
+ 매핑된 API 0개 (활성 0) + +
+
+ + + + + + + + + + + + + + + + + + +
API ID설명관리
inbox 매핑된 API가 없습니다
+
+
+
+ + +
+
+
+ 마지막 갱신: - + +
+ + + + +
+
+ hourglass_empty + 버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요 +
+
+
+
+ +
+
+ + + + + + + + diff --git a/build.gradle b/build.gradle index 261119d..2051e54 100644 --- a/build.gradle +++ b/build.gradle @@ -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"] } diff --git a/kjb-docs/GwMetricHourJob.md b/kjb-docs/GwMetricHourJob.md new file mode 100644 index 0000000..efce18f --- /dev/null +++ b/kjb-docs/GwMetricHourJob.md @@ -0,0 +1,5 @@ +# ObpGwMetricHourJob +- Job명 : ObpGwMetricHourJob +- 설명 : GW 시간당 거래 지표 - OBP 과금기초데이터 +- com.eactive.eai.rms.onl.common.service.ObpGwMetricHourJob +- aggregation.hour.range : 집계 범위(최대 24로 제한) \ No newline at end of file diff --git a/readme.md b/readme.md index 684f7b1..7ea3719 100644 --- a/readme.md +++ b/readme.md @@ -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) +``` \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/rms/data/entity/man/monitoringProperty/service/MonitoringPropertyService.java b/src/main/java/com/eactive/eai/rms/data/entity/man/monitoringProperty/service/MonitoringPropertyService.java index bbd3e0d..6ce6042 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/man/monitoringProperty/service/MonitoringPropertyService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/man/monitoringProperty/service/MonitoringPropertyService.java @@ -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 { public String findPrpty2ValById(String prptyGroupName, String key) { MonitoringPropertyId id = new MonitoringPropertyId(); id.setPrptyGroupName(prptyGroupName); - id.setPrptyGroupName(key); + id.setPrptyName(key); Optional property = repository.findById(id); return property.map(MonitoringProperty::getPrpty2Val).orElse(null); @@ -41,12 +43,16 @@ public class MonitoringPropertyService id.setPrptyName(prptyName); Optional 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 findAllByIdPrptyName(String prptyName) { diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricDAO.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricDAO.java new file mode 100644 index 0000000..ca3d661 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricDAO.java @@ -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 selectAllBzwkCodes(String schemaId) { + HashMap paramMap = new HashMap<>(); + paramMap.put("schemaId", schemaId); + return this.template.queryForList("ObpGwMetric.selectAllBzwkCodes", paramMap); + } + + /** + * EAIBZWKDSTCD별 시간대 로그 집계 + */ + @SuppressWarnings("unchecked") + public List selectHourlyMetricsByBzwkCode(HashMap paramMap) { + return this.template.queryForList("ObpGwMetric.selectHourlyMetricsByBzwkCode", paramMap); + } + + /** + * API 이름 조회 (TSEAIHE01) + */ + @SuppressWarnings("unchecked") + public Map selectApiNames(String schemaId, Set apiIds) { + if (apiIds == null || apiIds.isEmpty()) { + return new HashMap<>(); + } + + HashMap paramMap = new HashMap<>(); + paramMap.put("schemaId", schemaId); + paramMap.put("apiIds", new ArrayList<>(apiIds)); // Set → List 변환 (iBATIS iterate 호환) + + List> resultList = this.template.queryForList("ObpGwMetric.selectApiNames", paramMap); + + Map result = new HashMap<>(); + for (Map 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 selectUris(String schemaId, Set apiIds) { + if (apiIds == null || apiIds.isEmpty()) { + return new HashMap<>(); + } + + HashMap paramMap = new HashMap<>(); + paramMap.put("schemaId", schemaId); + paramMap.put("apiIds", new ArrayList<>(apiIds)); // Set → List 변환 (iBATIS iterate 호환) + + List> resultList = this.template.queryForList("ObpGwMetric.selectUris", paramMap); + + Map result = new HashMap<>(); + for (Map 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 selectAppIdsByOrgIds(Set orgIds) { + if (orgIds == null || orgIds.isEmpty()) { + return new HashMap<>(); + } + + HashMap paramMap = new HashMap<>(); + paramMap.put("orgIds", new ArrayList<>(orgIds)); // Set → List 변환 (iBATIS iterate 호환) + + List> resultList = this.template.queryForList("ObpGwMetric.selectAppIdsByOrgIds", paramMap); + + Map result = new HashMap<>(); + for (Map 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; + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricRepository.java new file mode 100644 index 0000000..58c5031 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricRepository.java @@ -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 { + + /** + * UPSERT 판단을 위한 기존 데이터 조회 + * 동일한 timeslice + apiId + clientId + hostname + orgId 조합이 존재하는지 확인 + */ + Optional findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId( + LocalDateTime timeslice, + String apiId, + String clientId, + String hostname, + String orgId + ); + + /** + * 특정 시간대의 모든 GwMetric 데이터 조회 + */ + List findByTimeslice(LocalDateTime timeslice); + + /** + * 특정 시간 범위의 GwMetric 데이터 조회 + */ + List findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime); +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricService.java new file mode 100644 index 0000000..be973ab --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricService.java @@ -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 findByUniqueKey(LocalDateTime timeslice, String apiId, String clientId, String hostname, String orgId) { + return repository.findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId( + timeslice, apiId, clientId, hostname, orgId); + } + + /** + * 특정 시간대의 모든 GwMetric 데이터 조회 + */ + public List findByTimeslice(LocalDateTime timeslice) { + return repository.findByTimeslice(timeslice); + } + + /** + * 특정 시간 범위의 GwMetric 데이터 조회 + */ + public List findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime) { + return repository.findByTimesliceBetween(startTime, endTime); + } + + /** + * 엔티티 저장 (INSERT 또는 UPDATE) + */ + public ObpGwMetric save(ObpGwMetric entity) { + return repository.save(entity); + } + + /** + * 엔티티 목록 저장 + */ + public List saveAll(List entities) { + return repository.saveAll(entities); + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricVO.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricVO.java new file mode 100644 index 0000000..db55110 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricVO.java @@ -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 : ""; + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupMappingRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupMappingRepository.java new file mode 100644 index 0000000..618a1e2 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupMappingRepository.java @@ -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 { + + List findByGroupId(String groupId); + + @Modifying + @Query("DELETE FROM InflowControlGroupMapping m WHERE m.groupId = :groupId") + void deleteByGroupId(@Param("groupId") String groupId); + + /** + * 인터페이스 ID로 매핑 조회 (중복 체크용) + */ + Optional findByInterfaceId(String interfaceId); +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupRepository.java new file mode 100644 index 0000000..d267d5c --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/inflow/InflowControlGroupRepository.java @@ -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 { + +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/approval/credential/CredentialUIMapper.java b/src/main/java/com/eactive/eai/rms/onl/apim/approval/credential/CredentialUIMapper.java index e3c7ff8..208dfa8 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/approval/credential/CredentialUIMapper.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/approval/credential/CredentialUIMapper.java @@ -28,7 +28,7 @@ public abstract class CredentialUIMapper implements GenericMapper세션 체크를 우회하기 위해 InterceptorSkipController 인터페이스 구현

+ */ +@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 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)); + } + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricDTO.java b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricDTO.java new file mode 100644 index 0000000..02e8785 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricDTO.java @@ -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); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricManService.java b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricManService.java new file mode 100644 index 0000000..f4a458f --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricManService.java @@ -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시간 단위로 집계 + * + *

Job 파라미터로 오버라이드 가능 (aggregation.hour.range)

+ */ + 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 bzwkCodes = obpGwMetricDAO.selectAllBzwkCodes(apigwDataSourceType.getSchema()); + log.info("[1단계] EAIBZWKDSTCD 코드 조회 완료: count={}, elapsed={}ms", + bzwkCodes.size(), System.currentTimeMillis() - step1Start); + + // ═══════════════════════════════════════════════════════════════════ + // 2단계: 코드별 개별 쿼리 실행 후 결과 병합 + // ═══════════════════════════════════════════════════════════════════ + long step2Start = System.currentTimeMillis(); + Map aggregatedMap = new HashMap<>(); + int totalLogCount = 0; + + for (String bzwkCode : bzwkCodes) { + long codeStart = System.currentTimeMillis(); + + HashMap 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 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 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 apiIds = aggregatedList.stream() + .map(ObpGwMetricVO::getApiId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Map apiNameMap = obpGwMetricDAO.selectApiNames(apigwDataSourceType.getSchema(), apiIds); + Map 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 orgIds = aggregatedList.stream() + .map(ObpGwMetricVO::getOrgId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map appIdMap = obpGwMetricDAO.selectAppIdsByOrgIds(orgIds); + + int insertCount = 0; + int updateCount = 0; + + for (ObpGwMetricVO vo : aggregatedList) { + LocalDateTime timesliceDateTime = vo.getTimesliceAsLocalDateTime(); + + Optional 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 apiNameMap, + Map uriMap, Map 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 findByTimeslice(LocalDateTime timeslice) { + DataSourceContextHolder.setDataSourceType( + DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.MONITORING)); + + List entities = obpGwMetricService.findByTimeslice(timeslice); + return entities.stream() + .map(ObpGwMetricDTO::from) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/audit/adapter/map/AuditUIMapper.java b/src/main/java/com/eactive/eai/rms/onl/audit/adapter/map/AuditUIMapper.java index fcb681d..282bfbd 100644 --- a/src/main/java/com/eactive/eai/rms/onl/audit/adapter/map/AuditUIMapper.java +++ b/src/main/java/com/eactive/eai/rms/onl/audit/adapter/map/AuditUIMapper.java @@ -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); diff --git a/src/main/java/com/eactive/eai/rms/onl/common/service/ObpGwMetricHourJob.java b/src/main/java/com/eactive/eai/rms/onl/common/service/ObpGwMetricHourJob.java new file mode 100644 index 0000000..7b93297 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/common/service/ObpGwMetricHourJob.java @@ -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 테이블에 저장 + * + *

Cron Schedule 권장:

+ *
    + *
  • 기본: 0 5 * * * ? (매시 05분 1회 실행)
  • + *
  • 보정 (15분): 0 5,20,35,50 * * * ? (15분 간격 4회 실행)
  • + *
  • 보정 (10분): 0 5,15,25,35,45,55 * * * ? (10분 간격 6회 실행)
  • + *
+ * + *

Job 파라미터 (JobDataMap):

+ *
    + *
  • aggregation.hour.range: 집계 범위 시간 (단위: 시간, 기본값: 1) + *
      + *
    • 1: 직전 1시간만 집계
    • + *
    • 3: 직전 3시간 각각 집계
    • + *
    • 5: 직전 5시간 각각 집계
    • + *
    + *
  • + *
+ * + *

예시 (Quartz 스케줄 등록 시):

+ *
+ * JobDataMap jobDataMap = new JobDataMap();
+ * jobDataMap.put("aggregation.hour.range", "3");
+ * 
+ */ +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); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/common/service/TransactionReportHourJob.java b/src/main/java/com/eactive/eai/rms/onl/common/service/TransactionReportHourJob.java index 72e81a3..83fd045 100644 --- a/src/main/java/com/eactive/eai/rms/onl/common/service/TransactionReportHourJob.java +++ b/src/main/java/com/eactive/eai/rms/onl/common/service/TransactionReportHourJob.java @@ -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" + ); /////////////////////////////////////////////// // 로컬테스트용 diff --git a/src/main/java/com/eactive/eai/rms/onl/common/service/ums/UmsDispatchService.java b/src/main/java/com/eactive/eai/rms/onl/common/service/ums/UmsDispatchService.java index 5211520..4a42c9c 100644 --- a/src/main/java/com/eactive/eai/rms/onl/common/service/ums/UmsDispatchService.java +++ b/src/main/java/com/eactive/eai/rms/onl/common/service/ums/UmsDispatchService.java @@ -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 pendingMessages = entityManager.createQuery( "SELECT m FROM MessageRequest m WHERE m.requestStatus = :status " + "ORDER BY m.requestDate ASC") diff --git a/src/main/java/com/eactive/eai/rms/onl/common/util/FileSystemUtil.java b/src/main/java/com/eactive/eai/rms/onl/common/util/FileSystemUtil.java new file mode 100644 index 0000000..7026ed7 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/common/util/FileSystemUtil.java @@ -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. + * + *

Examples for Linux/macOS:

+ *
    + *
  • {@code pathNormalize("/asd1/asd2", "asd3")} returns {@code "/asd1/asd2/asd3"}
  • + *
  • {@code pathNormalize("/asd1/asd2/", "/asd3")} returns {@code "/asd1/asd2/asd3"}
  • + *
+ * + *

Examples for Windows:

+ *
    + *
  • {@code pathNormalize("C:/Users/Test", "Documents")} returns {@code "C:\\Users\\Test\\Documents"}
  • + *
  • {@code pathNormalize("C:/Users/Test/", "/Documents")} returns {@code "C:\\Users\\Test\\Documents"}
  • + *
+ * + * @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("\\", "/"); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManController.java b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManController.java new file mode 100644 index 0000000..ab4fe96 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManController.java @@ -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> selectList(HttpServletRequest request, + Pageable pageVo, String searchGroupName) { + Page uiPage = service.selectGroupList(pageVo, searchGroupName); + return ResponseEntity.ok(new GridResponse<>(uiPage)); + } + + /** + * 그룹 상세 조회 + */ + @RequestMapping(value = "/onl/admin/inflow/inflowGroupControlMan.json", params = "cmd=DETAIL") + public ResponseEntity 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 useYnRows = comboService.getFromCode("USE_YN"); + List timeUnitRows = comboService.getFromCode("INFLOW_CTRL_TIME_UNIT"); + + Map> 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> selectInterfaceList(HttpServletRequest request, + Pageable pageVo, String searchName) { + Page 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 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> statusList = service.getGroupBucketStatus(groupId); + + Map 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 interfaceList = objectMapper.readValue(interfaceListJson, + new TypeReference>() { + }); + ui.setInterfaceList(interfaceList); + } + } + + /** + * 세션에서 사용자 ID 조회 + */ + private String getSessionUserId(HttpServletRequest request) { + Object userId = request.getSession().getAttribute("userId"); + return userId != null ? userId.toString() : "SYSTEM"; + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManMapper.java b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManMapper.java new file mode 100644 index 0000000..83cf138 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManMapper.java @@ -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); +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManService.java b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManService.java new file mode 100644 index 0000000..29f7b34 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManService.java @@ -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 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 groupList = jpaQueryFactory + .selectFrom(qGroup) + .where(boolBuilder) + .orderBy(qGroup.groupId.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + List 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 groupOpt = groupRepository.findById(groupId); + if (!groupOpt.isPresent()) { + return null; + } + + InflowGroupControlManUI ui = mapper.toVo(groupOpt.get()); + + // 매핑된 인터페이스 목록 조회 (인터페이스 설명 포함) + List mappingList = selectMappingListWithDesc(groupId); + ui.setInterfaceList(mappingList); + + return ui; + } + + /** + * 그룹에 매핑된 인터페이스 목록 조회 (인터페이스 설명 포함) + */ + private List selectMappingListWithDesc(String groupId) { + JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager); + QInflowControlGroupMapping qMapping = QInflowControlGroupMapping.inflowControlGroupMapping; + QEAIMessageEntity qMessage = QEAIMessageEntity.eAIMessageEntity; + + List 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 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 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 mappingOpt = mappingRepository.findByInterfaceId(interfaceId); + if (!mappingOpt.isPresent()) { + return null; + } + + InflowControlGroupMapping mapping = mappingOpt.get(); + String existingGroupId = mapping.getGroupId(); + + // 현재 그룹과 동일하면 중복 아님 (수정 시) + if (existingGroupId.equals(currentGroupId)) { + return null; + } + + // 그룹명 조회 + Optional groupOpt = groupRepository.findById(existingGroupId); + if (groupOpt.isPresent()) { + return groupOpt.get().getGroupName(); + } + return existingGroupId; // 그룹명 없으면 ID 반환 + } + + /** + * 인터페이스 목록 조회 (팝업용) + */ + public Page 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 tupleList = jpaQueryFactory + .select(qMessage.eaisvcname, qMessage.eaisvcdesc) + .from(qMessage) + .where(boolBuilder) + .orderBy(qMessage.eaisvcname.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + List 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> getGroupBucketStatus(String groupId) { + List> result = new ArrayList<>(); + List> serverList = eaiServerInfoService.getEaiServerIpList(); + + for (Map 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 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 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; + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManUI.java b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManUI.java new file mode 100644 index 0000000..2dac454 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupControlManUI.java @@ -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 interfaceList; +} diff --git a/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupMappingUI.java b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupMappingUI.java new file mode 100644 index 0000000..1fdc8a4 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/manage/inflow/group/InflowGroupMappingUI.java @@ -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; +} diff --git a/src/main/java/com/eactive/ext/kjb/util/KjbPropertyInjector.java b/src/main/java/com/eactive/ext/kjb/util/KjbPropertyInjector.java index 8b4ac6c..dd2c738 100644 --- a/src/main/java/com/eactive/ext/kjb/util/KjbPropertyInjector.java +++ b/src/main/java/com/eactive/ext/kjb/util/KjbPropertyInjector.java @@ -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 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; } diff --git a/src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml b/src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml new file mode 100644 index 0000000..ab42299 --- /dev/null +++ b/src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml @@ -0,0 +1,90 @@ + + + + + + + + SELECT EAIBZWKDSTCD + FROM $schemaId$.TSEAICM01 + ORDER BY EAIBZWKDSTCD + + + + + + + + + + + + + + + + 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 + + + + + SELECT + EAISVCNAME AS API_ID, + EAISVCDESC AS API_NAME + FROM $schemaId$.TSEAIHE01 + WHERE EAISVCNAME IN + + #apiIds[]# + + + + + + SELECT + EAISVCNAME AS API_ID, + APIFULLPATH AS URI + FROM $schemaId$.TSEAIHS04 + WHERE EAISVCNAME IN + + #apiIds[]# + + + + + + SELECT + ORG_ID, + ID AS APP_ID + FROM PTL_APP_REQUEST + WHERE ORG_ID IN + + #orgIds[]# + + + + diff --git a/src/test/java/com/eactive/eai/rms/onl/common/util/FileSystemUtilTest.java b/src/test/java/com/eactive/eai/rms/onl/common/util/FileSystemUtilTest.java new file mode 100644 index 0000000..0d2e19b --- /dev/null +++ b/src/test/java/com/eactive/eai/rms/onl/common/util/FileSystemUtilTest.java @@ -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")); + } +}