From 974830846bd031e403e003e8b02cba768318c0d4 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Fri, 5 Dec 2025 10:39:20 +0900 Subject: [PATCH 01/15] =?UTF-8?q?OBP=20=EA=B3=BC=EA=B8=88=EC=9A=A9=20gw=20?= =?UTF-8?q?metric=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- CLAUDE.md | 51 ++++++++++++- GEMINI.md | 4 + build.gradle | 11 ++- .../credential/CredentialUIMapper.java | 2 +- .../onl/audit/adapter/map/AuditUIMapper.java | 2 +- .../service/TransactionReportHourJob.java | 11 ++- .../rms/onl/common/util/FileSystemUtil.java | 73 +++++++++++++++++++ .../apim/mapping/ApiInterfaceUIMapper.java | 2 +- .../onl/common/util/FileSystemUtilTest.java | 50 +++++++++++++ 10 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 GEMINI.md create mode 100644 src/main/java/com/eactive/eai/rms/onl/common/util/FileSystemUtil.java create mode 100644 src/test/java/com/eactive/eai/rms/onl/common/util/FileSystemUtilTest.java 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..f99420b 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에서 엔티티를 찾을 수 없다고 표시되면, `gradle compileJava`를 먼저 실행하여 코드를 생성해야 합니다. +- **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다. ## 프로젝트 개요 @@ -69,6 +80,39 @@ gradle dependencies gradle tasks --all ``` +## 로컬 개발 빠른 시작 + +새로운 환경에서 프로젝트를 처음 설정하고 실행하는 권장 절차입니다. + +1. **IDE 설정 파일 생성 (최초 1회)** + ```bash + # IntelliJ IDEA 사용 시 + gradle idea + + # Eclipse 사용 시 + gradle eclipse + ``` + +2. **QueryDSL 등 소스 코드 생성** + ```bash + gradle compileJava + ``` + +3. **전체 빌드 및 테스트 실행** + ```bash + gradle build + ``` + +4. **IDE에서 프로젝트 열기** + - `gradle idea` 또는 `gradle eclipse` 실행 후, IDE에서 프로젝트를 엽니다. + +5. **실행 구성(Run Configuration) 설정** + - 사용하는 WAS(Tomcat 등)에 `eapim-admin` 프로젝트를 배포하도록 설정합니다. + - **VM Options**에 위에 명시된 "필수 환경 변수 (Tomcat)"를 모두 추가합니다. + +6. **서버 실행** + - IDE에서 설정한 실행 구성을 시작하여 서버를 실행합니다. + ## 아키텍처 및 모듈 의존성 ### 멀티 모듈 구조 @@ -132,6 +176,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 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/build.gradle b/build.gradle index 77d3530..39c105a 100644 --- a/build.gradle +++ b/build.gradle @@ -36,10 +36,17 @@ java { } } +sourceSets { + main { + java { + srcDirs generatedJavaDir + } + } +} + compileJava { options.encoding = 'UTF-8' options.compilerArgs = ['-parameters'] - sourceSets.main.java { srcDir generatedJavaDir } options.generatedSourceOutputDirectory = project.file(generatedJavaDir) aptOptions { @@ -64,7 +71,7 @@ war { tasks.withType(JavaCompile) { options.encoding = 'UTF-8' - options.compilerArgs = ['-parameters'] + options.compilerArgs += ['-parameters', "-Xlint:unchecked", "-Xlint:deprecation"] } 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 -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/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:

+ * + * + *

Examples for Windows:

+ * + * + * @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/transaction/apim/mapping/ApiInterfaceUIMapper.java b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/mapping/ApiInterfaceUIMapper.java index c933b63..8d4c762 100644 --- a/src/main/java/com/eactive/eai/rms/onl/transaction/apim/mapping/ApiInterfaceUIMapper.java +++ b/src/main/java/com/eactive/eai/rms/onl/transaction/apim/mapping/ApiInterfaceUIMapper.java @@ -148,7 +148,7 @@ public interface ApiInterfaceUIMapper { @Mapping(target = "eaisendrecv", source = "sendRecvType") @Mapping(target = "eaidirection", source = "inOutType") @Mapping(target = "standardMessageItems", source = "standardMessageItems") - @Mapping(target = "apiFullPath", source = "apiFullPath") + StandardMessageInfo toStandardMessageInfo(ApiInterfaceUI vo); @AfterMapping 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")); + } +} From 6e1e077ac6a8797225315d1abbeae1ab56d405dd Mon Sep 17 00:00:00 2001 From: Rinjae Date: Fri, 5 Dec 2025 17:28:47 +0900 Subject: [PATCH 02/15] claude --- CLAUDE.md | 53 +++++++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f99420b..881e98d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ AI 어시스턴트가 작업을 시작하기 전에 반드시 이해해야 할 - **신규 기능**: 항상 **JPA**와 Spring Data 리포지토리를 사용해야 합니다. - **레거시 코드**: 기존 iBATIS (`*.xml` 매퍼) 코드를 수정할 때는 해당 패턴을 따라야 합니다. - **멀티 모듈 Gradle 프로젝트**: `settings.gradle`에 정의된 것처럼 여러 하위 모듈(`elink-online-*`, `kjb-*` 등)로 구성되어 있습니다. 특정 기능은 다른 모듈에 위치할 수 있습니다. -- **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `gradle compileJava`를 먼저 실행하여 코드를 생성해야 합니다. +- **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `kjb-gradle.sh compileJava`를 먼저 실행하여 코드를 생성해야 합니다. - **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다. ## 프로젝트 개요 @@ -38,46 +38,51 @@ 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 ``` ## 로컬 개발 빠른 시작 @@ -87,24 +92,24 @@ gradle tasks --all 1. **IDE 설정 파일 생성 (최초 1회)** ```bash # IntelliJ IDEA 사용 시 - gradle idea - + kjb-gradle.sh idea + # Eclipse 사용 시 - gradle eclipse + kjb-gradle.sh eclipse ``` 2. **QueryDSL 등 소스 코드 생성** ```bash - gradle compileJava + kjb-gradle.sh compileJava ``` 3. **전체 빌드 및 테스트 실행** ```bash - gradle build + kjb-gradle.sh build ``` 4. **IDE에서 프로젝트 열기** - - `gradle idea` 또는 `gradle eclipse` 실행 후, IDE에서 프로젝트를 엽니다. + - `kjb-gradle.sh idea` 또는 `kjb-gradle.sh eclipse` 실행 후, IDE에서 프로젝트를 엽니다. 5. **실행 구성(Run Configuration) 설정** - 사용하는 WAS(Tomcat 등)에 `eapim-admin` 프로젝트를 배포하도록 설정합니다. @@ -155,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`에 포함되어 있으며 커밋해서는 안 됩니다. @@ -164,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를 생성합니다 @@ -249,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 ``` ## 기술 스택 @@ -326,7 +331,7 @@ com.eactive.eai ### Eclipse ```bash -gradle eclipse +kjb-gradle.sh eclipse ``` 다음을 생성합니다: @@ -338,7 +343,7 @@ gradle eclipse ### IntelliJ IDEA ```bash -gradle idea +kjb-gradle.sh idea ``` 또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다. From cfeda1b7af75c6e169b7e4d3017d10973cac18a7 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Fri, 5 Dec 2025 17:29:30 +0900 Subject: [PATCH 03/15] gw metric --- .../oracle/sqlMapConfigOfOnl.xml | 3 + .../entity/onl/apim/obp/ObpGwMetricDAO.java | 103 +++++++ .../onl/apim/obp/ObpGwMetricRepository.java | 39 +++ .../onl/apim/obp/ObpGwMetricService.java | 54 ++++ .../entity/onl/apim/obp/ObpGwMetricVO.java | 68 +++++ .../onl/apim/obp/ObpGwMetricController.java | 97 +++++++ .../eai/rms/onl/apim/obp/ObpGwMetricDTO.java | 98 +++++++ .../onl/apim/obp/ObpGwMetricManService.java | 257 ++++++++++++++++++ .../common/service/ObpGwMetricHourJob.java | 56 ++++ .../eai/apim/obp/ObpGwMetric-oracle.xml | 91 +++++++ 10 files changed, 866 insertions(+) create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricDAO.java create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricRepository.java create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricService.java create mode 100644 src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricVO.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricController.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricDTO.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricManService.java create mode 100644 src/main/java/com/eactive/eai/rms/onl/common/service/ObpGwMetricHourJob.java create mode 100644 src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml 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/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..b61fbac --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricDAO.java @@ -0,0 +1,103 @@ +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.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", apiIds); + + List> resultList = this.template.queryForList("ObpGwMetric.selectApiNames", paramMap); + + return resultList.stream() + .collect(Collectors.toMap( + m -> m.get("API_ID"), + m -> m.get("API_NAME"), + (v1, v2) -> v1 + )); + } + + /** + * 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", apiIds); + + List> resultList = this.template.queryForList("ObpGwMetric.selectUris", paramMap); + + return resultList.stream() + .collect(Collectors.toMap( + m -> m.get("API_ID"), + m -> m.get("URI"), + (v1, v2) -> v1 + )); + } + + /** + * 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", orgIds); + + List> resultList = this.template.queryForList("ObpGwMetric.selectAppIdsByOrgIds", paramMap); + + return resultList.stream() + .collect(Collectors.toMap( + m -> m.get("ORG_ID"), + m -> m.get("APP_ID"), + (v1, v2) -> v1 + )); + } +} 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..79d3022 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricService.java @@ -0,0 +1,54 @@ +package com.eactive.eai.rms.data.entity.onl.apim.obp; + +import com.eactive.apim.portal.obp.entity.ObpGwMetric; +import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce; +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 extends AbstractEMSDataSerivce { + + /** + * UPSERT 판단을 위한 기존 데이터 조회 + */ + public Optional findByUniqueKey(LocalDateTime timeslice, String apiId, String clientId, String hostname, String orgId) { + return getRepository().findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId( + timeslice, apiId, clientId, hostname, orgId); + } + + /** + * 특정 시간대의 모든 GwMetric 데이터 조회 + */ + public List findByTimeslice(LocalDateTime timeslice) { + return getRepository().findByTimeslice(timeslice); + } + + /** + * 특정 시간 범위의 GwMetric 데이터 조회 + */ + public List findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime) { + return getRepository().findByTimesliceBetween(startTime, endTime); + } + + /** + * 엔티티 저장 (INSERT 또는 UPDATE) + */ + public ObpGwMetric save(ObpGwMetric entity) { + return getRepository().save(entity); + } + + /** + * 엔티티 목록 저장 + */ + public List saveAll(List entities) { + return getRepository().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..20adc9a --- /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/onl/apim/obp/ObpGwMetricController.java b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricController.java new file mode 100644 index 0000000..fb3168b --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricController.java @@ -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 + * + *

세션 체크를 우회하기 위해 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..8543dec --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/obp/ObpGwMetricManService.java @@ -0,0 +1,257 @@ +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시간 단위로 집계 + */ + private static final int 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() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime baseHour = now.truncatedTo(ChronoUnit.HOURS); // 현재 시간의 정각 + + log.info("========== ObpGwMetric 집계 시작 =========="); + log.info("실행 시간: {}, AGGREGATION_HOUR_RANGE: {}", now, AGGREGATION_HOUR_RANGE); + + for (int i = 1; i <= AGGREGATION_HOUR_RANGE; i++) { + LocalDateTime targetHour = baseHour.minusHours(i); + log.info("집계 대상 시간대: {}", targetHour); + createHourlyData(targetHour); + } + + log.info("========== ObpGwMetric 집계 완료 =========="); + } + + /** + * 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); // 보정 횟수 증가 + 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); + 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/common/service/ObpGwMetricHourJob.java b/src/main/java/com/eactive/eai/rms/onl/common/service/ObpGwMetricHourJob.java new file mode 100644 index 0000000..35befd3 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/common/service/ObpGwMetricHourJob.java @@ -0,0 +1,56 @@ +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerException; +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회 실행)
  • + *
+ */ +public class ObpGwMetricHourJob implements Job { + + private static final Logger log = LoggerFactory.getLogger(ObpGwMetricHourJob.class); + + private transient MonitoringContext monitoringContext; + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + log.info("*** START ObpGwMetricHourJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm")); + + ApplicationContext appContext = null; + 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); + + try { + obpGwMetricManService.executeHourlyAggregation(); + } 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")); + } +} 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..caeb659 --- /dev/null +++ b/src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml @@ -0,0 +1,91 @@ + + + + + + + + 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 NOT LIKE 'RE%' AND RSPNSERRCDNAME NOT LIKE 'RF%')) + 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, + BZWKSVCKEYNAME 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[]# + + + + From a90b481120b206a315f970fe0865523de86a716d Mon Sep 17 00:00:00 2001 From: Rinjae Date: Wed, 10 Dec 2025 15:12:42 +0900 Subject: [PATCH 04/15] =?UTF-8?q?OBP=20GwMetric=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kjb-docs/GwMetricHourJob.md | 5 + readme.md | 5 +- .../entity/onl/apim/obp/ObpGwMetricDAO.java | 52 ++++++---- .../onl/apim/obp/ObpGwMetricService.java | 19 ++-- .../entity/onl/apim/obp/ObpGwMetricVO.java | 2 +- .../onl/apim/obp/ObpGwMetricManService.java | 29 +++++- .../common/service/ObpGwMetricHourJob.java | 99 ++++++++++++++++++- .../eai/apim/obp/ObpGwMetric-oracle.xml | 13 ++- 8 files changed, 175 insertions(+), 49 deletions(-) create mode 100644 kjb-docs/GwMetricHourJob.md 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/onl/apim/obp/ObpGwMetricDAO.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricDAO.java index b61fbac..ca3d661 100644 --- 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 @@ -3,6 +3,7 @@ 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; @@ -44,16 +45,19 @@ public class ObpGwMetricDAO extends SqlMapClientTemplateDao { HashMap paramMap = new HashMap<>(); paramMap.put("schemaId", schemaId); - paramMap.put("apiIds", apiIds); + paramMap.put("apiIds", new ArrayList<>(apiIds)); // Set → List 변환 (iBATIS iterate 호환) List> resultList = this.template.queryForList("ObpGwMetric.selectApiNames", paramMap); - return resultList.stream() - .collect(Collectors.toMap( - m -> m.get("API_ID"), - m -> m.get("API_NAME"), - (v1, v2) -> v1 - )); + 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; } /** @@ -67,16 +71,19 @@ public class ObpGwMetricDAO extends SqlMapClientTemplateDao { HashMap paramMap = new HashMap<>(); paramMap.put("schemaId", schemaId); - paramMap.put("apiIds", apiIds); + paramMap.put("apiIds", new ArrayList<>(apiIds)); // Set → List 변환 (iBATIS iterate 호환) List> resultList = this.template.queryForList("ObpGwMetric.selectUris", paramMap); - return resultList.stream() - .collect(Collectors.toMap( - m -> m.get("API_ID"), - m -> m.get("URI"), - (v1, v2) -> v1 - )); + 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; } /** @@ -89,15 +96,18 @@ public class ObpGwMetricDAO extends SqlMapClientTemplateDao { } HashMap paramMap = new HashMap<>(); - paramMap.put("orgIds", orgIds); + paramMap.put("orgIds", new ArrayList<>(orgIds)); // Set → List 변환 (iBATIS iterate 호환) List> resultList = this.template.queryForList("ObpGwMetric.selectAppIdsByOrgIds", paramMap); - return resultList.stream() - .collect(Collectors.toMap( - m -> m.get("ORG_ID"), - m -> m.get("APP_ID"), - (v1, v2) -> v1 - )); + 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/ObpGwMetricService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/obp/ObpGwMetricService.java index 79d3022..be973ab 100644 --- 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 @@ -1,7 +1,6 @@ package com.eactive.eai.rms.data.entity.onl.apim.obp; import com.eactive.apim.portal.obp.entity.ObpGwMetric; -import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,13 +13,19 @@ import java.util.Optional; */ @Service @Transactional(transactionManager = "transactionManagerForEMS") -public class ObpGwMetricService extends AbstractEMSDataSerivce { +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 getRepository().findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId( + return repository.findByTimesliceAndApiIdAndClientIdAndHostnameAndOrgId( timeslice, apiId, clientId, hostname, orgId); } @@ -28,27 +33,27 @@ public class ObpGwMetricService extends AbstractEMSDataSerivce findByTimeslice(LocalDateTime timeslice) { - return getRepository().findByTimeslice(timeslice); + return repository.findByTimeslice(timeslice); } /** * 특정 시간 범위의 GwMetric 데이터 조회 */ public List findByTimesliceBetween(LocalDateTime startTime, LocalDateTime endTime) { - return getRepository().findByTimesliceBetween(startTime, endTime); + return repository.findByTimesliceBetween(startTime, endTime); } /** * 엔티티 저장 (INSERT 또는 UPDATE) */ public ObpGwMetric save(ObpGwMetric entity) { - return getRepository().save(entity); + return repository.save(entity); } /** * 엔티티 목록 저장 */ public List saveAll(List entities) { - return getRepository().saveAll(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 index 20adc9a..db55110 100644 --- 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 @@ -45,7 +45,7 @@ public class ObpGwMetricVO { if (timeslice == null || timeslice.isEmpty()) { return null; } - // yyyyMMddHH0000000 형식에서 앞 10자리(yyyyMMddHH)만 사용 + // "yyyyMMddHH0000000" 형식에서 앞 10자리(yyyyMMddHH)만 사용 String dateTimeStr = timeslice.substring(0, 10) + "0000"; return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); } 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 index 8543dec..f4a458f 100644 --- 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 @@ -31,11 +31,13 @@ public class ObpGwMetricManService extends BaseService { private static final Logger log = LoggerFactory.getLogger(ObpGwMetricManService.class); /** - * 집계 범위 시간 (단위: 시간) + * 기본 집계 범위 시간 (단위: 시간) * - 1: 직전 1시간만 집계 * - 5: 직전 5시간까지 각각 1시간 단위로 집계 + * + *

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

*/ - private static final int AGGREGATION_HOUR_RANGE = 1; + 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"); @@ -50,22 +52,35 @@ public class ObpGwMetricManService extends BaseService { } /** - * 다중 시간대 집계 실행 (Job에서 호출) + * 다중 시간대 집계 실행 (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("실행 시간: {}, AGGREGATION_HOUR_RANGE: {}", now, AGGREGATION_HOUR_RANGE); + log.info("실행 시간: {}, aggregationHourRange: {}", now, aggregationHourRange); - for (int i = 1; i <= AGGREGATION_HOUR_RANGE; i++) { + 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)); } /** @@ -184,6 +199,8 @@ public class ObpGwMetricManService extends BaseService { 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 { @@ -239,6 +256,8 @@ public class ObpGwMetricManService extends BaseService { entity.setUri(uriMap.get(vo.getApiId())); entity.setAppId(appIdMap.get(vo.getOrgId())); entity.setUpdateCount(0); + entity.setCreatedAt(LocalDateTime.now()); + entity.setCreatedBy("GwMetricHourJob"); return entity; } 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 index 35befd3..7b93297 100644 --- 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 @@ -4,12 +4,13 @@ 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.slf4j.Logger; -import org.slf4j.LoggerFactory; 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; /** @@ -22,18 +23,45 @@ import org.springframework.context.ApplicationContext; *
  • 보정 (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")); - ApplicationContext appContext = null; + // Job 파라미터 로깅 + JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); + logJobParameters(jobDataMap); + + ApplicationContext appContext; try { appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext"); } catch (SchedulerException e) { @@ -44,8 +72,11 @@ public class ObpGwMetricHourJob implements Job { monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext"); ObpGwMetricManService obpGwMetricManService = appContext.getBean(ObpGwMetricManService.class); + // 집계 범위 시간 파라미터 파싱 + int aggregationHourRange = parseAggregationHourRange(jobDataMap); + try { - obpGwMetricManService.executeHourlyAggregation(); + obpGwMetricManService.executeHourlyAggregation(aggregationHourRange); } catch (Exception e) { log.error("ObpGwMetricHourJob execution failed", e); throw new JobExecutionException(e); @@ -53,4 +84,64 @@ public class ObpGwMetricHourJob implements Job { 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/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml b/src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml index caeb659..ab42299 100644 --- a/src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml +++ b/src/main/resources/com/eactive/eai/apim/obp/ObpGwMetric-oracle.xml @@ -24,9 +24,9 @@ + SELECT EAISVCNAME AS API_ID, - BZWKSVCKEYNAME AS URI + APIFULLPATH AS URI FROM $schemaId$.TSEAIHS04 WHERE EAISVCNAME IN From 38f37adc1071eae5aafc08521a05a740b4674f69 Mon Sep 17 00:00:00 2001 From: "Yunsam.Eo" Date: Wed, 10 Dec 2025 20:21:05 +0900 Subject: [PATCH 05/15] =?UTF-8?q?API=EB=93=B1=EB=A1=9D=EC=8B=9C=20HTTP=20I?= =?UTF-8?q?nbound=20Adapter=20=ED=97=88=EC=9A=A9.=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jsp/onl/transaction/apim/apiInterfaceManDetail.jsp | 10 +++++----- .../rms/onl/transaction/apim/ApiInterfaceService.java | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/WebContent/jsp/onl/transaction/apim/apiInterfaceManDetail.jsp b/WebContent/jsp/onl/transaction/apim/apiInterfaceManDetail.jsp index 62702a9..a87c12f 100644 --- a/WebContent/jsp/onl/transaction/apim/apiInterfaceManDetail.jsp +++ b/WebContent/jsp/onl/transaction/apim/apiInterfaceManDetail.jsp @@ -313,9 +313,9 @@ return name + '=' + value; }).join(';'); if(headerValues.length > 0){ - $("#headerRoutingButton").text('Headers ['+label+']'); + $("#headerRoutingButton").text('MessageKey ['+label+']'); }else{ - $("#headerRoutingButton").text('HTTP 헤더 라우팅 정보 없음'); + $("#headerRoutingButton").text('MessageKey 라우팅 정보 없음'); } } @@ -382,7 +382,7 @@ '' + '' + '
    ' + - '' + + '' + '
    ' + ''; $('#headerPairs').append(newPairHtml); @@ -1632,7 +1632,7 @@