Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23ba7993c3 | |||
| 90c43ea7b0 | |||
| 191857811f | |||
| 88ffc1cc28 | |||
| d3bd4acb62 | |||
| 1f7166502f | |||
| 91f36605e8 | |||
| 0ebd31e256 | |||
| 45c6edbbeb | |||
| 335c7ec53a | |||
| 0b341b85f1 | |||
| e9ac62d3ed | |||
| 133c05f1d7 | |||
| 4a1a4be54d | |||
| dbd36f28d4 | |||
| 4ee50be000 | |||
| 3d916eda53 | |||
| 49fdc3b577 | |||
| 9d55db3281 | |||
| 9a5d5e4815 | |||
| e9bff78cbf | |||
| 6ccd669ec6 | |||
| 5db046eb10 | |||
| 8bd8558038 | |||
| c3f5d6a42e | |||
| 72cdb04261 | |||
| d3601c3991 | |||
| e213c96994 | |||
| 889d1fcfad | |||
| 45f980f638 |
+3
-2
@@ -1,5 +1,3 @@
|
|||||||
gradle.properties
|
|
||||||
|
|
||||||
# Package Files #
|
# Package Files #
|
||||||
*.war
|
*.war
|
||||||
|
|
||||||
@@ -248,3 +246,6 @@ eapim-admin-kjb/
|
|||||||
*.report.md
|
*.report.md
|
||||||
*.claude.md
|
*.claude.md
|
||||||
tmpclaude-*-cwd
|
tmpclaude-*-cwd
|
||||||
|
logs
|
||||||
|
rinjae_
|
||||||
|
tomcat-base
|
||||||
@@ -10,7 +10,7 @@ AI 어시스턴트가 작업을 시작하기 전에 반드시 이해해야 할
|
|||||||
- **신규 기능**: 항상 **JPA**와 Spring Data 리포지토리를 사용해야 합니다.
|
- **신규 기능**: 항상 **JPA**와 Spring Data 리포지토리를 사용해야 합니다.
|
||||||
- **레거시 코드**: 기존 iBATIS (`*.xml` 매퍼) 코드를 수정할 때는 해당 패턴을 따라야 합니다.
|
- **레거시 코드**: 기존 iBATIS (`*.xml` 매퍼) 코드를 수정할 때는 해당 패턴을 따라야 합니다.
|
||||||
- **멀티 모듈 Gradle 프로젝트**: `settings.gradle`에 정의된 것처럼 여러 하위 모듈(`elink-online-*`, `kjb-*` 등)로 구성되어 있습니다. 특정 기능은 다른 모듈에 위치할 수 있습니다.
|
- **멀티 모듈 Gradle 프로젝트**: `settings.gradle`에 정의된 것처럼 여러 하위 모듈(`elink-online-*`, `kjb-*` 등)로 구성되어 있습니다. 특정 기능은 다른 모듈에 위치할 수 있습니다.
|
||||||
- **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `kjb-gradle.sh compileJava`를 먼저 실행하여 코드를 생성해야 합니다.
|
- **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `./gradlew compileJava` 를 먼저 실행하여 코드를 생성해야 합니다.
|
||||||
- **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다.
|
- **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다.
|
||||||
|
|
||||||
## 프로젝트 개요
|
## 프로젝트 개요
|
||||||
@@ -39,50 +39,48 @@ eLink EMS (eLink Management System)는 eLink의 웹 기반 관리 서비스로,
|
|||||||
|
|
||||||
### 빌드 명령어
|
### 빌드 명령어
|
||||||
|
|
||||||
**⚠️ 중요**: 환경변수 문제로 인해 `gradle` 대신 `kjb-gradle.sh` 스크립트를 사용해야 합니다.
|
프로젝트 루트의 Gradle wrapper(`./gradlew`)를 사용합니다. JDK 8 toolchain 위치는 `.envrc` 등으로 잡습니다(아래 "기술 스택" 섹션 참조).
|
||||||
- 스크립트 위치: `/c/eactive/workspaces/shell-scripts/kjb-gradle.sh`
|
|
||||||
- PATH에 등록되어 있으므로 바로 `kjb-gradle.sh` 명령어 사용 가능
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 표준 빌드
|
# 표준 빌드
|
||||||
kjb-gradle.sh build
|
./gradlew build
|
||||||
|
|
||||||
# Weblogic 배포용 빌드 (테스트 제외)
|
# Weblogic 배포용 빌드 (테스트 제외)
|
||||||
kjb-gradle.sh build -x test -Pprofile=weblogic
|
./gradlew build -x test -Pprofile=weblogic
|
||||||
|
|
||||||
# WAR 파일 빌드
|
# WAR 파일 빌드
|
||||||
kjb-gradle.sh war
|
./gradlew war
|
||||||
|
|
||||||
# 클린 빌드
|
# 클린 빌드
|
||||||
kjb-gradle.sh clean build
|
./gradlew clean build
|
||||||
```
|
```
|
||||||
|
|
||||||
### 테스트 실행
|
### 테스트 실행
|
||||||
```bash
|
```bash
|
||||||
# 모든 테스트 실행
|
# 모든 테스트 실행
|
||||||
kjb-gradle.sh test
|
./gradlew test
|
||||||
|
|
||||||
# 특정 테스트 클래스 실행
|
# 특정 테스트 클래스 실행
|
||||||
kjb-gradle.sh test --tests "com.example.ClassName"
|
./gradlew test --tests "com.example.ClassName"
|
||||||
|
|
||||||
# 특정 테스트 패키지 실행 (JUnit 플랫폼 사용)
|
# 특정 테스트 패키지 실행 (JUnit 플랫폼 사용)
|
||||||
kjb-gradle.sh test --tests "com.eactive.eai.rms.*"
|
./gradlew test --tests "com.eactive.eai.rms.*"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 개발 태스크
|
### 개발 태스크
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 패키징 없이 클래스만 컴파일
|
# 패키징 없이 클래스만 컴파일
|
||||||
kjb-gradle.sh classes
|
./gradlew classes
|
||||||
|
|
||||||
# QueryDSL Q-classes 및 기타 애노테이션 생성
|
# QueryDSL Q-classes 및 기타 애노테이션 생성
|
||||||
kjb-gradle.sh compileJava
|
./gradlew compileJava
|
||||||
|
|
||||||
# 의존성 트리 보기
|
# 의존성 트리 보기
|
||||||
kjb-gradle.sh dependencies
|
./gradlew dependencies
|
||||||
|
|
||||||
# 사용 가능한 모든 태스크 목록
|
# 사용 가능한 모든 태스크 목록
|
||||||
kjb-gradle.sh tasks --all
|
./gradlew tasks --all
|
||||||
```
|
```
|
||||||
|
|
||||||
## 로컬 개발 빠른 시작
|
## 로컬 개발 빠른 시작
|
||||||
@@ -92,26 +90,26 @@ kjb-gradle.sh tasks --all
|
|||||||
1. **IDE 설정 파일 생성 (최초 1회)**
|
1. **IDE 설정 파일 생성 (최초 1회)**
|
||||||
```bash
|
```bash
|
||||||
# Eclipse 사용 시 (기본)
|
# Eclipse 사용 시 (기본)
|
||||||
kjb-gradle.sh eclipse
|
./gradlew eclipse
|
||||||
|
|
||||||
# IntelliJ IDEA 사용 시 (build.gradle.intellij 파일 사용 필요)
|
# IntelliJ IDEA 사용 시 (build.gradle.intellij 파일 사용 필요)
|
||||||
# 1. build.gradle을 build.gradle.eclipse로 백업
|
# 1. build.gradle을 build.gradle.eclipse로 백업
|
||||||
# 2. build.gradle.intellij를 build.gradle로 복사
|
# 2. build.gradle.intellij를 build.gradle로 복사
|
||||||
# 3. kjb-gradle.sh idea 실행
|
# 3. ./gradlew idea 실행
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **QueryDSL 등 소스 코드 생성**
|
2. **QueryDSL 등 소스 코드 생성**
|
||||||
```bash
|
```bash
|
||||||
kjb-gradle.sh compileJava
|
./gradlew compileJava
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **전체 빌드 및 테스트 실행**
|
3. **전체 빌드 및 테스트 실행**
|
||||||
```bash
|
```bash
|
||||||
kjb-gradle.sh build
|
./gradlew build
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **IDE에서 프로젝트 열기**
|
4. **IDE에서 프로젝트 열기**
|
||||||
- Eclipse: `kjb-gradle.sh eclipse` 실행 후, IDE에서 프로젝트를 엽니다.
|
- Eclipse: `./gradlew eclipse` 실행 후, IDE에서 프로젝트를 엽니다.
|
||||||
- IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다.
|
- IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다.
|
||||||
|
|
||||||
5. **실행 구성(Run Configuration) 설정**
|
5. **실행 구성(Run Configuration) 설정**
|
||||||
@@ -162,14 +160,14 @@ eapim-admin (root project)
|
|||||||
#### Q-Class 생성 위치
|
#### Q-Class 생성 위치
|
||||||
|
|
||||||
Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다.
|
Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다.
|
||||||
- `kjb-gradle.sh compileJava` 또는 `kjb-gradle.sh build` 실행 시 Gradle이 자동으로 생성
|
- `./gradlew compileJava` 또는 `./gradlew build` 실행 시 Gradle이 자동으로 생성
|
||||||
- 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다
|
- 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다
|
||||||
|
|
||||||
#### 생성된 코드 작업하기
|
#### 생성된 코드 작업하기
|
||||||
|
|
||||||
- **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다
|
- **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다
|
||||||
- **업데이트 pull 후**: `kjb-gradle.sh compileJava`를 실행하여 Q-classes를 재생성합니다
|
- **업데이트 pull 후**: `./gradlew compileJava` 를 실행하여 Q-classes를 재생성합니다
|
||||||
- **Q-classes IDE 오류 시**: `kjb-gradle.sh clean compileJava`로 재생성합니다
|
- **Q-classes IDE 오류 시**: `./gradlew clean compileJava` 로 재생성합니다
|
||||||
|
|
||||||
#### IDE별 빌드 설정 파일
|
#### IDE별 빌드 설정 파일
|
||||||
|
|
||||||
@@ -255,12 +253,21 @@ IntelliJ를 사용하려면 `build.gradle.intellij`를 `build.gradle`로 교체
|
|||||||
WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다:
|
WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kjb-gradle.sh build -x test -Pprofile=weblogic
|
./gradlew build -x test -Pprofile=weblogic
|
||||||
```
|
```
|
||||||
|
|
||||||
## 기술 스택
|
## 기술 스택
|
||||||
|
|
||||||
- **Java 8**: Gradle toolchain을 통해 강제
|
- **Java 8**: Gradle toolchain을 통해 강제 (`JavaLanguageVersion.of(8)`). 프로젝트의 `gradle.properties` 에는 JDK 경로를 기입하지 않습니다 — 개인 환경별로 `.envrc`(direnv) 또는 셸 rc 파일에서 `JAVA_HOME` 을 JDK 8 위치로 export 하세요. Gradle 8 의 toolchain auto-detect 가 `JAVA_HOME` 도 후보 경로로 인식합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 예: 프로젝트 루트의 .envrc (Zulu 8 / macOS Apple Silicon)
|
||||||
|
JAVA_HOME=${HOME}/opts/jdks/zulu8.94.0.17-ca-jdk8.0.492-macosx_aarch64
|
||||||
|
GRADLE_USER_HOME=${HOME}/eactive/djb-eapim/gradle-home
|
||||||
|
export PATH=${JAVA_HOME}/bin:${PATH}
|
||||||
|
```
|
||||||
|
|
||||||
|
Gradle 표준 위치(`/Library/Java/JavaVirtualMachines/...`, Homebrew, SDKMAN 등)에 설치한 경우는 별도 설정 없이도 auto-detect 됩니다.
|
||||||
- **Spring Framework 5.3.27**: 코어 프레임워크 (MVC, Data JPA)
|
- **Spring Framework 5.3.27**: 코어 프레임워크 (MVC, Data JPA)
|
||||||
- **Spring Data JPA 2.5.2**: 리포지토리 추상화
|
- **Spring Data JPA 2.5.2**: 리포지토리 추상화
|
||||||
- **Hibernate 5.4.33/5.6.15**: ORM 구현 (JPA/ORM)
|
- **Hibernate 5.4.33/5.6.15**: ORM 구현 (JPA/ORM)
|
||||||
@@ -271,9 +278,10 @@ kjb-gradle.sh build -x test -Pprofile=weblogic
|
|||||||
- **Jackson 2.13.1**: JSON 처리
|
- **Jackson 2.13.1**: JSON 처리
|
||||||
- **Logback 1.2.10**: 로깅 프레임워크 (SLF4J와 함께)
|
- **Logback 1.2.10**: 로깅 프레임워크 (SLF4J와 함께)
|
||||||
- **AWS SDK 2.20.142**: S3 연동
|
- **AWS SDK 2.20.142**: S3 연동
|
||||||
- **Kubernetes Client 18.0.1**: K8s 관리
|
|
||||||
- **EhCache**: 캐싱, **Lombok**: 코드 생성, **MapStruct**: 객체 매핑
|
- **EhCache**: 캐싱, **Lombok**: 코드 생성, **MapStruct**: 객체 매핑
|
||||||
|
|
||||||
|
> Kubernetes Client 의존성은 JDK 8 호환 문제로 제거됨(`io.kubernetes:client-java:18.0.1` 은 JDK 11+ 필요). K8s 모드 기능이 필요하면 `≤13.0.2` 로 다운그레이드 후 재도입 검토.
|
||||||
|
|
||||||
## 프로젝트 규칙
|
## 프로젝트 규칙
|
||||||
|
|
||||||
### 패키지 구조
|
### 패키지 구조
|
||||||
@@ -376,7 +384,7 @@ com.eactive.eai
|
|||||||
### IntelliJ IDEA (권장)
|
### IntelliJ IDEA (권장)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kjb-gradle.sh idea
|
./gradlew idea
|
||||||
```
|
```
|
||||||
|
|
||||||
또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다.
|
또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다.
|
||||||
@@ -395,7 +403,7 @@ cp build.gradle build.gradle.intellij
|
|||||||
cp build.gradle.eclipse build.gradle
|
cp build.gradle.eclipse build.gradle
|
||||||
|
|
||||||
# 3. Eclipse 프로젝트 생성
|
# 3. Eclipse 프로젝트 생성
|
||||||
kjb-gradle.sh eclipse
|
./gradlew eclipse
|
||||||
```
|
```
|
||||||
|
|
||||||
Eclipse 전용 설정(`build.gradle.eclipse`)은 다음을 포함합니다:
|
Eclipse 전용 설정(`build.gradle.eclipse`)은 다음을 포함합니다:
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
p:render="ONL,COM"
|
p:render="ONL,COM"
|
||||||
/>
|
/>
|
||||||
<!-- BAP 일괄전송FTP -->
|
<!-- BAP 일괄전송FTP -->
|
||||||
<bean
|
<!-- <bean
|
||||||
id="BAP"
|
id="BAP"
|
||||||
class="com.eactive.eai.rms.common.datasource.DataSourceType"
|
class="com.eactive.eai.rms.common.datasource.DataSourceType"
|
||||||
p:name="BAP"
|
p:name="BAP"
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
p:online="true"
|
p:online="true"
|
||||||
p:jndiName="jdbc/dsOBP_BAP"
|
p:jndiName="jdbc/dsOBP_BAP"
|
||||||
p:render="BAP,COM"
|
p:render="BAP,COM"
|
||||||
/>
|
/> -->
|
||||||
<!-- RMS_DEFAULT -->
|
<!-- RMS_DEFAULT -->
|
||||||
<bean
|
<bean
|
||||||
id="MONITORING"
|
id="MONITORING"
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
<entry
|
<entry
|
||||||
key="APIGW"
|
key="APIGW"
|
||||||
value-ref="APIGW" />
|
value-ref="APIGW" />
|
||||||
<entry
|
<!-- <entry
|
||||||
key="BAP"
|
key="BAP"
|
||||||
value-ref="BAP" />
|
value-ref="BAP" /> -->
|
||||||
</map>
|
</map>
|
||||||
</property>
|
</property>
|
||||||
<property
|
<property
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
<property name="configLocations">
|
<property name="configLocations">
|
||||||
<list>
|
<list>
|
||||||
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfOnl.xml</value>
|
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfOnl.xml</value>
|
||||||
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBap.xml</value>
|
<!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBap.xml</value> -->
|
||||||
<!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBat.xml</value>
|
<!-- <value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfBat.xml</value>
|
||||||
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfService.xml</value> -->
|
<value>/WEB-INF/sqlmap-config/${db.vendor}/sqlMapConfigOfService.xml</value> -->
|
||||||
</list>
|
</list>
|
||||||
|
|||||||
@@ -9,15 +9,7 @@
|
|||||||
<param-name>contextConfigLocation</param-name>
|
<param-name>contextConfigLocation</param-name>
|
||||||
<param-value>/WEB-INF/applicationContext.xml</param-value>
|
<param-value>/WEB-INF/applicationContext.xml</param-value>
|
||||||
</context-param>
|
</context-param>
|
||||||
<!-- XSS Filter -->
|
|
||||||
<filter>
|
|
||||||
<filter-name>CrossScriptingFilter</filter-name>
|
|
||||||
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</filter-class>
|
|
||||||
</filter>
|
|
||||||
<filter-mapping>
|
|
||||||
<filter-name>CrossScriptingFilter</filter-name>
|
|
||||||
<url-pattern>/*</url-pattern>
|
|
||||||
</filter-mapping>
|
|
||||||
|
|
||||||
<filter>
|
<filter>
|
||||||
<filter-name>encodingFilter</filter-name>
|
<filter-name>encodingFilter</filter-name>
|
||||||
@@ -57,6 +49,17 @@
|
|||||||
<url-pattern>*.json</url-pattern>
|
<url-pattern>*.json</url-pattern>
|
||||||
</filter-mapping>
|
</filter-mapping>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- XSS Filter -->
|
||||||
|
<filter>
|
||||||
|
<filter-name>CrossScriptingFilter</filter-name>
|
||||||
|
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</filter-class>
|
||||||
|
</filter>
|
||||||
|
<filter-mapping>
|
||||||
|
<filter-name>CrossScriptingFilter</filter-name>
|
||||||
|
<url-pattern>/*</url-pattern>
|
||||||
|
</filter-mapping>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<filter>
|
<filter>
|
||||||
<filter-name>MonitoringFrontFilter</filter-name>
|
<filter-name>MonitoringFrontFilter</filter-name>
|
||||||
|
|||||||
@@ -31,19 +31,19 @@ function init(){
|
|||||||
new makeOptions("CODE","NAME").setObj($("select[name=searchJobInst]")).setNoValueInclude(true).setNoValue("",
|
new makeOptions("CODE","NAME").setObj($("select[name=searchJobInst]")).setNoValueInclude(true).setNoValue("",
|
||||||
"<%= localeMessage.getString("combo.all")%>").setData(json.instanceList).rendering();
|
"<%= localeMessage.getString("combo.all")%>").setData(json.instanceList).rendering();
|
||||||
|
|
||||||
if(typeof callback === 'function') {
|
putSelectFromParam();
|
||||||
callback(url,key);
|
|
||||||
}
|
initGrid();
|
||||||
},
|
},
|
||||||
error:function(e){
|
error:function(e){
|
||||||
alert(e.responseText);
|
alert(e.responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$(document).ready(function() {
|
|
||||||
init();
|
function initGrid() {
|
||||||
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||||
console.log(gridPostData);
|
|
||||||
$('#grid').jqGrid({
|
$('#grid').jqGrid({
|
||||||
datatype:"json",
|
datatype:"json",
|
||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
@@ -125,8 +125,12 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
resizeJqGridWidth('grid','content_middle','1000');
|
resizeJqGridWidth('grid','content_middle','1000');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
init();
|
||||||
|
|
||||||
$("#btn_search").click(function(){
|
$("#btn_search").click(function(){
|
||||||
var postData = getSearchForJqgrid("cmd","LIST");
|
var postData = getSearchForJqgrid("cmd","LIST");
|
||||||
|
|||||||
@@ -465,21 +465,21 @@ $(document).ready(function() {
|
|||||||
<table class="table_row" cellspacing="0">
|
<table class="table_row" cellspacing="0">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:180px;">Default Scheduler</th>
|
<th style="width:180px;">Default Scheduler</th>
|
||||||
<td>On Memory: <span id="deft_onMemory"></span></td>
|
<td style="width:150px">On Memory: <span id="deft_onMemory"></span></td>
|
||||||
<td style="width: 260px;">Previous Fired: <span id="deft_previous"></span></td>
|
<td >Previous Fired: <span id="deft_previous"></span></td>
|
||||||
<td style="width: 260px;">Next Fire Time: <span id="deft_next"></span></td>
|
<td >Next Fire Time: <span id="deft_next"></span></td>
|
||||||
<td style="width: 220px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td>
|
<td style="width: 250px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td>
|
||||||
<td style="width: 200px;">PersistJobData: <span id="deft_persistJobData"></span></td>
|
<td style="width: 120px;">PersistJobData: <span id="deft_persistJobData"></span></td>
|
||||||
<td style="width: 200px;">Durable: <span id="deft_durable"></span></td>
|
<td style="width: 120px;">Durable: <span id="deft_durable"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:180px;">Clustered Scheduler</th>
|
<th style="width:180px;">Clustered Scheduler</th>
|
||||||
<td>On Memory: <span id="clus_onMemory"></span></td>
|
<td style="width:150px">On Memory: <span id="clus_onMemory"></span></td>
|
||||||
<td style="width: 260px;">Previous Fired: <span id="clus_previous"></span></td>
|
<td>Previous Fired: <span id="clus_previous"></span></td>
|
||||||
<td style="width: 260px;">Next Fire Time: <span id="clus_next"></span></td>
|
<td>Next Fire Time: <span id="clus_next"></span></td>
|
||||||
<td style="width: 220px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td>
|
<td style="width: 250px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td>
|
||||||
<td style="width: 200px;">PersistJobData: <span id="clus_persistJobData"></span></td>
|
<td style="width: 120px;">PersistJobData: <span id="clus_persistJobData"></span></td>
|
||||||
<td style="width: 200px;">Durable: <span id="clus_durable"></span></td>
|
<td style="width: 120px;">Durable: <span id="clus_durable"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||||
|
<%@ page import="java.io.*"%>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||||
|
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
|
||||||
|
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||||
|
<%
|
||||||
|
response.setHeader("Pragma", "No-cache");
|
||||||
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
|
response.setHeader("Expires", "0");
|
||||||
|
|
||||||
|
%>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title></title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||||
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||||
|
<script language="javascript" >
|
||||||
|
var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />';
|
||||||
|
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
|
||||||
|
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#grid').jqGrid({
|
||||||
|
datatype:"json",
|
||||||
|
mtype: 'POST',
|
||||||
|
url: url,
|
||||||
|
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val()},
|
||||||
|
colNames:['클라이언트 ID',
|
||||||
|
'클라이언트명',
|
||||||
|
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
|
||||||
|
'<%= localeMessage.getString("infAdpConMan.thr") %>',
|
||||||
|
'<%= localeMessage.getString("infAdpConMan.thrTimeUnit") %>',
|
||||||
|
'<%= localeMessage.getString("infAdpConMan.useYn") %>'
|
||||||
|
],
|
||||||
|
colModel:[
|
||||||
|
{ name : 'NAME' , align:'left' , width:'100' , sortable:false },
|
||||||
|
{ name : 'DESC' , align:'left' , width:'200'},
|
||||||
|
{ name : 'THRESHOLDPERSECOND' , align:'right' , width:'60'},
|
||||||
|
{ name : 'THRESHOLD' , align:'right' , width:'60'},
|
||||||
|
{ name : 'THRESHOLDTIMEUNIT' , align:'center' , width:'80'},
|
||||||
|
{ name : 'USEYN' , align:'center' , width:'60' , editoptions:{value:"0:사용안함;1:사용함"}, formatter:"select" }
|
||||||
|
],
|
||||||
|
jsonReader: {
|
||||||
|
repeatitems:false
|
||||||
|
},
|
||||||
|
pager : $('#pager'),
|
||||||
|
page : '${param.page}',
|
||||||
|
rowNum : '${rmsDefaultRowNum}',
|
||||||
|
autoheight: true,
|
||||||
|
height: $("#container").height(),
|
||||||
|
autowidth: true,
|
||||||
|
viewrecords: true,
|
||||||
|
rowList : eval('[${rmsDefaultRowList}]'),
|
||||||
|
ondblClickRow: function(rowId) {
|
||||||
|
var rowData = $(this).getRowData(rowId);
|
||||||
|
var name = rowData['NAME'];
|
||||||
|
var url2 = url_view;
|
||||||
|
url2 += '?cmd=DETAIL';
|
||||||
|
url2 += '&page='+$(this).getGridParam("page");
|
||||||
|
url2 += '&returnUrl='+getReturnUrl();
|
||||||
|
url2 += '&menuId='+'${param.menuId}';
|
||||||
|
//검색값
|
||||||
|
url2 += '&searchName='+$("input[name=searchName]").val();
|
||||||
|
//key값
|
||||||
|
url2 += '&name='+name;
|
||||||
|
goNav(url2);
|
||||||
|
|
||||||
|
},
|
||||||
|
gridComplete:function (d){
|
||||||
|
var colModel = $(this).getGridParam("colModel");
|
||||||
|
for(var i = 0 ; i< colModel.length; i++){
|
||||||
|
$(this).setColProp(colModel[i].name, {sortable : false});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
resizeJqGridWidth('grid','content_middle','1000');
|
||||||
|
|
||||||
|
$("#btn_search").click(function(){
|
||||||
|
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val()}, page:1 }).trigger("reloadGrid");
|
||||||
|
});
|
||||||
|
$("#btn_new").click(function(){
|
||||||
|
var url2 = url_view;
|
||||||
|
url2 += '?cmd=DETAIL';
|
||||||
|
url2 += '&page='+$("#grid").getGridParam("page");
|
||||||
|
url2 += '&returnUrl='+getReturnUrl();
|
||||||
|
url2 += '&menuId='+'${param.menuId}';
|
||||||
|
//검색값
|
||||||
|
url2 += '&searchName=';
|
||||||
|
|
||||||
|
goNav(url2);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("input[name^=search]").keydown(function(key){
|
||||||
|
if (key.keyCode == 13){
|
||||||
|
$("#btn_search").click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonControl();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="right_box">
|
||||||
|
<div class="content_top">
|
||||||
|
<ul class="path">
|
||||||
|
<li><a href="#">${rmsMenuPath}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div><!-- end content_top -->
|
||||||
|
<div class="content_middle" id="content_middle">
|
||||||
|
<div class="search_wrap">
|
||||||
|
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||||
|
</div>
|
||||||
|
<div class="title">클라이언트 유량제어<span class="tooltip">클라이언트(API Key) 유량제어를 관리한다</span></div>
|
||||||
|
|
||||||
|
<table class="search_condition" cellspacing=0;>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th style="width:180px;">클라이언트명</th>
|
||||||
|
<td><input type="text" name="searchName" value="${param.searchName}"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table id="grid" ></table>
|
||||||
|
<div id="pager"></div>
|
||||||
|
|
||||||
|
</div><!-- end content_middle -->
|
||||||
|
</div><!-- end right_box -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||||
|
<%@ page import="java.io.*"%>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||||
|
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
|
||||||
|
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||||
|
<%
|
||||||
|
response.setHeader("Pragma", "No-cache");
|
||||||
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
|
response.setHeader("Expires", "0");
|
||||||
|
%>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title></title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||||
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||||
|
|
||||||
|
<script language="javascript" >
|
||||||
|
var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />';
|
||||||
|
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
|
||||||
|
var isDetail = false;
|
||||||
|
function init(url,key,callback){
|
||||||
|
$.ajax({
|
||||||
|
type : "POST",
|
||||||
|
url:url,
|
||||||
|
dataType:"json",
|
||||||
|
data:{cmd: 'LIST_INIT_COMBO'},
|
||||||
|
success:function(json){
|
||||||
|
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
|
||||||
|
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
|
||||||
|
|
||||||
|
if(typeof callback === 'function') {
|
||||||
|
callback(url,key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error:function(e){
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function detail(url,key){
|
||||||
|
if (!isDetail)return;
|
||||||
|
$.ajax({
|
||||||
|
type : "POST",
|
||||||
|
url:url,
|
||||||
|
dataType:"json",
|
||||||
|
data:{cmd: 'DETAIL', name : key},
|
||||||
|
success:function(json){
|
||||||
|
var data = json;
|
||||||
|
$("input[name=name]").attr('readonly',true);
|
||||||
|
$("input[name=desc]").attr('readonly',true);
|
||||||
|
|
||||||
|
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){
|
||||||
|
var name = $(this).attr("name");
|
||||||
|
var tag = $(this).prop("tagName").toLowerCase();
|
||||||
|
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
error:function(e){
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$(document).ready(function() {
|
||||||
|
var returnUrl = getReturnUrlForReturn();
|
||||||
|
var key ="${param.name}";
|
||||||
|
if (key != "" && key !="null"){
|
||||||
|
isDetail = true;
|
||||||
|
}
|
||||||
|
init(url,key,detail);
|
||||||
|
|
||||||
|
|
||||||
|
$("#btn_modify").click(function(){
|
||||||
|
var postData = $('#ajaxForm').serializeArray();
|
||||||
|
if (isDetail){
|
||||||
|
postData.push({ name: "cmd" , value:"UPDATE"});
|
||||||
|
}else{
|
||||||
|
postData.push({ name: "cmd" , value:"INSERT"});
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
type : "POST",
|
||||||
|
url:url,
|
||||||
|
data:postData,
|
||||||
|
success:function(args){
|
||||||
|
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
||||||
|
goNav(returnUrl);//LIST로 이동
|
||||||
|
},
|
||||||
|
error:function(e){
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$("#btn_delete").click(function(){
|
||||||
|
|
||||||
|
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
|
||||||
|
|
||||||
|
var postData = $('#ajaxForm').serializeArray();
|
||||||
|
postData.push({ name: "cmd" , value:"DELETE"});
|
||||||
|
$.ajax({
|
||||||
|
type : "POST",
|
||||||
|
url:url,
|
||||||
|
data:postData,
|
||||||
|
success:function(args){
|
||||||
|
alert("<%= localeMessage.getString("common.deleteMsg") %>");
|
||||||
|
goNav(returnUrl);//LIST로 이동
|
||||||
|
|
||||||
|
},
|
||||||
|
error:function(e){
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$("#btn_previous").click(function(){
|
||||||
|
goNav(returnUrl);//LIST로 이동
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonControl(isDetail);
|
||||||
|
titleControl(isDetail);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="right_box">
|
||||||
|
<div class="content_top">
|
||||||
|
<ul class="path">
|
||||||
|
<li><a href="#">${rmsMenuPath}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div><!-- end content_top -->
|
||||||
|
<div class="content_middle">
|
||||||
|
<div class="search_wrap">
|
||||||
|
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
|
||||||
|
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
|
||||||
|
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||||
|
</div>
|
||||||
|
<div class="title">클라이언트 유량제어 <span class="tooltip" >클라이언트 유량제어 </span></div>
|
||||||
|
|
||||||
|
<table id="grid" ></table>
|
||||||
|
<div id="pager"></div>
|
||||||
|
|
||||||
|
<!-- detail -->
|
||||||
|
<form id="ajaxForm">
|
||||||
|
<table class="table_row" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<th style="width:20%;">클라이언트 ID</th>
|
||||||
|
<td><input type="text" name="name" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width:20%;">클라이언트명</th>
|
||||||
|
<td><input type="text" name="desc" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th>
|
||||||
|
<td><input type="text" name="thresholdPerSecond" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th>
|
||||||
|
<td><input type="text" name="threshold" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th>
|
||||||
|
<td>
|
||||||
|
<div class="select-style">
|
||||||
|
<select name="thresholdTimeUnit" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
|
||||||
|
<td>
|
||||||
|
<div class="select-style">
|
||||||
|
<select name="useYn" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div><!-- end content_middle -->
|
||||||
|
</div><!-- end right_box -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||||
|
<%@ page import="java.io.*"%>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||||
|
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
|
||||||
|
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||||
|
<%
|
||||||
|
response.setHeader("Pragma", "No-cache");
|
||||||
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
|
response.setHeader("Expires", "0");
|
||||||
|
%>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title></title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||||
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||||
|
<script language="javascript">
|
||||||
|
var url = '<c:url value="/onl/admin/security/cryptoModuleMan.json" />';
|
||||||
|
var url_view = '<c:url value="/onl/admin/security/cryptoModuleMan.view" />';
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#grid').jqGrid({
|
||||||
|
datatype: "json",
|
||||||
|
mtype: 'POST',
|
||||||
|
url: url,
|
||||||
|
postData: {
|
||||||
|
cmd: 'LIST',
|
||||||
|
searchName: $('input[name=searchName]').val(),
|
||||||
|
algType: $('select[name=algType]').val(),
|
||||||
|
keySourceType: $('select[name=keySourceType]').val(),
|
||||||
|
useYn: $('select[name=useYn]').val()
|
||||||
|
},
|
||||||
|
colNames: ['ID', '모듈명', '설명', '알고리즘', '운영모드', '키 소스', '사용'],
|
||||||
|
colModel: [
|
||||||
|
{ name: 'CRYPTO_ID', hidden: true },
|
||||||
|
{ name: 'CRYPTO_NAME', align: 'left' },
|
||||||
|
{ name: 'CRYPTO_DESC', align: 'left', sortable: false },
|
||||||
|
{ name: 'ALG_TYPE', align: 'center', width: 80 },
|
||||||
|
{ name: 'CIPHER_MODE', align: 'center', width: 80 },
|
||||||
|
{ name: 'KEY_SOURCE_TYPE', align: 'center', width: 100 },
|
||||||
|
{ name: 'USE_YN', align: 'center', width: 60 }
|
||||||
|
],
|
||||||
|
jsonReader: { repeatitems: false },
|
||||||
|
pager: $('#pager'),
|
||||||
|
page: '${param.page}',
|
||||||
|
rowNum: '${rmsDefaultRowNum}',
|
||||||
|
autoheight: true,
|
||||||
|
height: $("#container").height(),
|
||||||
|
autowidth: true,
|
||||||
|
viewrecords: true,
|
||||||
|
rowList: eval('[${rmsDefaultRowList}]'),
|
||||||
|
ondblClickRow: function(rowId) {
|
||||||
|
var rowData = $(this).getRowData(rowId);
|
||||||
|
var cryptoId = rowData['CRYPTO_ID'];
|
||||||
|
var url2 = url_view + '?cmd=DETAIL';
|
||||||
|
url2 += '&page=' + $(this).getGridParam("page");
|
||||||
|
url2 += '&returnUrl=' + getReturnUrl();
|
||||||
|
url2 += '&menuId=' + '${param.menuId}';
|
||||||
|
url2 += '&searchName=' + $("input[name=searchName]").val();
|
||||||
|
url2 += '&cryptoId=' + cryptoId;
|
||||||
|
goNav(url2);
|
||||||
|
},
|
||||||
|
gridComplete: function() {
|
||||||
|
var colModel = $(this).getGridParam("colModel");
|
||||||
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadError: function(jqXHR, textStatus, errorThrown) {
|
||||||
|
var location = '<%=request.getContextPath()%>/';
|
||||||
|
comloadError(jqXHR, textStatus, errorThrown, location);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeJqGridWidth('grid', 'content_middle', '1000');
|
||||||
|
|
||||||
|
$("#btn_search").click(function() {
|
||||||
|
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||||
|
postData['algType'] = $('select[name=algType]').val();
|
||||||
|
postData['keySourceType'] = $('select[name=keySourceType]').val();
|
||||||
|
postData['useYn'] = $('select[name=useYn]').val();
|
||||||
|
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btn_new").click(function() {
|
||||||
|
var url2 = url_view + '?cmd=DETAIL';
|
||||||
|
url2 += '&page=' + $("#grid").getGridParam("page");
|
||||||
|
url2 += '&returnUrl=' + getReturnUrl();
|
||||||
|
url2 += '&menuId=' + '${param.menuId}';
|
||||||
|
goNav(url2);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("input[name=searchName]").keydown(function(key) {
|
||||||
|
if (key.keyCode == 13) { $("#btn_search").click(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonControl();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="right_box">
|
||||||
|
<div class="content_top">
|
||||||
|
<ul class="path">
|
||||||
|
<li><a href="#">${rmsMenuPath}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="content_middle" id="content_middle">
|
||||||
|
<div class="search_wrap">
|
||||||
|
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> 신규</button>
|
||||||
|
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> 검색</button>
|
||||||
|
</div>
|
||||||
|
<div class="title">암호화 모듈 설정</div>
|
||||||
|
<form id="ajaxForm" onsubmit="return false;">
|
||||||
|
<table class="search_condition" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th style="width:100px;">모듈명</th>
|
||||||
|
<td><input type="text" name="searchName" value="${param.searchName}"></td>
|
||||||
|
<th style="width:100px;">알고리즘</th>
|
||||||
|
<td>
|
||||||
|
<select name="algType">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="AES">AES</option>
|
||||||
|
<option value="ARIA">ARIA</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<th style="width:100px;">키 소스</th>
|
||||||
|
<td>
|
||||||
|
<select name="keySourceType">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="STATIC">STATIC</option>
|
||||||
|
<option value="DYNAMIC">DYNAMIC</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<th style="width:80px;">사용</th>
|
||||||
|
<td>
|
||||||
|
<select name="useYn">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="Y">Y</option>
|
||||||
|
<option value="N">N</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<table id="grid"></table>
|
||||||
|
<div id="pager"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||||
|
<%@ page import="java.io.*"%>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||||
|
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
|
||||||
|
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||||
|
<%
|
||||||
|
response.setHeader("Pragma", "No-cache");
|
||||||
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
|
response.setHeader("Expires", "0");
|
||||||
|
%>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title></title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||||
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||||
|
<script language="javascript">
|
||||||
|
var url = '<c:url value="/onl/admin/security/cryptoModuleMan.json" />';
|
||||||
|
var url_view = '<c:url value="/onl/admin/security/cryptoModuleMan.view" />';
|
||||||
|
var isDetail = false;
|
||||||
|
|
||||||
|
function isValid() {
|
||||||
|
if ($('input[name=cryptoName]').val().trim() === '') {
|
||||||
|
alert('모듈명을 입력하세요.');
|
||||||
|
$('input[name=cryptoName]').focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($('select[name=algType]').val() === '') {
|
||||||
|
alert('알고리즘을 선택하세요.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($('select[name=cipherMode]').val() === '') {
|
||||||
|
alert('운영모드를 선택하세요.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var keySourceType = $('select[name=keySourceType]').val();
|
||||||
|
if (keySourceType === '') {
|
||||||
|
alert('키 소스 유형을 선택하세요.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (keySourceType === 'STATIC') {
|
||||||
|
if ($('input[name=encKeyHex]').val().trim() === '') {
|
||||||
|
alert('암호화 키(Hex)를 입력하세요.');
|
||||||
|
$('input[name=encKeyHex]').focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (keySourceType === 'DYNAMIC') {
|
||||||
|
if ($('input[name=keyDerivStrategy]').val().trim() === '') {
|
||||||
|
alert('키 도출 전략 FQCN을 입력하세요.');
|
||||||
|
$('input[name=keyDerivStrategy]').focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($('textarea[name=keyDerivParams]').val().trim() === '') {
|
||||||
|
alert('키 도출 파라미터(JSON)를 입력하세요.');
|
||||||
|
$('textarea[name=keyDerivParams]').focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function textToHex(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
var encoder = new TextEncoder();
|
||||||
|
var bytes = encoder.encode(text);
|
||||||
|
return Array.from(bytes).map(function(b) {
|
||||||
|
return b.toString(16).padStart(2, '0');
|
||||||
|
}).join('').toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHexFromText(textEl, hexName, lenSpanId) {
|
||||||
|
var hex = textToHex($(textEl).val());
|
||||||
|
$('input[name=' + hexName + ']').val(hex);
|
||||||
|
updateHexLength(hex, lenSpanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHexLength(hex, lenSpanId) {
|
||||||
|
var charLen = hex ? hex.replace(/\s/g, '').length : 0;
|
||||||
|
var byteLen = Math.floor(charLen / 2);
|
||||||
|
var msg = charLen > 0 ? byteLen + ' bytes (' + charLen + '자)' : '';
|
||||||
|
$('#' + lenSpanId).text(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleKeySourceFields() {
|
||||||
|
var keySourceType = $('select[name=keySourceType]').val();
|
||||||
|
if (keySourceType === 'STATIC') {
|
||||||
|
$('.static-section').show();
|
||||||
|
$('.dynamic-section').hide();
|
||||||
|
} else if (keySourceType === 'DYNAMIC') {
|
||||||
|
$('.static-section').hide();
|
||||||
|
$('.dynamic-section').show();
|
||||||
|
} else {
|
||||||
|
$('.static-section').show();
|
||||||
|
$('.dynamic-section').show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detail(url, key) {
|
||||||
|
if (!isDetail) return;
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: url,
|
||||||
|
dataType: 'json',
|
||||||
|
data: { cmd: 'DETAIL', cryptoId: key },
|
||||||
|
success: function(data) {
|
||||||
|
$('input[name=cryptoId]').val(data['CRYPTO_ID']);
|
||||||
|
$('input[name=cryptoName]').val(data['CRYPTO_NAME']).attr('readonly', true);
|
||||||
|
$('input[name=cryptoDesc]').val(data['CRYPTO_DESC']);
|
||||||
|
$('select[name=algType]').val(data['ALG_TYPE']);
|
||||||
|
$('select[name=cipherMode]').val(data['CIPHER_MODE']);
|
||||||
|
$('select[name=padding]').val(data['PADDING'] || '');
|
||||||
|
$('input[name=ivHex]').val(data['IV_HEX']);
|
||||||
|
$('select[name=keySourceType]').val(data['KEY_SOURCE_TYPE']);
|
||||||
|
$('input[name=encKeyHex]').val(data['ENC_KEY_HEX']);
|
||||||
|
$('input[name=decKeyHex]').val(data['DEC_KEY_HEX']);
|
||||||
|
$('input[name=keyDerivStrategy]').val(data['KEY_DERIV_STRATEGY']);
|
||||||
|
$('textarea[name=keyDerivParams]').val(data['KEY_DERIV_PARAMS']);
|
||||||
|
$('select[name=cacheYn]').val(data['CACHE_YN']);
|
||||||
|
$('input[name=cacheTtlSec]').val(data['CACHE_TTL_SEC']);
|
||||||
|
$('select[name=useYn]').val(data['USE_YN']);
|
||||||
|
updateHexLength(data['ENC_KEY_HEX'], 'encKeyLen');
|
||||||
|
updateHexLength(data['DEC_KEY_HEX'], 'decKeyLen');
|
||||||
|
updateHexLength(data['IV_HEX'], 'ivHexLen');
|
||||||
|
if (data['MODIFIED_BY'] || data['MODIFIED_AT']) {
|
||||||
|
$('#span_modified_by').text(data['MODIFIED_BY'] || '');
|
||||||
|
$('#span_modified_at').text((data['MODIFIED_AT'] || '').replace('T', ' '));
|
||||||
|
$('#row_modified_info').show();
|
||||||
|
}
|
||||||
|
toggleKeySourceFields();
|
||||||
|
},
|
||||||
|
error: function(e) {
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
var returnUrl = getReturnUrlForReturn();
|
||||||
|
var key = '${param.cryptoId}';
|
||||||
|
if (key !== '' && key !== 'null') {
|
||||||
|
isDetail = true;
|
||||||
|
}
|
||||||
|
detail(url, key);
|
||||||
|
|
||||||
|
$('select[name=keySourceType]').change(function() {
|
||||||
|
toggleKeySourceFields();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('input[name=encKeyHex]').on('input', function() {
|
||||||
|
updateHexLength($(this).val(), 'encKeyLen');
|
||||||
|
});
|
||||||
|
$('input[name=decKeyHex]').on('input', function() {
|
||||||
|
updateHexLength($(this).val(), 'decKeyLen');
|
||||||
|
});
|
||||||
|
$('input[name=ivHex]').on('input', function() {
|
||||||
|
updateHexLength($(this).val(), 'ivHexLen');
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleKeySourceFields();
|
||||||
|
|
||||||
|
$('#btn_modify').click(function() {
|
||||||
|
if (!isValid()) return;
|
||||||
|
var postData = $('#ajaxForm').serializeArray();
|
||||||
|
postData.push({ name: 'cmd', value: isDetail ? 'UPDATE' : 'INSERT' });
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: url,
|
||||||
|
dataType: 'json',
|
||||||
|
data: postData,
|
||||||
|
success: function(data) {
|
||||||
|
var msg = '<%= localeMessage.getString("common.saveMsg") %>';
|
||||||
|
if (data && data.broadcastResult) {
|
||||||
|
msg += '\n\n[서버 반영 결과]\n' + data.broadcastResult;
|
||||||
|
}
|
||||||
|
alert(msg);
|
||||||
|
goNav(returnUrl);
|
||||||
|
},
|
||||||
|
error: function(e) {
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btn_delete').click(function() {
|
||||||
|
if (confirm('<%= localeMessage.getString("common.confirmMsg") %>') !== true) return;
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: url,
|
||||||
|
dataType: 'json',
|
||||||
|
data: { cmd: 'DELETE', cryptoId: $('input[name=cryptoId]').val() },
|
||||||
|
success: function(data) {
|
||||||
|
var msg = '<%= localeMessage.getString("common.deleteMsg") %>';
|
||||||
|
if (data && data.broadcastResult) {
|
||||||
|
msg += '\n\n[서버 반영 결과]\n' + data.broadcastResult;
|
||||||
|
}
|
||||||
|
alert(msg);
|
||||||
|
goNav(returnUrl);
|
||||||
|
},
|
||||||
|
error: function(e) {
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btn_previous').click(function() {
|
||||||
|
goNav(returnUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonControl(isDetail);
|
||||||
|
titleControl(isDetail);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="right_box">
|
||||||
|
<div class="content_top">
|
||||||
|
<ul class="path">
|
||||||
|
<li><a href="#">${rmsMenuPath}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="content_middle">
|
||||||
|
<div class="search_wrap">
|
||||||
|
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> 삭제</button>
|
||||||
|
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> 저장</button>
|
||||||
|
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> 이전</button>
|
||||||
|
</div>
|
||||||
|
<div class="title">암호화 모듈 설정 상세</div>
|
||||||
|
|
||||||
|
<form id="ajaxForm">
|
||||||
|
<input type="hidden" name="cryptoId" />
|
||||||
|
<table class="table_row" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<th style="width:20%;">모듈명 *</th>
|
||||||
|
<td><input type="text" name="cryptoName" style="width:300px;" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>설명</th>
|
||||||
|
<td><input type="text" name="cryptoDesc" style="width:400px;" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>알고리즘 *</th>
|
||||||
|
<td>
|
||||||
|
<select name="algType">
|
||||||
|
<option value="">선택</option>
|
||||||
|
<option value="AES">AES</option>
|
||||||
|
<option value="ARIA">ARIA</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>운영모드 *</th>
|
||||||
|
<td>
|
||||||
|
<select name="cipherMode">
|
||||||
|
<option value="">선택</option>
|
||||||
|
<option value="CBC">CBC</option>
|
||||||
|
<option value="GCM">GCM</option>
|
||||||
|
<option value="ECB">ECB</option>
|
||||||
|
<option value="FF1">FF1</option>
|
||||||
|
<option value="FF3-1">FF3-1</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>패딩</th>
|
||||||
|
<td>
|
||||||
|
<select name="padding">
|
||||||
|
<option value="">없음</option>
|
||||||
|
<option value="PKCS5Padding">PKCS5Padding</option>
|
||||||
|
<option value="NoPadding">NoPadding</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>IV (Hex)</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="ivHex" style="width:380px;" placeholder="HEX 직접 입력" />
|
||||||
|
<span id="ivHexLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
|
||||||
|
<br/>
|
||||||
|
<input type="text" id="ivHexText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
|
||||||
|
oninput="updateHexFromText(this, 'ivHex', 'ivHexLen')" />
|
||||||
|
<span class="help-inline">CBC: 16바이트(32자) 필수 / GCM: 설정 시 기본 AAD로 사용</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>키 소스 유형 *</th>
|
||||||
|
<td>
|
||||||
|
<select name="keySourceType">
|
||||||
|
<option value="">선택</option>
|
||||||
|
<option value="STATIC">STATIC</option>
|
||||||
|
<option value="DYNAMIC">DYNAMIC</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- STATIC 전용 -->
|
||||||
|
<tr class="static-section">
|
||||||
|
<th>암호화 키 (Hex) *</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="encKeyHex" style="width:380px;" placeholder="HEX 직접 입력" />
|
||||||
|
<span id="encKeyLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
|
||||||
|
<br/>
|
||||||
|
<input type="text" id="encKeyText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
|
||||||
|
oninput="updateHexFromText(this, 'encKeyHex', 'encKeyLen')" />
|
||||||
|
<span class="help-inline">AES-128: 16bytes(32자) / AES-192: 24bytes(48자) / AES-256: 32bytes(64자)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="static-section">
|
||||||
|
<th>복호화 키 (Hex)</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="decKeyHex" style="width:380px;" placeholder="HEX 직접 입력" />
|
||||||
|
<span id="decKeyLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
|
||||||
|
<br/>
|
||||||
|
<input type="text" id="decKeyText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
|
||||||
|
oninput="updateHexFromText(this, 'decKeyHex', 'decKeyLen')" />
|
||||||
|
<span class="help-inline">미입력 시 암호화 키와 동일</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- DYNAMIC 전용 -->
|
||||||
|
<tr class="dynamic-section">
|
||||||
|
<th>키 도출 전략 (FQCN) *</th>
|
||||||
|
<td><input type="text" name="keyDerivStrategy" style="width:500px;" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="dynamic-section">
|
||||||
|
<th>키 도출 파라미터 (JSON) *</th>
|
||||||
|
<td><textarea name="keyDerivParams" style="width:500px; height:80px;"></textarea></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>동적 키 캐시 *</th>
|
||||||
|
<td>
|
||||||
|
<select name="cacheYn">
|
||||||
|
<option value="N">N</option>
|
||||||
|
<option value="Y">Y</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>캐시 TTL (초)</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" name="cacheTtlSec" style="width:100px;" />
|
||||||
|
<span class="help-inline">기본값 300</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>사용 여부 *</th>
|
||||||
|
<td>
|
||||||
|
<select name="useYn">
|
||||||
|
<option value="Y">Y</option>
|
||||||
|
<option value="N">N</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="row_modified_info" style="display:none;">
|
||||||
|
<th>수정자 / 수정일시</th>
|
||||||
|
<td>
|
||||||
|
<span id="span_modified_by" style="font-weight:bold;"></span>
|
||||||
|
<span style="margin:0 8px; color:#ccc;">|</span>
|
||||||
|
<span id="span_modified_at" style="color:#555;"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -17,11 +17,52 @@
|
|||||||
<script language="javascript" >
|
<script language="javascript" >
|
||||||
var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />';
|
var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />';
|
||||||
var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />';
|
var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />';
|
||||||
|
var hardDeleteEnabled = ${hardDeleteEnabled};
|
||||||
const APPROVAL_TYPE_DISPLAY = {
|
const APPROVAL_TYPE_DISPLAY = {
|
||||||
'USER': '법인사용자',
|
'USER': '법인사용자',
|
||||||
'APP': 'API 사용'
|
'APP': 'API 사용'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function deleteSelectedApprovals() {
|
||||||
|
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
|
||||||
|
|
||||||
|
if (selectedIds.length === 0) {
|
||||||
|
showAlert("삭제할 승인 요청을 선택해주세요.", {type: 'warning'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hardDeleteEnabled) {
|
||||||
|
showAlert("완전삭제 기능이 비활성화되어 있습니다.", {type: 'error'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmMsg = "선택된 " + selectedIds.length + "건을 완전삭제(DB 영구 삭제)하시겠습니까?";
|
||||||
|
showConfirm(confirmMsg, {
|
||||||
|
type: 'error',
|
||||||
|
onConfirm: function() {
|
||||||
|
showConfirm("최종 확인: 완전삭제는 되돌릴 수 없습니다. 정말 실행하시겠습니까?", {
|
||||||
|
type: 'error',
|
||||||
|
title: '최종 확인',
|
||||||
|
onConfirm: function() {
|
||||||
|
$.ajax({
|
||||||
|
type: "POST", url: url, dataType: "json",
|
||||||
|
data: { cmd: 'HARD_DELETE_MULTIPLE', ids: selectedIds.join(',') },
|
||||||
|
success: function(data) {
|
||||||
|
showAlert(data.message, {
|
||||||
|
type: data.status === 'success' ? 'success' : 'error',
|
||||||
|
onClose: function() { $("#grid").trigger("reloadGrid"); }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function(e) {
|
||||||
|
showAlert("완전삭제 오류: " + e.responseText, {type: 'error'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('#grid').jqGrid({
|
$('#grid').jqGrid({
|
||||||
datatype: "json",
|
datatype: "json",
|
||||||
@@ -53,6 +94,8 @@
|
|||||||
viewrecords: true,
|
viewrecords: true,
|
||||||
autowidth: true,
|
autowidth: true,
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
|
multiselect: true,
|
||||||
|
multiboxonly: true,
|
||||||
ondblClickRow: function(rowId) {
|
ondblClickRow: function(rowId) {
|
||||||
var rowData = $(this).getRowData(rowId);
|
var rowData = $(this).getRowData(rowId);
|
||||||
var id = rowData['id'];
|
var id = rowData['id'];
|
||||||
@@ -93,6 +136,10 @@
|
|||||||
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
|
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#btn_delete_selected").click(function() {
|
||||||
|
deleteSelectedApprovals();
|
||||||
|
});
|
||||||
|
|
||||||
$("select[name^=search], input[name^=search]").keydown(function(key) {
|
$("select[name^=search], input[name^=search]").keydown(function(key) {
|
||||||
if (key.keyCode == 13) {
|
if (key.keyCode == 13) {
|
||||||
$("#btn_search").click();
|
$("#btn_search").click();
|
||||||
@@ -112,9 +159,17 @@
|
|||||||
</div><!-- end content_top -->
|
</div><!-- end content_top -->
|
||||||
<div class="content_middle" id="content_middle">
|
<div class="content_middle" id="content_middle">
|
||||||
<div class="search_wrap">
|
<div class="search_wrap">
|
||||||
|
<c:if test="${hardDeleteEnabled}">
|
||||||
|
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
|
||||||
|
</c:if>
|
||||||
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="title">승인 요청 목록<span class="tooltip">승인 요청을 관리합니다.</span></div>
|
<div class="title">승인 요청 목록<span class="tooltip">승인 요청을 관리합니다.</span></div>
|
||||||
|
<c:if test="${hardDeleteEnabled}">
|
||||||
|
<div style="background:#fff3cd; border:1px solid #ffc107; padding:8px 15px; margin:5px 0; border-radius:4px; color:#856404;">
|
||||||
|
<strong>[주의]</strong> 선택 삭제 시 승인 요청과 승인자 이력이 DB에서 영구 삭제됩니다.
|
||||||
|
</div>
|
||||||
|
</c:if>
|
||||||
<form id="ajaxForm" onsubmit="return false;">
|
<form id="ajaxForm" onsubmit="return false;">
|
||||||
<table class="search_condition" cellspacing="0">
|
<table class="search_condition" cellspacing="0">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -17,6 +17,17 @@
|
|||||||
<script language="javascript" >
|
<script language="javascript" >
|
||||||
var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />';
|
var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />';
|
||||||
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
|
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
|
||||||
|
var combo;
|
||||||
|
|
||||||
|
function formatNoticeType(cellvalue, options, rowObject) {
|
||||||
|
var name = "";
|
||||||
|
for (var i = 0; i < combo.noticeTypeList.length; i++) {
|
||||||
|
if (combo.noticeTypeList[i].CODE == cellvalue) {
|
||||||
|
return combo.noticeTypeList[i].NAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cellvalue;
|
||||||
|
}
|
||||||
|
|
||||||
function formatuseYn(cellvalue, options, rowObject) {
|
function formatuseYn(cellvalue, options, rowObject) {
|
||||||
return cellvalue === 'N'
|
return cellvalue === 'N'
|
||||||
@@ -24,27 +35,57 @@
|
|||||||
: '<span>사용</span>';
|
: '<span>사용</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatFixYn(cellvalue, options, rowObject) {
|
||||||
|
return cellvalue === 'Y'
|
||||||
|
? '고정'
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
function formatFile(cellvalue, options, rowObject) {
|
function formatFile(cellvalue, options, rowObject) {
|
||||||
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
|
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
|
||||||
var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : '';
|
var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : '';
|
||||||
return cellvalue + icon;
|
return cellvalue + icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function init(){
|
||||||
|
$.ajax({
|
||||||
|
type : "POST",
|
||||||
|
url:url,
|
||||||
|
dataType:"json",
|
||||||
|
data:{cmd: 'LIST_INIT_COMBO'},
|
||||||
|
success:function(json){
|
||||||
|
console.log('init', json);
|
||||||
|
combo = json;
|
||||||
|
new makeOptions("CODE","NAME").setObj($("select[name=searchNoticeType]")).setNoValueInclude(true).setNoValue('','<%=localeMessage.getString("combo.all")%>').setData(json.noticeTypeList).rendering();
|
||||||
|
|
||||||
|
putSelectFromParam();
|
||||||
|
},
|
||||||
|
error:function(e){
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
$('#grid').jqGrid({
|
$('#grid').jqGrid({
|
||||||
datatype:"json",
|
datatype:"json",
|
||||||
mtype: 'POST',
|
mtype: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
postData : { cmd : 'LIST',
|
postData : { cmd : 'LIST',
|
||||||
|
searchNoticeType: $('select[name=searchNoticeType]').val(),
|
||||||
searchUseYn: $('select[name=searchUseYn]').val(),
|
searchUseYn: $('select[name=searchUseYn]').val(),
|
||||||
searchNoticeSubject: $('input[name=searchNoticeSubject]').val(),
|
searchNoticeSubject: $('input[name=searchNoticeSubject]').val(),
|
||||||
searchNoticeDetail: $('input[name=searchNoticeDetail]').val()
|
searchNoticeDetail: $('input[name=searchNoticeDetail]').val()
|
||||||
},
|
},
|
||||||
colNames:['id', 'No.', '제목', '사용', '마지막 수정일', '등록일', '등록자', '조회수'],
|
colNames:['id', 'No.', '게시유형', '제목', '고정여부', '사용', '마지막 수정일', '등록일', '등록자', '조회수'],
|
||||||
colModel:[
|
colModel:[
|
||||||
{ name : 'id' , align:'center', key:true, hidden:true},
|
{ name : 'id' , align:'center', key:true, hidden:true},
|
||||||
{ name : 'rowNum' , align:'center', width:80 },
|
{ name : 'rowNum' , align:'center', width:60 },
|
||||||
{ name : 'noticeSubject' , align:'left' , width:200 , formatter: formatFile },
|
{ name : 'noticeType' , align:'center', width:60, formatter: formatNoticeType },
|
||||||
|
{ name : 'noticeSubject' , align:'left' , width:300, formatter: formatFile },
|
||||||
|
{ name : 'fixYn' , align:'center', width:60, formatter: formatFixYn },
|
||||||
{ name : 'useYn' , align:'center', width:60, formatter: formatuseYn },
|
{ name : 'useYn' , align:'center', width:60, formatter: formatuseYn },
|
||||||
{ name : 'lastModifiedDate', align:'center', width:120 , formatter: timeStampFormat},
|
{ name : 'lastModifiedDate', align:'center', width:120 , formatter: timeStampFormat},
|
||||||
{ name : 'createdDate' , align:'center', width:120, formatter: timeStampFormat },
|
{ name : 'createdDate' , align:'center', width:120, formatter: timeStampFormat },
|
||||||
@@ -98,6 +139,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
resizeJqGridWidth('grid','content_middle','1000');
|
resizeJqGridWidth('grid','content_middle','1000');
|
||||||
|
|
||||||
$("#btn_search").click(function(){
|
$("#btn_search").click(function(){
|
||||||
@@ -140,9 +183,25 @@
|
|||||||
<div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div>
|
<div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div>
|
||||||
<form id="ajaxForm" onsubmit="return false;">
|
<form id="ajaxForm" onsubmit="return false;">
|
||||||
<table class="search_condition" cellspacing=0;>
|
<table class="search_condition" cellspacing=0;>
|
||||||
|
<colgroup>
|
||||||
|
<col style="width:180px;">
|
||||||
|
<col style="width:240px;">
|
||||||
|
<col style="width:180px;">
|
||||||
|
<col style="width:240px;">
|
||||||
|
<col style="width:180px;">
|
||||||
|
<col style="width:240px;">
|
||||||
|
</colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:180px;">사용여부</th>
|
<th>게시유형</th>
|
||||||
|
<td>
|
||||||
|
<div class="select-style">
|
||||||
|
<select name="searchNoticeType"></select>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<th>제목/내용</th>
|
||||||
|
<td><input type="text" name="searchSubjectDetail"></td>
|
||||||
|
<th>사용여부</th>
|
||||||
<td>
|
<td>
|
||||||
<div class="select-style">
|
<div class="select-style">
|
||||||
<select name="searchUseYn">
|
<select name="searchUseYn">
|
||||||
@@ -152,8 +211,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<th style="width:180px;">제목/내용</th>
|
|
||||||
<td><input type="text" name="searchSubjectDetail"></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -24,7 +24,36 @@
|
|||||||
var fileInfo = null;
|
var fileInfo = null;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
var key = "${param.id}";
|
||||||
|
isDetail = key != "" && key != "null";
|
||||||
|
|
||||||
|
if (isDetail) {
|
||||||
|
$("#title").append(" 수정");
|
||||||
|
buttonControl(true);
|
||||||
|
} else {
|
||||||
|
$("#title").append(" 등록");
|
||||||
|
$("#btn_modify").html('<i class="material-icons">save</i> <%= localeMessage.getString("button.register") %>');
|
||||||
|
buttonControl(false);
|
||||||
|
}
|
||||||
|
|
||||||
// 초기화 로직
|
// 초기화 로직
|
||||||
|
$.ajax({
|
||||||
|
type : "POST",
|
||||||
|
url:url,
|
||||||
|
dataType:"json",
|
||||||
|
data:{cmd: 'LIST_INIT_COMBO'},
|
||||||
|
success:function(json){
|
||||||
|
combo = json;
|
||||||
|
new makeOptions("CODE","NAME").setObj($("select[name=noticeType]")).setNoValueInclude(false).setData(json.noticeTypeList).rendering();
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
detail(key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error:function(e){
|
||||||
|
alert(e.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeHTMLEntities(text) {
|
function decodeHTMLEntities(text) {
|
||||||
@@ -98,8 +127,10 @@
|
|||||||
data: {cmd: 'DETAIL', id: key},
|
data: {cmd: 'DETAIL', id: key},
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
$("#id").val(key);
|
$("#id").val(key);
|
||||||
|
$('#noticeType').val(data.noticeType);
|
||||||
$("#noticeSubject").val(data.noticeSubject);
|
$("#noticeSubject").val(data.noticeSubject);
|
||||||
$("#useYn").prop("checked", data.useYn === "Y");
|
$("#useYn").prop("checked", data.useYn === "Y");
|
||||||
|
$("#fixYn").prop("checked", data.fixYn === "Y");
|
||||||
|
|
||||||
var decodedContent = decodeHTMLEntities(data.noticeDetail);
|
var decodedContent = decodeHTMLEntities(data.noticeDetail);
|
||||||
$('#contents').summernote('code', decodedContent);
|
$('#contents').summernote('code', decodedContent);
|
||||||
@@ -123,29 +154,16 @@
|
|||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
var returnUrl = getReturnUrlForReturn();
|
var returnUrl = getReturnUrlForReturn();
|
||||||
var key = "${param.id}";
|
|
||||||
isDetail = key != "" && key != "null";
|
|
||||||
|
|
||||||
if (isDetail) {
|
|
||||||
$("#title").append(" 수정");
|
|
||||||
buttonControl(true);
|
|
||||||
} else {
|
|
||||||
$("#title").append(" 등록");
|
|
||||||
$("#btn_modify").html('<i class="material-icons">save</i> <%= localeMessage.getString("button.register") %>');
|
|
||||||
buttonControl(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
|
||||||
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
|
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
|
||||||
initSummernote('#contents', {
|
initSummernote('#contents', {
|
||||||
placeholder: '여기에 내용을 입력하세요.',
|
placeholder: '여기에 내용을 입력하세요.',
|
||||||
height: 300
|
height: 300
|
||||||
});
|
});
|
||||||
|
|
||||||
if (key) {
|
|
||||||
detail(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#fileInput').change(function(e) {
|
$('#fileInput').change(function(e) {
|
||||||
var file = e.target.files[0];
|
var file = e.target.files[0];
|
||||||
@@ -175,6 +193,7 @@
|
|||||||
var formData = new FormData($("#ajaxForm")[0]);
|
var formData = new FormData($("#ajaxForm")[0]);
|
||||||
|
|
||||||
formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N");
|
formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N");
|
||||||
|
formData.set("fixYn", $("#fixYn").is(":checked") ? "Y" : "N");
|
||||||
formData.set("noticeDetail", $('#contents').summernote('code'));
|
formData.set("noticeDetail", $('#contents').summernote('code'));
|
||||||
|
|
||||||
formData.append("cmd", isDetail ? "UPDATE" : "INSERT");
|
formData.append("cmd", isDetail ? "UPDATE" : "INSERT");
|
||||||
@@ -244,31 +263,50 @@
|
|||||||
<form id="ajaxForm" enctype="multipart/form-data" accept-charset="UTF-8">
|
<form id="ajaxForm" enctype="multipart/form-data" accept-charset="UTF-8">
|
||||||
<input type="hidden" name="id" id="id">
|
<input type="hidden" name="id" id="id">
|
||||||
<table class="table_row" cellspacing="0">
|
<table class="table_row" cellspacing="0">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 10%"/>
|
||||||
|
<col style="width: 40%"/>
|
||||||
|
<col style="width: 10%"/>
|
||||||
|
<col style="width: 40%"/>
|
||||||
|
</colgroup>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:20%;">사용여부</th>
|
<th>게시유형</th>
|
||||||
<td>
|
<td colspan="3">
|
||||||
<input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용</td>
|
<div class="select-style" >
|
||||||
</tr>
|
<select name="noticeType" id="noticeType"></select>
|
||||||
<tr>
|
</div>
|
||||||
<th style="width:20%;">제목 <font color="red">*</font></th>
|
|
||||||
<td><input type="text" id="noticeSubject" name="noticeSubject" style="width:100%" data-required data-warning="제목을 입력하여 주십시오."/></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th style="width:20%;">본문 <font color="red">*</font></th>
|
|
||||||
<td>
|
|
||||||
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:20%;">첨부파일</th>
|
<th>제목 <font color="red">*</font></th>
|
||||||
|
<td colspan="3"><input type="text" id="noticeSubject" name="noticeSubject" style="width:100%" data-required data-warning="제목을 입력하여 주십시오."/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>사용여부</th>
|
||||||
<td>
|
<td>
|
||||||
|
<input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용
|
||||||
|
</td>
|
||||||
|
<th>고정여부</th>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="fixYn" id="fixYn" value="Y" ${portalNoticeUI.fixYn eq 'Y' ? 'checked' : ''}> 고정
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>본문 <font color="red">*</font></th>
|
||||||
|
<td colspan="3">
|
||||||
|
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- <tr>
|
||||||
|
<th>첨부파일</th>
|
||||||
|
<td colspan="3">
|
||||||
<div style="margin: 5px 0px; display: inline-block;">
|
<div style="margin: 5px 0px; display: inline-block;">
|
||||||
<input type="file" id="fileInput" name="files" style="display:none;" />
|
<input type="file" id="fileInput" name="files" style="display:none;" />
|
||||||
<button type="button" id="addFile" class="cssbtn smallBtn">파일 선택</button>
|
<button type="button" id="addFile" class="cssbtn smallBtn">파일 선택</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="attachFiles" style="margin: 5px 0px; display: inline-block;"></div>
|
<div id="attachFiles" style="margin: 5px 0px; display: inline-block;"></div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr> -->
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,6 +82,54 @@
|
|||||||
</style>
|
</style>
|
||||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||||
<script language="javascript">
|
<script language="javascript">
|
||||||
|
function executeAggregationHourly() {
|
||||||
|
var targetDate = $("#targetHour").val().replace(/-/g, "");
|
||||||
|
var resultDiv = $("#hourlyResult");
|
||||||
|
|
||||||
|
// 날짜 입력 검증
|
||||||
|
if (!targetDate || targetDate.trim() === '') {
|
||||||
|
alert('대상 날짜를 입력하세요. (예: 20250120)');
|
||||||
|
$("#targetHour").focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultDiv.hide();
|
||||||
|
$("#btn_execute_hourly").prop("disabled", true).text("실행중...");
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '<c:url value="/onl/kjb/statistics/apiStatsAggregationMan.json"/>',
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
cmd: 'AGGREGATION_HOUR',
|
||||||
|
targetDate: targetDate,
|
||||||
|
serviceType: '${param.serviceType}'
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
resultDiv.removeClass("error").addClass("success");
|
||||||
|
var message = response.message + "\n";
|
||||||
|
message += "대상 날짜: " + response.targetDate + "\n";
|
||||||
|
message += "처리 건수: " + response.processedCount;
|
||||||
|
resultDiv.find("pre").text(message);
|
||||||
|
resultDiv.show();
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
resultDiv.removeClass("success").addClass("error");
|
||||||
|
var message = "집계 실행 실패\n";
|
||||||
|
try {
|
||||||
|
var error = JSON.parse(xhr.responseText);
|
||||||
|
message += error.message;
|
||||||
|
} catch(e) {
|
||||||
|
message += xhr.responseText || "알 수 없는 오류가 발생했습니다.";
|
||||||
|
}
|
||||||
|
resultDiv.find("pre").text(message);
|
||||||
|
resultDiv.show();
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
$("#btn_execute_hourly").prop("disabled", false).text("집계 실행");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function executeHourlyToDaily() {
|
function executeHourlyToDaily() {
|
||||||
var targetDate = $("#targetDate").val().replace(/-/g, "");
|
var targetDate = $("#targetDate").val().replace(/-/g, "");
|
||||||
var resultDiv = $("#dailyResult");
|
var resultDiv = $("#dailyResult");
|
||||||
@@ -241,6 +289,7 @@
|
|||||||
var year = yesterday.getFullYear();
|
var year = yesterday.getFullYear();
|
||||||
var month = String(yesterday.getMonth() + 1).padStart(2, '0');
|
var month = String(yesterday.getMonth() + 1).padStart(2, '0');
|
||||||
var day = String(yesterday.getDate()).padStart(2, '0');
|
var day = String(yesterday.getDate()).padStart(2, '0');
|
||||||
|
$("#targetHour").val(year + month + day);
|
||||||
$("#targetDate").val(year + month + day);
|
$("#targetDate").val(year + month + day);
|
||||||
|
|
||||||
// 전월
|
// 전월
|
||||||
@@ -255,6 +304,12 @@
|
|||||||
$("#targetYear").val(lastYear);
|
$("#targetYear").val(lastYear);
|
||||||
|
|
||||||
// 버튼 이벤트
|
// 버튼 이벤트
|
||||||
|
$("#btn_execute_hourly").click(function() {
|
||||||
|
if (confirm("시간별 통계 집계를 실행하시겠습니까?")) {
|
||||||
|
executeAggregationHourly();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$("#btn_execute_daily").click(function() {
|
$("#btn_execute_daily").click(function() {
|
||||||
if (confirm("시간별→일별 통계 집계를 실행하시겠습니까?")) {
|
if (confirm("시간별→일별 통계 집계를 실행하시겠습니까?")) {
|
||||||
executeHourlyToDaily();
|
executeHourlyToDaily();
|
||||||
@@ -297,9 +352,26 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 거래로그 → 시간별 집계 -->
|
||||||
|
<div class="aggregation-section">
|
||||||
|
<h3>1. 거래로그 → 시간별 통계 집계</h3>
|
||||||
|
<div class="aggregation-form">
|
||||||
|
<label>대상 날짜:</label>
|
||||||
|
<input type="text" id="targetHour" placeholder="yyyyMMdd" maxlength="8">
|
||||||
|
<span style="color: #666; font-size: 12px;">(예: 20250120)</span>
|
||||||
|
<button type="button" class="cssbtn" id="btn_execute_hourly" level="W">
|
||||||
|
<i class="material-icons">play_arrow</i> 집계 실행
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="hourlyResult" class="result-area">
|
||||||
|
<strong>실행 결과:</strong>
|
||||||
|
<pre></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 시간별 → 일별 집계 -->
|
<!-- 시간별 → 일별 집계 -->
|
||||||
<div class="aggregation-section">
|
<div class="aggregation-section">
|
||||||
<h3>1. 시간별 → 일별 통계 집계</h3>
|
<h3>2. 시간별 → 일별 통계 집계</h3>
|
||||||
<div class="aggregation-form">
|
<div class="aggregation-form">
|
||||||
<label>대상 날짜:</label>
|
<label>대상 날짜:</label>
|
||||||
<input type="text" id="targetDate" placeholder="yyyyMMdd" maxlength="8">
|
<input type="text" id="targetDate" placeholder="yyyyMMdd" maxlength="8">
|
||||||
@@ -316,7 +388,7 @@
|
|||||||
|
|
||||||
<!-- 일별 → 월별 집계 -->
|
<!-- 일별 → 월별 집계 -->
|
||||||
<div class="aggregation-section">
|
<div class="aggregation-section">
|
||||||
<h3>2. 일별 → 월별 통계 집계</h3>
|
<h3>3. 일별 → 월별 통계 집계</h3>
|
||||||
<div class="aggregation-form">
|
<div class="aggregation-form">
|
||||||
<label>대상 월:</label>
|
<label>대상 월:</label>
|
||||||
<input type="text" id="targetMonth" placeholder="yyyyMM" maxlength="6">
|
<input type="text" id="targetMonth" placeholder="yyyyMM" maxlength="6">
|
||||||
@@ -333,7 +405,7 @@
|
|||||||
|
|
||||||
<!-- 월별 → 연별 집계 -->
|
<!-- 월별 → 연별 집계 -->
|
||||||
<div class="aggregation-section">
|
<div class="aggregation-section">
|
||||||
<h3>3. 월별 → 연별 통계 집계</h3>
|
<h3>4. 월별 → 연별 통계 집계</h3>
|
||||||
<div class="aggregation-form">
|
<div class="aggregation-form">
|
||||||
<label>대상 연도:</label>
|
<label>대상 연도:</label>
|
||||||
<input type="text" id="targetYear" placeholder="yyyy" maxlength="4">
|
<input type="text" id="targetYear" placeholder="yyyy" maxlength="4">
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
var url = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.json"/>';
|
var url = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.json"/>';
|
||||||
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.view"/>';
|
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.view"/>';
|
||||||
|
|
||||||
var totalDonutChart, seq900DonutChart, callChart, respChart;
|
var totalDonutChart, callChart;
|
||||||
|
|
||||||
function numberFormatter(cellvalue, options, rowObject) {
|
function numberFormatter(cellvalue, options, rowObject) {
|
||||||
if (cellvalue == null || cellvalue == '') return '0';
|
if (cellvalue == null || cellvalue == '') return '0';
|
||||||
@@ -54,9 +54,7 @@
|
|||||||
|
|
||||||
function initCharts() {
|
function initCharts() {
|
||||||
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
|
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
|
||||||
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
|
|
||||||
callChart = echarts.init(document.getElementById('callChart'));
|
callChart = echarts.init(document.getElementById('callChart'));
|
||||||
respChart = echarts.init(document.getElementById('respChart'));
|
|
||||||
|
|
||||||
// 총건수 도넛 차트
|
// 총건수 도넛 차트
|
||||||
var totalDonutOption = {
|
var totalDonutOption = {
|
||||||
@@ -73,7 +71,7 @@
|
|||||||
legend: {
|
legend: {
|
||||||
orient: 'horizontal',
|
orient: 'horizontal',
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
data: ['성공', 'Timeout', '시스템오류', '업무오류']
|
data: ['성공', 'Timeout', '시스템오류']
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: '총건수',
|
name: '총건수',
|
||||||
@@ -98,8 +96,7 @@
|
|||||||
data: [
|
data: [
|
||||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
|
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||||
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
|
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
graphic: [{
|
graphic: [{
|
||||||
@@ -118,64 +115,7 @@
|
|||||||
|
|
||||||
totalDonutChart.setOption(totalDonutOption);
|
totalDonutChart.setOption(totalDonutOption);
|
||||||
|
|
||||||
// Seq900 도넛 차트
|
|
||||||
var seq900DonutOption = {
|
|
||||||
title: {
|
|
||||||
text: 'Seq900 에러 분포',
|
|
||||||
left: 'center',
|
|
||||||
top: 10,
|
|
||||||
textStyle: { fontSize: 14 }
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: '{b}: {c} ({d}%)'
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
orient: 'horizontal',
|
|
||||||
bottom: 10,
|
|
||||||
data: ['Timeout', '시스템오류', '업무오류']
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
name: 'Seq900 에러',
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['40%', '70%'],
|
|
||||||
center: ['50%', '50%'],
|
|
||||||
avoidLabelOverlap: true,
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
formatter: '{b}: {c}'
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
|
|
||||||
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
type: 'text',
|
|
||||||
left: 'center',
|
|
||||||
top: 'center',
|
|
||||||
style: {
|
|
||||||
text: '0',
|
|
||||||
textAlign: 'center',
|
|
||||||
fill: '#333',
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
seq900DonutChart.setOption(seq900DonutOption);
|
|
||||||
|
|
||||||
// 호출량 차트
|
// 호출량 차트
|
||||||
var callOption = {
|
var callOption = {
|
||||||
@@ -191,32 +131,13 @@
|
|||||||
series: [
|
series: [
|
||||||
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
|
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
|
||||||
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
|
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
|
||||||
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] },
|
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
|
||||||
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
callChart.setOption(callOption);
|
callChart.setOption(callOption);
|
||||||
|
|
||||||
// 응답시간 차트
|
|
||||||
var respOption = {
|
|
||||||
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
|
||||||
},
|
|
||||||
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
|
||||||
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
|
||||||
yAxis: { type: 'value' },
|
|
||||||
series: [
|
|
||||||
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
|
|
||||||
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
|
|
||||||
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
respChart.setOption(respOption);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCharts(data) {
|
function updateCharts(data) {
|
||||||
@@ -268,8 +189,7 @@
|
|||||||
data: [
|
data: [
|
||||||
{ value: totalSuccess, name: '성공' },
|
{ value: totalSuccess, name: '성공' },
|
||||||
{ value: totalTimeout, name: 'Timeout' },
|
{ value: totalTimeout, name: 'Timeout' },
|
||||||
{ value: totalSystemErr, name: '시스템오류' },
|
{ value: totalSystemErr, name: '시스템오류' }
|
||||||
{ value: totalBizErr, name: '업무오류' }
|
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
graphic: [{
|
graphic: [{
|
||||||
@@ -279,22 +199,6 @@
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seq900 도넛 차트 업데이트
|
|
||||||
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
|
|
||||||
seq900DonutChart.setOption({
|
|
||||||
series: [{
|
|
||||||
data: [
|
|
||||||
{ value: seq900Timeout, name: 'Timeout' },
|
|
||||||
{ value: seq900SystemErr, name: '시스템오류' },
|
|
||||||
{ value: seq900BizErr, name: '업무오류' }
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
style: {
|
|
||||||
text: seq900Total.toLocaleString()
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 호출량 차트 업데이트
|
// 호출량 차트 업데이트
|
||||||
callChart.setOption({
|
callChart.setOption({
|
||||||
@@ -302,24 +206,13 @@
|
|||||||
series: [
|
series: [
|
||||||
{ data: successData },
|
{ data: successData },
|
||||||
{ data: timeoutData },
|
{ data: timeoutData },
|
||||||
{ data: systemErrData },
|
{ data: systemErrData }
|
||||||
{ data: bizErrData }
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// 응답시간 차트 업데이트
|
|
||||||
respChart.setOption({
|
|
||||||
xAxis: { data: times.map(function(t) { return t.substring(6, 8) + '일'; }) },
|
|
||||||
series: [
|
|
||||||
{ data: p95RespData },
|
|
||||||
{ data: p50RespData },
|
|
||||||
{ data: avgRespData }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 드릴다운을 위해 원본 데이터 저장
|
// 드릴다운을 위해 원본 데이터 저장
|
||||||
callChart.rawData = data;
|
callChart.rawData = data;
|
||||||
respChart.rawData = data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchChartData() {
|
function fetchChartData() {
|
||||||
@@ -535,8 +428,7 @@
|
|||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'API명',
|
'API명',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
@@ -545,10 +437,6 @@
|
|||||||
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
|
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
|
||||||
@@ -580,9 +468,8 @@
|
|||||||
colNames: [
|
colNames: [
|
||||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
||||||
'Inbound Adapter', 'Outbound Adapter',
|
'Inbound Adapter', 'Outbound Adapter',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
|
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
{ name: 'statTime', align: 'center', width: '120', sortable: false },
|
{ name: 'statTime', align: 'center', width: '120', sortable: false },
|
||||||
@@ -596,15 +483,9 @@
|
|||||||
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
|
||||||
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
|
|
||||||
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
|
|
||||||
],
|
],
|
||||||
jsonReader: { repeatitems: false },
|
jsonReader: { repeatitems: false },
|
||||||
pager: $('#pager'),
|
pager: $('#pager'),
|
||||||
@@ -685,9 +566,7 @@
|
|||||||
// 윈도우 리사이즈 시 차트 리사이즈
|
// 윈도우 리사이즈 시 차트 리사이즈
|
||||||
$(window).resize(function() {
|
$(window).resize(function() {
|
||||||
if (totalDonutChart) totalDonutChart.resize();
|
if (totalDonutChart) totalDonutChart.resize();
|
||||||
if (seq900DonutChart) seq900DonutChart.resize();
|
|
||||||
if (callChart) callChart.resize();
|
if (callChart) callChart.resize();
|
||||||
if (respChart) respChart.resize();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonControl();
|
buttonControl();
|
||||||
@@ -712,7 +591,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:100px;">조회기간</th>
|
<th style="width:100px;">조회기간</th>
|
||||||
<td colspan="3">
|
<td colspan="5">
|
||||||
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;">
|
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;">
|
||||||
~
|
~
|
||||||
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;">
|
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;">
|
||||||
@@ -724,22 +603,20 @@
|
|||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||||
</td>
|
</td>
|
||||||
|
<th style="width:100px;">업무구분</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
||||||
|
</td>
|
||||||
<th style="width:100px;">인스턴스</th>
|
<th style="width:100px;">인스턴스</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
|
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:100px;">업무구분</th>
|
|
||||||
<td>
|
|
||||||
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
|
||||||
</td>
|
|
||||||
<th style="width:100px;">클라이언트ID</th>
|
<th style="width:100px;">클라이언트ID</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchClientId" value="${param.searchClientId}">
|
<input type="text" name="searchClientId" value="${param.searchClientId}">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th style="width:100px;">Inbound Adapter</th>
|
<th style="width:100px;">Inbound Adapter</th>
|
||||||
<td>
|
<td>
|
||||||
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
|
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
|
||||||
@@ -754,12 +631,7 @@
|
|||||||
<!-- 도넛 차트 (상단 50%씩) -->
|
<!-- 도넛 차트 (상단 50%씩) -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div id="totalDonutChart" class="chart"></div>
|
<div id="totalDonutChart" class="chart"></div>
|
||||||
<div id="seq900DonutChart" class="chart"></div>
|
|
||||||
</div>
|
|
||||||
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
|
|
||||||
<div class="chart-container">
|
|
||||||
<div id="callChart" class="chart"></div>
|
<div id="callChart" class="chart"></div>
|
||||||
<div id="respChart" class="chart"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 요약 그리드 -->
|
<!-- 요약 그리드 -->
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>';
|
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>';
|
||||||
var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
|
var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
|
||||||
|
|
||||||
var totalDonutChart, seq900DonutChart, callChart, respChart;
|
var totalDonutChart, callChart;
|
||||||
|
|
||||||
function numberFormatter(cellvalue, options, rowObject) {
|
function numberFormatter(cellvalue, options, rowObject) {
|
||||||
if (cellvalue == null || cellvalue == '') return '0';
|
if (cellvalue == null || cellvalue == '') return '0';
|
||||||
@@ -55,9 +55,7 @@
|
|||||||
|
|
||||||
function initCharts() {
|
function initCharts() {
|
||||||
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
|
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
|
||||||
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
|
|
||||||
callChart = echarts.init(document.getElementById('callChart'));
|
callChart = echarts.init(document.getElementById('callChart'));
|
||||||
respChart = echarts.init(document.getElementById('respChart'));
|
|
||||||
|
|
||||||
// 총건수 도넛 차트
|
// 총건수 도넛 차트
|
||||||
var totalDonutOption = {
|
var totalDonutOption = {
|
||||||
@@ -74,7 +72,7 @@
|
|||||||
legend: {
|
legend: {
|
||||||
orient: 'horizontal',
|
orient: 'horizontal',
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
data: ['성공', 'Timeout', '시스템오류', '업무오류']
|
data: ['성공', 'Timeout', '시스템오류']
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: '총건수',
|
name: '총건수',
|
||||||
@@ -99,8 +97,7 @@
|
|||||||
data: [
|
data: [
|
||||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
|
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||||
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
|
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
graphic: [{
|
graphic: [{
|
||||||
@@ -119,64 +116,7 @@
|
|||||||
|
|
||||||
totalDonutChart.setOption(totalDonutOption);
|
totalDonutChart.setOption(totalDonutOption);
|
||||||
|
|
||||||
// Seq900 도넛 차트
|
|
||||||
var seq900DonutOption = {
|
|
||||||
title: {
|
|
||||||
text: 'Seq900 에러 분포',
|
|
||||||
left: 'center',
|
|
||||||
top: 10,
|
|
||||||
textStyle: { fontSize: 14 }
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: '{b}: {c} ({d}%)'
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
orient: 'horizontal',
|
|
||||||
bottom: 10,
|
|
||||||
data: ['Timeout', '시스템오류', '업무오류']
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
name: 'Seq900 에러',
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['40%', '70%'],
|
|
||||||
center: ['50%', '50%'],
|
|
||||||
avoidLabelOverlap: true,
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
formatter: '{b}: {c}'
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
|
|
||||||
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
type: 'text',
|
|
||||||
left: 'center',
|
|
||||||
top: 'center',
|
|
||||||
style: {
|
|
||||||
text: '0',
|
|
||||||
textAlign: 'center',
|
|
||||||
fill: '#333',
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
seq900DonutChart.setOption(seq900DonutOption);
|
|
||||||
|
|
||||||
// 호출량 차트
|
// 호출량 차트
|
||||||
var callOption = {
|
var callOption = {
|
||||||
@@ -185,47 +125,24 @@
|
|||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
||||||
},
|
},
|
||||||
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 },
|
legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
|
||||||
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
||||||
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
||||||
yAxis: { type: 'value', minInterval: 1 },
|
yAxis: { type: 'value', minInterval: 1 },
|
||||||
series: [
|
series: [
|
||||||
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
|
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
|
||||||
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
|
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
|
||||||
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] },
|
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
|
||||||
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
callChart.setOption(callOption);
|
callChart.setOption(callOption);
|
||||||
|
|
||||||
// 응답시간 차트
|
|
||||||
var respOption = {
|
|
||||||
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
|
||||||
},
|
|
||||||
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
|
||||||
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
|
||||||
yAxis: { type: 'value' },
|
|
||||||
series: [
|
|
||||||
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
|
|
||||||
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
|
|
||||||
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
respChart.setOption(respOption);
|
|
||||||
|
|
||||||
// 드릴다운 이벤트 (시간 클릭 시 분단위 화면으로 이동)
|
// 드릴다운 이벤트 (시간 클릭 시 분단위 화면으로 이동)
|
||||||
callChart.on('click', function(params) {
|
callChart.on('click', function(params) {
|
||||||
drillDownToMinute(callChart, params.dataIndex);
|
drillDownToMinute(callChart, params.dataIndex);
|
||||||
});
|
});
|
||||||
respChart.on('click', function(params) {
|
|
||||||
drillDownToMinute(respChart, params.dataIndex);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function drillDownToMinute(chart, dataIndex) {
|
function drillDownToMinute(chart, dataIndex) {
|
||||||
@@ -308,8 +225,7 @@
|
|||||||
data: [
|
data: [
|
||||||
{ value: totalSuccess, name: '성공' },
|
{ value: totalSuccess, name: '성공' },
|
||||||
{ value: totalTimeout, name: 'Timeout' },
|
{ value: totalTimeout, name: 'Timeout' },
|
||||||
{ value: totalSystemErr, name: '시스템오류' },
|
{ value: totalSystemErr, name: '시스템오류' }
|
||||||
{ value: totalBizErr, name: '업무오류' }
|
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
graphic: [{
|
graphic: [{
|
||||||
@@ -319,22 +235,6 @@
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seq900 도넛 차트 업데이트
|
|
||||||
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
|
|
||||||
seq900DonutChart.setOption({
|
|
||||||
series: [{
|
|
||||||
data: [
|
|
||||||
{ value: seq900Timeout, name: 'Timeout' },
|
|
||||||
{ value: seq900SystemErr, name: '시스템오류' },
|
|
||||||
{ value: seq900BizErr, name: '업무오류' }
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
style: {
|
|
||||||
text: seq900Total.toLocaleString()
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 호출량 차트 업데이트
|
// 호출량 차트 업데이트
|
||||||
callChart.setOption({
|
callChart.setOption({
|
||||||
@@ -342,24 +242,13 @@
|
|||||||
series: [
|
series: [
|
||||||
{ data: successData },
|
{ data: successData },
|
||||||
{ data: timeoutData },
|
{ data: timeoutData },
|
||||||
{ data: systemErrData },
|
{ data: systemErrData }
|
||||||
{ data: bizErrData }
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// 응답시간 차트 업데이트
|
|
||||||
respChart.setOption({
|
|
||||||
xAxis: { data: times.map(function(t) { return t.substring(8, 10) + '시'; }) },
|
|
||||||
series: [
|
|
||||||
{ data: p95RespData },
|
|
||||||
{ data: p50RespData },
|
|
||||||
{ data: avgRespData }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 드릴다운을 위해 원본 데이터 저장
|
// 드릴다운을 위해 원본 데이터 저장
|
||||||
callChart.rawData = data;
|
callChart.rawData = data;
|
||||||
respChart.rawData = data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchChartData() {
|
function fetchChartData() {
|
||||||
@@ -541,8 +430,7 @@
|
|||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'API명',
|
'API명',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
@@ -551,10 +439,6 @@
|
|||||||
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
|
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
|
||||||
@@ -586,9 +470,8 @@
|
|||||||
colNames: [
|
colNames: [
|
||||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
||||||
'Inbound Adapter', 'Outbound Adapter',
|
'Inbound Adapter', 'Outbound Adapter',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
|
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
{ name: 'statTime', align: 'center', width: '120', sortable: false },
|
{ name: 'statTime', align: 'center', width: '120', sortable: false },
|
||||||
@@ -602,15 +485,9 @@
|
|||||||
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
|
||||||
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
|
|
||||||
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
|
|
||||||
],
|
],
|
||||||
jsonReader: { repeatitems: false },
|
jsonReader: { repeatitems: false },
|
||||||
pager: $('#pager'),
|
pager: $('#pager'),
|
||||||
@@ -669,9 +546,7 @@
|
|||||||
// 윈도우 리사이즈 시 차트 리사이즈
|
// 윈도우 리사이즈 시 차트 리사이즈
|
||||||
$(window).resize(function() {
|
$(window).resize(function() {
|
||||||
if (totalDonutChart) totalDonutChart.resize();
|
if (totalDonutChart) totalDonutChart.resize();
|
||||||
if (seq900DonutChart) seq900DonutChart.resize();
|
|
||||||
if (callChart) callChart.resize();
|
if (callChart) callChart.resize();
|
||||||
if (respChart) respChart.resize();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonControl();
|
buttonControl();
|
||||||
@@ -736,12 +611,7 @@
|
|||||||
<!-- 도넛 차트 (상단 50%씩) -->
|
<!-- 도넛 차트 (상단 50%씩) -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div id="totalDonutChart" class="chart"></div>
|
<div id="totalDonutChart" class="chart"></div>
|
||||||
<div id="seq900DonutChart" class="chart"></div>
|
|
||||||
</div>
|
|
||||||
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
|
|
||||||
<div class="chart-container">
|
|
||||||
<div id="callChart" class="chart"></div>
|
<div id="callChart" class="chart"></div>
|
||||||
<div id="respChart" class="chart"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 요약 그리드 -->
|
<!-- 요약 그리드 -->
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
%>
|
%>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>분별 통계 조회</title>
|
<title>대시보드</title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||||
<style>
|
<style>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
var url = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.json"/>';
|
var url = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.json"/>';
|
||||||
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
|
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
|
||||||
|
|
||||||
var totalDonutChart, seq900DonutChart, callChart, respChart;
|
var totalDonutChart, callChart;
|
||||||
|
|
||||||
function numberFormatter(cellvalue, options, rowObject) {
|
function numberFormatter(cellvalue, options, rowObject) {
|
||||||
if (cellvalue == null || cellvalue == '') return '0';
|
if (cellvalue == null || cellvalue == '') return '0';
|
||||||
@@ -47,9 +47,7 @@
|
|||||||
|
|
||||||
function initCharts() {
|
function initCharts() {
|
||||||
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
|
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
|
||||||
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
|
|
||||||
callChart = echarts.init(document.getElementById('callChart'));
|
callChart = echarts.init(document.getElementById('callChart'));
|
||||||
respChart = echarts.init(document.getElementById('respChart'));
|
|
||||||
|
|
||||||
// 총건수 도넛 차트
|
// 총건수 도넛 차트
|
||||||
var totalDonutOption = {
|
var totalDonutOption = {
|
||||||
@@ -66,7 +64,7 @@
|
|||||||
legend: {
|
legend: {
|
||||||
orient: 'horizontal',
|
orient: 'horizontal',
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
data: ['성공', 'Timeout', '시스템오류', '업무오류']
|
data: ['성공', 'Timeout', '시스템오류']
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: '총건수',
|
name: '총건수',
|
||||||
@@ -91,8 +89,7 @@
|
|||||||
data: [
|
data: [
|
||||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
|
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||||
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
|
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
graphic: [{
|
graphic: [{
|
||||||
@@ -111,64 +108,7 @@
|
|||||||
|
|
||||||
totalDonutChart.setOption(totalDonutOption);
|
totalDonutChart.setOption(totalDonutOption);
|
||||||
|
|
||||||
// Seq900 도넛 차트
|
|
||||||
var seq900DonutOption = {
|
|
||||||
title: {
|
|
||||||
text: 'Seq900 에러 분포',
|
|
||||||
left: 'center',
|
|
||||||
top: 10,
|
|
||||||
textStyle: { fontSize: 14 }
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: '{b}: {c} ({d}%)'
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
orient: 'horizontal',
|
|
||||||
bottom: 10,
|
|
||||||
data: ['Timeout', '시스템오류', '업무오류']
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
name: 'Seq900 에러',
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['40%', '70%'],
|
|
||||||
center: ['50%', '50%'],
|
|
||||||
avoidLabelOverlap: true,
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
formatter: '{b}: {c}'
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
|
|
||||||
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
type: 'text',
|
|
||||||
left: 'center',
|
|
||||||
top: 'center',
|
|
||||||
style: {
|
|
||||||
text: '0',
|
|
||||||
textAlign: 'center',
|
|
||||||
fill: '#333',
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
seq900DonutChart.setOption(seq900DonutOption);
|
|
||||||
|
|
||||||
// 호출량 차트
|
// 호출량 차트
|
||||||
var callOption = {
|
var callOption = {
|
||||||
@@ -177,7 +117,7 @@
|
|||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
||||||
},
|
},
|
||||||
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 },
|
legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
|
||||||
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
||||||
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
||||||
yAxis: { type: 'value', minInterval: 1 },
|
yAxis: { type: 'value', minInterval: 1 },
|
||||||
@@ -188,34 +128,12 @@
|
|||||||
series: [
|
series: [
|
||||||
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false },
|
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false },
|
||||||
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [], showSymbol: false },
|
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [], showSymbol: false },
|
||||||
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [], showSymbol: false },
|
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [], showSymbol: false }
|
||||||
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [], showSymbol: false }
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
var respOption = {
|
|
||||||
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
|
||||||
},
|
|
||||||
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
|
||||||
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
|
||||||
yAxis: { type: 'value' },
|
|
||||||
dataZoom: [
|
|
||||||
{ type: 'inside', start: 0, end: 100 },
|
|
||||||
{ start: 0, end: 100 }
|
|
||||||
],
|
|
||||||
series: [
|
|
||||||
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [], showSymbol: false },
|
|
||||||
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false },
|
|
||||||
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [], showSymbol: false }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
callChart.setOption(callOption);
|
callChart.setOption(callOption);
|
||||||
respChart.setOption(respOption);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCharts(data) {
|
function updateCharts(data) {
|
||||||
@@ -269,8 +187,7 @@
|
|||||||
data: [
|
data: [
|
||||||
{ value: totalSuccess, name: '성공' },
|
{ value: totalSuccess, name: '성공' },
|
||||||
{ value: totalTimeout, name: 'Timeout' },
|
{ value: totalTimeout, name: 'Timeout' },
|
||||||
{ value: totalSystemErr, name: '시스템오류' },
|
{ value: totalSystemErr, name: '시스템오류' }
|
||||||
{ value: totalBizErr, name: '업무오류' }
|
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
graphic: [{
|
graphic: [{
|
||||||
@@ -280,22 +197,6 @@
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seq900 도넛 차트 업데이트
|
|
||||||
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
|
|
||||||
seq900DonutChart.setOption({
|
|
||||||
series: [{
|
|
||||||
data: [
|
|
||||||
{ value: seq900Timeout, name: 'Timeout' },
|
|
||||||
{ value: seq900SystemErr, name: '시스템오류' },
|
|
||||||
{ value: seq900BizErr, name: '업무오류' }
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
style: {
|
|
||||||
text: seq900Total.toLocaleString()
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 호출량 차트 업데이트
|
// 호출량 차트 업데이트
|
||||||
callChart.setOption({
|
callChart.setOption({
|
||||||
@@ -303,20 +204,10 @@
|
|||||||
series: [
|
series: [
|
||||||
{ data: successData },
|
{ data: successData },
|
||||||
{ data: timeoutData },
|
{ data: timeoutData },
|
||||||
{ data: systemErrData },
|
{ data: systemErrData }
|
||||||
{ data: bizErrData }
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// 응답시간 차트 업데이트
|
|
||||||
respChart.setOption({
|
|
||||||
xAxis: { data: times },
|
|
||||||
series: [
|
|
||||||
{ data: p95RespData },
|
|
||||||
{ data: p50RespData },
|
|
||||||
{ data: avgRespData }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 조회 기간 검증 및 조정 (최대 1시간)
|
// 조회 기간 검증 및 조정 (최대 1시간)
|
||||||
@@ -552,8 +443,7 @@
|
|||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'API명',
|
'API명',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
@@ -562,10 +452,6 @@
|
|||||||
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
|
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
|
||||||
@@ -597,9 +483,8 @@
|
|||||||
colNames: [
|
colNames: [
|
||||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
||||||
'Inbound Adapter', 'Outbound Adapter',
|
'Inbound Adapter', 'Outbound Adapter',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
|
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
{ name: 'statTime', align: 'center', width: '120', sortable: false },
|
{ name: 'statTime', align: 'center', width: '120', sortable: false },
|
||||||
@@ -613,15 +498,9 @@
|
|||||||
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
|
||||||
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
|
|
||||||
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
|
|
||||||
],
|
],
|
||||||
jsonReader: { repeatitems: false },
|
jsonReader: { repeatitems: false },
|
||||||
pager: $('#pager'),
|
pager: $('#pager'),
|
||||||
@@ -711,9 +590,7 @@
|
|||||||
// 윈도우 리사이즈 시 차트 리사이즈
|
// 윈도우 리사이즈 시 차트 리사이즈
|
||||||
$(window).resize(function() {
|
$(window).resize(function() {
|
||||||
if (totalDonutChart) totalDonutChart.resize();
|
if (totalDonutChart) totalDonutChart.resize();
|
||||||
if (seq900DonutChart) seq900DonutChart.resize();
|
|
||||||
if (callChart) callChart.resize();
|
if (callChart) callChart.resize();
|
||||||
if (respChart) respChart.resize();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonControl();
|
buttonControl();
|
||||||
@@ -733,7 +610,7 @@
|
|||||||
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
|
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="title" id="title">API 분단위 통계</div>
|
<div class="title" id="title">대시보드</div>
|
||||||
<table class="search_condition" cellspacing="0">
|
<table class="search_condition" cellspacing="0">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -747,72 +624,28 @@
|
|||||||
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 1시간, 초과시 자동 조정)</span>
|
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 1시간, 초과시 자동 조정)</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th style="width:100px;">API명</th>
|
|
||||||
<td>
|
|
||||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
|
||||||
</td>
|
|
||||||
<th style="width:100px;">인스턴스</th>
|
|
||||||
<td>
|
|
||||||
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th style="width:100px;">업무구분</th>
|
|
||||||
<td>
|
|
||||||
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
|
||||||
</td>
|
|
||||||
<th style="width:100px;">클라이언트ID</th>
|
|
||||||
<td>
|
|
||||||
<input type="text" name="searchClientId" value="${param.searchClientId}">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th style="width:100px;">Inbound Adapter</th>
|
|
||||||
<td>
|
|
||||||
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
|
|
||||||
</td>
|
|
||||||
<th style="width:100px;">Outbound Adapter</th>
|
|
||||||
<td>
|
|
||||||
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<!-- 도넛 차트 (상단 50%씩) -->
|
<!-- 도넛 차트 (상단 50%씩) -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div id="totalDonutChart" class="chart"></div>
|
<div id="totalDonutChart" class="chart"></div>
|
||||||
<div id="seq900DonutChart" class="chart"></div>
|
|
||||||
</div>
|
|
||||||
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
|
|
||||||
<div class="chart-container">
|
|
||||||
<div id="callChart" class="chart"></div>
|
<div id="callChart" class="chart"></div>
|
||||||
<div id="respChart" class="chart"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- 요약 그리드 -->
|
<!-- 요약 그리드 -->
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<div class="title" style="margin: 0;">API명별 요약 통계</div>
|
<div class="title" style="margin: 0;">API별 사용현황</div>
|
||||||
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
|
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
|
||||||
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
|
<i class="material-icons">file_download</i> Excel 다운로드
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<table id="gridSummary"></table>
|
<table id="gridSummary"></table>
|
||||||
<div id="pagerSummary"></div>
|
<div id="pagerSummary"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 상세 그리드 -->
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
|
||||||
<div class="title" style="margin: 0;">상세 통계</div>
|
|
||||||
<button type="button" class="cssbtn" id="btn_excel_export" level="R">
|
|
||||||
<i class="material-icons">file_download</i> Excel 다운로드 (상세)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<table id="grid"></table>
|
|
||||||
<div id="pager"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
var url = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.json"/>';
|
var url = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.json"/>';
|
||||||
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.view"/>';
|
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.view"/>';
|
||||||
|
|
||||||
var totalDonutChart, seq900DonutChart, callChart, respChart;
|
var totalDonutChart, callChart;
|
||||||
|
|
||||||
function numberFormatter(cellvalue, options, rowObject) {
|
function numberFormatter(cellvalue, options, rowObject) {
|
||||||
if (cellvalue == null || cellvalue == '') return '0';
|
if (cellvalue == null || cellvalue == '') return '0';
|
||||||
@@ -54,9 +54,7 @@
|
|||||||
|
|
||||||
function initCharts() {
|
function initCharts() {
|
||||||
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
|
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
|
||||||
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
|
|
||||||
callChart = echarts.init(document.getElementById('callChart'));
|
callChart = echarts.init(document.getElementById('callChart'));
|
||||||
respChart = echarts.init(document.getElementById('respChart'));
|
|
||||||
|
|
||||||
// 총건수 도넛 차트
|
// 총건수 도넛 차트
|
||||||
var totalDonutOption = {
|
var totalDonutOption = {
|
||||||
@@ -73,7 +71,7 @@
|
|||||||
legend: {
|
legend: {
|
||||||
orient: 'horizontal',
|
orient: 'horizontal',
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
data: ['성공', 'Timeout', '시스템오류', '업무오류']
|
data: ['성공', 'Timeout', '시스템오류']
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: '총건수',
|
name: '총건수',
|
||||||
@@ -98,8 +96,7 @@
|
|||||||
data: [
|
data: [
|
||||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
|
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||||
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
|
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
graphic: [{
|
graphic: [{
|
||||||
@@ -118,64 +115,7 @@
|
|||||||
|
|
||||||
totalDonutChart.setOption(totalDonutOption);
|
totalDonutChart.setOption(totalDonutOption);
|
||||||
|
|
||||||
// Seq900 도넛 차트
|
|
||||||
var seq900DonutOption = {
|
|
||||||
title: {
|
|
||||||
text: 'Seq900 에러 분포',
|
|
||||||
left: 'center',
|
|
||||||
top: 10,
|
|
||||||
textStyle: { fontSize: 14 }
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: '{b}: {c} ({d}%)'
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
orient: 'horizontal',
|
|
||||||
bottom: 10,
|
|
||||||
data: ['Timeout', '시스템오류', '업무오류']
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
name: 'Seq900 에러',
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['40%', '70%'],
|
|
||||||
center: ['50%', '50%'],
|
|
||||||
avoidLabelOverlap: true,
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
formatter: '{b}: {c}'
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
|
|
||||||
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
type: 'text',
|
|
||||||
left: 'center',
|
|
||||||
top: 'center',
|
|
||||||
style: {
|
|
||||||
text: '0',
|
|
||||||
textAlign: 'center',
|
|
||||||
fill: '#333',
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
seq900DonutChart.setOption(seq900DonutOption);
|
|
||||||
|
|
||||||
// 호출량 차트
|
// 호출량 차트
|
||||||
var callOption = {
|
var callOption = {
|
||||||
@@ -184,39 +124,18 @@
|
|||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
||||||
},
|
},
|
||||||
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 },
|
legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
|
||||||
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
||||||
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
||||||
yAxis: { type: 'value', minInterval: 1 },
|
yAxis: { type: 'value', minInterval: 1 },
|
||||||
series: [
|
series: [
|
||||||
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
|
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
|
||||||
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
|
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
|
||||||
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] },
|
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
|
||||||
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
callChart.setOption(callOption);
|
callChart.setOption(callOption);
|
||||||
|
|
||||||
// 응답시간 차트
|
|
||||||
var respOption = {
|
|
||||||
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
|
||||||
},
|
|
||||||
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
|
||||||
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
|
||||||
yAxis: { type: 'value' },
|
|
||||||
series: [
|
|
||||||
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
|
|
||||||
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
|
|
||||||
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
respChart.setOption(respOption);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCharts(data) {
|
function updateCharts(data) {
|
||||||
@@ -268,8 +187,7 @@
|
|||||||
data: [
|
data: [
|
||||||
{ value: totalSuccess, name: '성공' },
|
{ value: totalSuccess, name: '성공' },
|
||||||
{ value: totalTimeout, name: 'Timeout' },
|
{ value: totalTimeout, name: 'Timeout' },
|
||||||
{ value: totalSystemErr, name: '시스템오류' },
|
{ value: totalSystemErr, name: '시스템오류' }
|
||||||
{ value: totalBizErr, name: '업무오류' }
|
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
graphic: [{
|
graphic: [{
|
||||||
@@ -279,22 +197,7 @@
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seq900 도넛 차트 업데이트
|
|
||||||
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
|
|
||||||
seq900DonutChart.setOption({
|
|
||||||
series: [{
|
|
||||||
data: [
|
|
||||||
{ value: seq900Timeout, name: 'Timeout' },
|
|
||||||
{ value: seq900SystemErr, name: '시스템오류' },
|
|
||||||
{ value: seq900BizErr, name: '업무오류' }
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
style: {
|
|
||||||
text: seq900Total.toLocaleString()
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 호출량 차트 업데이트
|
// 호출량 차트 업데이트
|
||||||
callChart.setOption({
|
callChart.setOption({
|
||||||
@@ -307,19 +210,10 @@
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// 응답시간 차트 업데이트
|
|
||||||
respChart.setOption({
|
|
||||||
xAxis: { data: times.map(function(t) { return t.substring(4, 6) + '월'; }) },
|
|
||||||
series: [
|
|
||||||
{ data: p95RespData },
|
|
||||||
{ data: p50RespData },
|
|
||||||
{ data: avgRespData }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 드릴다운을 위해 원본 데이터 저장
|
// 드릴다운을 위해 원본 데이터 저장
|
||||||
callChart.rawData = data;
|
callChart.rawData = data;
|
||||||
respChart.rawData = data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchChartData() {
|
function fetchChartData() {
|
||||||
@@ -539,8 +433,7 @@
|
|||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'API명',
|
'API명',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
@@ -549,10 +442,6 @@
|
|||||||
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
|
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
|
||||||
@@ -584,9 +473,8 @@
|
|||||||
colNames: [
|
colNames: [
|
||||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
||||||
'Inbound Adapter', 'Outbound Adapter',
|
'Inbound Adapter', 'Outbound Adapter',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
|
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
{ name: 'statTime', align: 'center', width: '120', sortable: false },
|
{ name: 'statTime', align: 'center', width: '120', sortable: false },
|
||||||
@@ -600,15 +488,9 @@
|
|||||||
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
|
||||||
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
|
|
||||||
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
|
|
||||||
],
|
],
|
||||||
jsonReader: { repeatitems: false },
|
jsonReader: { repeatitems: false },
|
||||||
pager: $('#pager'),
|
pager: $('#pager'),
|
||||||
@@ -675,9 +557,7 @@
|
|||||||
// 윈도우 리사이즈 시 차트 리사이즈
|
// 윈도우 리사이즈 시 차트 리사이즈
|
||||||
$(window).resize(function() {
|
$(window).resize(function() {
|
||||||
if (totalDonutChart) totalDonutChart.resize();
|
if (totalDonutChart) totalDonutChart.resize();
|
||||||
if (seq900DonutChart) seq900DonutChart.resize();
|
|
||||||
if (callChart) callChart.resize();
|
if (callChart) callChart.resize();
|
||||||
if (respChart) respChart.resize();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonControl();
|
buttonControl();
|
||||||
@@ -744,14 +624,10 @@
|
|||||||
<!-- 도넛 차트 (상단 50%씩) -->
|
<!-- 도넛 차트 (상단 50%씩) -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div id="totalDonutChart" class="chart"></div>
|
<div id="totalDonutChart" class="chart"></div>
|
||||||
<div id="seq900DonutChart" class="chart"></div>
|
|
||||||
</div>
|
|
||||||
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
|
|
||||||
<div class="chart-container">
|
|
||||||
<div id="callChart" class="chart"></div>
|
<div id="callChart" class="chart"></div>
|
||||||
<div id="respChart" class="chart"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- 요약 그리드 -->
|
<!-- 요약 그리드 -->
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
var url = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.json"/>';
|
var url = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.json"/>';
|
||||||
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.view"/>';
|
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.view"/>';
|
||||||
|
|
||||||
var totalDonutChart, seq900DonutChart, callChart, respChart;
|
var totalDonutChart, callChart;
|
||||||
|
|
||||||
function numberFormatter(cellvalue, options, rowObject) {
|
function numberFormatter(cellvalue, options, rowObject) {
|
||||||
if (cellvalue == null || cellvalue == '') return '0';
|
if (cellvalue == null || cellvalue == '') return '0';
|
||||||
@@ -54,9 +54,7 @@
|
|||||||
|
|
||||||
function initCharts() {
|
function initCharts() {
|
||||||
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
|
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
|
||||||
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
|
|
||||||
callChart = echarts.init(document.getElementById('callChart'));
|
callChart = echarts.init(document.getElementById('callChart'));
|
||||||
respChart = echarts.init(document.getElementById('respChart'));
|
|
||||||
|
|
||||||
// 총건수 도넛 차트
|
// 총건수 도넛 차트
|
||||||
var totalDonutOption = {
|
var totalDonutOption = {
|
||||||
@@ -73,7 +71,7 @@
|
|||||||
legend: {
|
legend: {
|
||||||
orient: 'horizontal',
|
orient: 'horizontal',
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
data: ['성공', 'Timeout', '시스템오류', '업무오류']
|
data: ['성공', 'Timeout', '시스템오류']
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: '총건수',
|
name: '총건수',
|
||||||
@@ -98,8 +96,7 @@
|
|||||||
data: [
|
data: [
|
||||||
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
|
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
|
||||||
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
|
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
graphic: [{
|
graphic: [{
|
||||||
@@ -118,64 +115,7 @@
|
|||||||
|
|
||||||
totalDonutChart.setOption(totalDonutOption);
|
totalDonutChart.setOption(totalDonutOption);
|
||||||
|
|
||||||
// Seq900 도넛 차트
|
|
||||||
var seq900DonutOption = {
|
|
||||||
title: {
|
|
||||||
text: 'Seq900 에러 분포',
|
|
||||||
left: 'center',
|
|
||||||
top: 10,
|
|
||||||
textStyle: { fontSize: 14 }
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: '{b}: {c} ({d}%)'
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
orient: 'horizontal',
|
|
||||||
bottom: 10,
|
|
||||||
data: ['Timeout', '시스템오류', '업무오류']
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
name: 'Seq900 에러',
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['40%', '70%'],
|
|
||||||
center: ['50%', '50%'],
|
|
||||||
avoidLabelOverlap: true,
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
formatter: '{b}: {c}'
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: true
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
|
|
||||||
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
|
|
||||||
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
type: 'text',
|
|
||||||
left: 'center',
|
|
||||||
top: 'center',
|
|
||||||
style: {
|
|
||||||
text: '0',
|
|
||||||
textAlign: 'center',
|
|
||||||
fill: '#333',
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
seq900DonutChart.setOption(seq900DonutOption);
|
|
||||||
|
|
||||||
// 호출량 차트
|
// 호출량 차트
|
||||||
var callOption = {
|
var callOption = {
|
||||||
@@ -184,39 +124,20 @@
|
|||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
||||||
},
|
},
|
||||||
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 },
|
legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
|
||||||
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
||||||
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
||||||
yAxis: { type: 'value', minInterval: 1 },
|
yAxis: { type: 'value', minInterval: 1 },
|
||||||
series: [
|
series: [
|
||||||
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
|
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
|
||||||
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
|
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
|
||||||
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] },
|
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
|
||||||
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
callChart.setOption(callOption);
|
callChart.setOption(callOption);
|
||||||
|
|
||||||
// 응답시간 차트
|
|
||||||
var respOption = {
|
|
||||||
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
|
||||||
},
|
|
||||||
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
|
|
||||||
xAxis: { type: 'category', boundaryGap: false, data: [] },
|
|
||||||
yAxis: { type: 'value' },
|
|
||||||
series: [
|
|
||||||
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
|
|
||||||
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
|
|
||||||
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
respChart.setOption(respOption);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCharts(data) {
|
function updateCharts(data) {
|
||||||
@@ -268,8 +189,7 @@
|
|||||||
data: [
|
data: [
|
||||||
{ value: totalSuccess, name: '성공' },
|
{ value: totalSuccess, name: '성공' },
|
||||||
{ value: totalTimeout, name: 'Timeout' },
|
{ value: totalTimeout, name: 'Timeout' },
|
||||||
{ value: totalSystemErr, name: '시스템오류' },
|
{ value: totalSystemErr, name: '시스템오류' }
|
||||||
{ value: totalBizErr, name: '업무오류' }
|
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
graphic: [{
|
graphic: [{
|
||||||
@@ -279,22 +199,7 @@
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seq900 도넛 차트 업데이트
|
|
||||||
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
|
|
||||||
seq900DonutChart.setOption({
|
|
||||||
series: [{
|
|
||||||
data: [
|
|
||||||
{ value: seq900Timeout, name: 'Timeout' },
|
|
||||||
{ value: seq900SystemErr, name: '시스템오류' },
|
|
||||||
{ value: seq900BizErr, name: '업무오류' }
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
graphic: [{
|
|
||||||
style: {
|
|
||||||
text: seq900Total.toLocaleString()
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 호출량 차트 업데이트
|
// 호출량 차트 업데이트
|
||||||
callChart.setOption({
|
callChart.setOption({
|
||||||
@@ -307,19 +212,10 @@
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// 응답시간 차트 업데이트
|
|
||||||
respChart.setOption({
|
|
||||||
xAxis: { data: times.map(function(t) { return t.substring(0, 4) + '년'; }) },
|
|
||||||
series: [
|
|
||||||
{ data: p95RespData },
|
|
||||||
{ data: p50RespData },
|
|
||||||
{ data: avgRespData }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 드릴다운을 위해 원본 데이터 저장
|
// 드릴다운을 위해 원본 데이터 저장
|
||||||
callChart.rawData = data;
|
callChart.rawData = data;
|
||||||
respChart.rawData = data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchChartData() {
|
function fetchChartData() {
|
||||||
@@ -513,8 +409,7 @@
|
|||||||
postData: gridPostData,
|
postData: gridPostData,
|
||||||
colNames: [
|
colNames: [
|
||||||
'API명',
|
'API명',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
@@ -523,10 +418,6 @@
|
|||||||
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
|
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
|
||||||
@@ -558,9 +449,8 @@
|
|||||||
colNames: [
|
colNames: [
|
||||||
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
|
||||||
'Inbound Adapter', 'Outbound Adapter',
|
'Inbound Adapter', 'Outbound Adapter',
|
||||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
'총건수', '성공', 'Timeout', '시스템오류',
|
||||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
|
|
||||||
],
|
],
|
||||||
colModel: [
|
colModel: [
|
||||||
{ name: 'statTime', align: 'center', width: '120', sortable: false },
|
{ name: 'statTime', align: 'center', width: '120', sortable: false },
|
||||||
@@ -574,15 +464,9 @@
|
|||||||
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
||||||
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
|
|
||||||
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
|
||||||
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
|
|
||||||
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
|
|
||||||
],
|
],
|
||||||
jsonReader: { repeatitems: false },
|
jsonReader: { repeatitems: false },
|
||||||
pager: $('#pager'),
|
pager: $('#pager'),
|
||||||
@@ -644,9 +528,7 @@
|
|||||||
// 윈도우 리사이즈 시 차트 리사이즈
|
// 윈도우 리사이즈 시 차트 리사이즈
|
||||||
$(window).resize(function() {
|
$(window).resize(function() {
|
||||||
if (totalDonutChart) totalDonutChart.resize();
|
if (totalDonutChart) totalDonutChart.resize();
|
||||||
if (seq900DonutChart) seq900DonutChart.resize();
|
|
||||||
if (callChart) callChart.resize();
|
if (callChart) callChart.resize();
|
||||||
if (respChart) respChart.resize();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonControl();
|
buttonControl();
|
||||||
@@ -712,12 +594,7 @@
|
|||||||
<!-- 도넛 차트 (상단 50%씩) -->
|
<!-- 도넛 차트 (상단 50%씩) -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div id="totalDonutChart" class="chart"></div>
|
<div id="totalDonutChart" class="chart"></div>
|
||||||
<div id="seq900DonutChart" class="chart"></div>
|
|
||||||
</div>
|
|
||||||
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
|
|
||||||
<div class="chart-container">
|
|
||||||
<div id="callChart" class="chart"></div>
|
<div id="callChart" class="chart"></div>
|
||||||
<div id="respChart" class="chart"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 요약 그리드 -->
|
<!-- 요약 그리드 -->
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||||
|
<%@ page import="java.io.*"%>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||||
|
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||||
|
<%
|
||||||
|
response.setHeader("Pragma", "No-cache");
|
||||||
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
|
response.setHeader("Expires", "0");
|
||||||
|
request.setCharacterEncoding("UTF-8");
|
||||||
|
%>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>대시보드</title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||||
|
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||||
|
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
|
||||||
|
<script language="javascript">
|
||||||
|
var url = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.json"/>';
|
||||||
|
var url_view = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.view"/>';
|
||||||
|
|
||||||
|
|
||||||
|
function numberFormatter(cellvalue, options, rowObject) {
|
||||||
|
if (cellvalue == null || cellvalue == '') return '0';
|
||||||
|
return Number(cellvalue).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function decimalFormatter(cellvalue, options, rowObject) {
|
||||||
|
if (cellvalue == null || cellvalue == '') return '0';
|
||||||
|
return (cellvalue).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPostData() {
|
||||||
|
var postData = {}
|
||||||
|
if (arguments.length == 2) {
|
||||||
|
postData[arguments[0]] = arguments[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchStartDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
|
||||||
|
var searchEndDate = $("input[name=searchEndDate]").val().replace(/-/g, "");
|
||||||
|
|
||||||
|
if (searchStartDate && searchEndDate) {
|
||||||
|
var start = new Date(searchStartDate.substring(0, 4), parseInt(searchStartDate.substring(4, 6)) - 1, searchStartDate.substring(6, 8));
|
||||||
|
var end = new Date(searchEndDate.substring(0, 4), parseInt(searchEndDate.substring(4, 6)) - 1, searchEndDate.substring(6, 8));
|
||||||
|
var diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays > 31) {
|
||||||
|
alert('조회 기간은 최대 31일까지 가능합니다.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postData.searchType = $('input[name="searchType"]:checked').val();
|
||||||
|
postData.searchOrgName = $('#searchOrgName').val();
|
||||||
|
postData.searchApiName = $('#searchApiName').val();
|
||||||
|
postData.searchStartDateTime = searchStartDate;
|
||||||
|
postData.searchEndDateTime = searchEndDate;
|
||||||
|
|
||||||
|
return postData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function search() {
|
||||||
|
var postData = getPostData("cmd", "LIST");
|
||||||
|
if (postData) {
|
||||||
|
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportToExcel() {
|
||||||
|
|
||||||
|
var postData = getPostData("cmd", "EXCEL_EXPORT");
|
||||||
|
if (!postData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
postData.serviceType = '${param.serviceType}';
|
||||||
|
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', url, true);
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
console.log('[Excel Export] Response received - Status:', xhr.status);
|
||||||
|
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
var blob = xhr.response;
|
||||||
|
var contentType = xhr.getResponseHeader('Content-Type');
|
||||||
|
console.log('[Excel Export] Blob size:', blob.size, 'Content-Type:', contentType);
|
||||||
|
|
||||||
|
// JSON 에러 응답인지 확인
|
||||||
|
if (contentType && contentType.indexOf('application/json') !== -1) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function() {
|
||||||
|
try {
|
||||||
|
var errorObj = JSON.parse(reader.result);
|
||||||
|
alert(errorObj.message || 'Excel 파일 생성에 실패했습니다.');
|
||||||
|
} catch (e) {
|
||||||
|
alert('Excel 파일 생성에 실패했습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(blob);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blob 크기가 0이면 에러
|
||||||
|
if (blob.size === 0) {
|
||||||
|
alert('Excel 파일 생성에 실패했습니다. (빈 응답)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일명 추출
|
||||||
|
var filename = 'api_stats.xlsx';
|
||||||
|
var disposition = xhr.getResponseHeader('Content-Disposition');
|
||||||
|
console.log('[Excel Export] Content-Disposition:', disposition);
|
||||||
|
|
||||||
|
if (disposition && disposition.indexOf('filename=') !== -1) {
|
||||||
|
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
|
||||||
|
var matches = filenameRegex.exec(disposition);
|
||||||
|
if (matches != null && matches[1]) {
|
||||||
|
filename = matches[1].replace(/['"]/g, '');
|
||||||
|
filename = decodeURIComponent(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Excel Export] Downloading as:', filename);
|
||||||
|
|
||||||
|
// 파일 다운로드
|
||||||
|
var link = document.createElement('a');
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(link.href);
|
||||||
|
|
||||||
|
console.log('[Excel Export] Download triggered');
|
||||||
|
|
||||||
|
} else if (xhr.status === 204) {
|
||||||
|
alert('조회된 데이터가 없습니다.');
|
||||||
|
} else {
|
||||||
|
// 에러 응답 처리
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function() {
|
||||||
|
var message = 'Excel 다운로드 중 오류가 발생했습니다.';
|
||||||
|
console.log('reader.result', reader.result)
|
||||||
|
try {
|
||||||
|
var errorObj = JSON.parse(reader.result);
|
||||||
|
if (errorObj.message) message = errorObj.message;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Excel Export] Error parsing error response:', e);
|
||||||
|
}
|
||||||
|
alert(message);
|
||||||
|
};
|
||||||
|
reader.readAsText(xhr.response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = function() {
|
||||||
|
console.error('[Excel Export] Network error');
|
||||||
|
alert('네트워크 오류가 발생했습니다.');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Form data 생성
|
||||||
|
var formData = [];
|
||||||
|
for (var key in postData) {
|
||||||
|
if (postData.hasOwnProperty(key) && postData[key] != null && postData[key] !== '') {
|
||||||
|
formData.push(encodeURIComponent(key) + '=' + encodeURIComponent(postData[key]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send(formData.join('&'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function list() {
|
||||||
|
var gridPostData = getPostData("cmd", "LIST");
|
||||||
|
|
||||||
|
$('#grid').jqGrid({
|
||||||
|
datatype: "json",
|
||||||
|
mtype: 'POST',
|
||||||
|
postData: gridPostData,
|
||||||
|
colNames: [
|
||||||
|
'구분',
|
||||||
|
'총건수', '성공', '성공율(%)', '실패율(%)', 'Timeout', '시스템오류',
|
||||||
|
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||||
|
],
|
||||||
|
colModel: [
|
||||||
|
{ name: 'orgName', align: 'left', width: '250', sortable: false },
|
||||||
|
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
|
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
|
{ name: 'successRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
|
{ name: 'failRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
|
||||||
|
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
|
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
|
||||||
|
{ name: 'avgRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false },
|
||||||
|
{ name: 'minRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false },
|
||||||
|
{ name: 'maxRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false }
|
||||||
|
],
|
||||||
|
jsonReader: { repeatitems: false },
|
||||||
|
pager: $('#pager'),
|
||||||
|
page: '${param.page}',
|
||||||
|
rowNum: '${rmsDefaultRowNum}',
|
||||||
|
autoheight: true,
|
||||||
|
height: 'auto',
|
||||||
|
autowidth: true,
|
||||||
|
viewrecords: true,
|
||||||
|
rowList: eval('[${rmsDefaultRowList}]'),
|
||||||
|
loadComplete: function(d) {
|
||||||
|
var colModel = $(this).getGridParam("colModel");
|
||||||
|
for (var i = 0; i < colModel.length; i++) {
|
||||||
|
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
// 날짜/시간 입력 마스크
|
||||||
|
$("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true });
|
||||||
|
$("input[name=searchStartDate], input[name=searchEndDate]").datepicker();
|
||||||
|
|
||||||
|
var today = getToday();
|
||||||
|
var startDate = today;
|
||||||
|
var endDate = today;
|
||||||
|
|
||||||
|
|
||||||
|
if (!$("input[name=searchStartDate]").val()) {
|
||||||
|
$("input[name=searchStartDate]").val(startDate);
|
||||||
|
}
|
||||||
|
if (!$("input[name=searchEndDate]").val()) {
|
||||||
|
$("input[name=searchEndDate]").val(endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
list();
|
||||||
|
|
||||||
|
resizeJqGridWidth('grid', 'content_middle', '1200');
|
||||||
|
|
||||||
|
$("#btn_search").click(function() {
|
||||||
|
search();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#btn_excel").click(function() {
|
||||||
|
exportToExcel();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("input[name^=search]").keydown(function(key) {
|
||||||
|
if (key.keyCode == 13) {
|
||||||
|
$("#btn_search").click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
buttonControl();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="right_box">
|
||||||
|
<div class="content_top">
|
||||||
|
<ul class="path">
|
||||||
|
<li><a href="#">${rmsMenuPath}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="content_middle" id="content_middle">
|
||||||
|
<div class="search_wrap">
|
||||||
|
<button type="button" class="cssbtn" id="btn_excel" level="R" style="display: inline-block;"><i class="material-icons">table_view</i> 엑셀</button>
|
||||||
|
<button type="button" class="cssbtn" id="btn_search" level="R" style="display: inline-block;">
|
||||||
|
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="title" id="title">API 사용현황</div>
|
||||||
|
<table class="search_condition" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th style="width:120px;">조회기간</th>
|
||||||
|
<td colspan="5">
|
||||||
|
<input type="text" name="searchStartDate" id="searchStartDate" value="${param.searchStartDate}" style="width:100px;">
|
||||||
|
~
|
||||||
|
<input type="text" name="searchEndDate" id="searchEndDate" value="${param.searchEndDate}" style="width:100px;">
|
||||||
|
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width:120px;">조회구분</th>
|
||||||
|
<td>
|
||||||
|
<input type="radio" name="searchType" id="searchTypeORG" value="ORG" checked="checked"><label for="searchTypeORG">제휴사</label></input>
|
||||||
|
<input type="radio" name="searchType" id="searchTypeAPI" value="API"><label for="searchTypeAPI">API</label></input>
|
||||||
|
<input type="radio" name="searchType" id="searchTypeDATE" value="DATE"><label for="searchTypeDATE">사용일</label></input>
|
||||||
|
</td>
|
||||||
|
<th style="width:120px;">제휴사명</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="searchOrgName" id="searchOrgName" value="${param.searchOrgName}">
|
||||||
|
</td>
|
||||||
|
<th style="width:120px;">API명</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="searchApiName" id="searchApiName" value="${param.searchApiName}">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<table id="grid"></table>
|
||||||
|
<div id="pager"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -411,7 +411,7 @@
|
|||||||
function detail(url,key){
|
function detail(url,key){
|
||||||
jsonUrl = url;
|
jsonUrl = url;
|
||||||
if (!isDetail)return;
|
if (!isDetail)return;
|
||||||
$("input[name='btnRadioSyncAsync']").attr('disabled', true);
|
// $("input[name='btnRadioSyncAsync']").attr('disabled', true);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type : "POST",
|
type : "POST",
|
||||||
url:url,
|
url:url,
|
||||||
@@ -962,11 +962,13 @@
|
|||||||
headerNames = messageKeyList;
|
headerNames = messageKeyList;
|
||||||
isHeaderRouting = true;
|
isHeaderRouting = true;
|
||||||
initHeaders(headerNames);
|
initHeaders(headerNames);
|
||||||
$('#slideInboundRoutingRule').carousel(0);
|
//$('#slideInboundRoutingRule').carousel(0);
|
||||||
|
$('#collapseInboundRoutingInfo').collapse('show')
|
||||||
setHeaderRoutingLabel();
|
setHeaderRoutingLabel();
|
||||||
}else{
|
}else{
|
||||||
isHeaderRouting = false;
|
isHeaderRouting = false;
|
||||||
$('#slideInboundRoutingRule').carousel(1);
|
$('#collapseInboundRoutingInfo').collapse('hide')
|
||||||
|
//$('#slideInboundRoutingRule').carousel(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1572,7 +1574,7 @@
|
|||||||
<fieldset class="groupbox-border">
|
<fieldset class="groupbox-border">
|
||||||
<legend class="groupbox-border">INTERFACE TYPE</legend>
|
<legend class="groupbox-border">INTERFACE TYPE</legend>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-md-3">
|
<!-- <div class="form-group col-md-3">
|
||||||
<label for="eaiBzwkDstcd"><span class="material-icons-outlined">sync</span> Sync/Async 타입</label><br>
|
<label for="eaiBzwkDstcd"><span class="material-icons-outlined">sync</span> Sync/Async 타입</label><br>
|
||||||
<div class="btn-group" id="btnGroupSyncAsync" role="group">
|
<div class="btn-group" id="btnGroupSyncAsync" role="group">
|
||||||
<input type="radio"
|
<input type="radio"
|
||||||
@@ -1615,19 +1617,19 @@
|
|||||||
<i class="bi bi-share-fill"></i> S > A
|
<i class="bi bi-share-fill"></i> S > A
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
<div class="form-group col-md-3">
|
<div class="form-group col-md-3">
|
||||||
<label for="eaiBzwkDstcd">요청/응답 구분</label><br>
|
<label for="eaiBzwkDstcd">요청/응답 구분</label><br>
|
||||||
<div class="btn-group" role="group" aria-label="Basic radio toggle button group"
|
<div class="btn-group" role="group" aria-label="Basic radio toggle button group"
|
||||||
x-effect="if (apiInterface.syncAsyncType !== 'async') apiInterface.requestType = 'S'">
|
x-effect="if (apiInterface.syncAsyncType !== 'async') apiInterface.requestType = 'S'">
|
||||||
<input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeRequest"
|
<input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeRequest"
|
||||||
x-model="apiInterface.requestType"
|
x-model="apiInterface.requestType"
|
||||||
value="S" :disabled="apiInterface.syncAsyncType !== 'async' || $store.formState.isReqResDisabled">
|
value="S" :disabled="$store.formState.isReqResDisabled">
|
||||||
<label class="btn btn-outline-primary" for="btnTypeRequest"><i class="bi bi-arrow-bar-right"></i> 요청</label>
|
<label class="btn btn-outline-primary" for="btnTypeRequest"><i class="bi bi-arrow-bar-right"></i> 요청</label>
|
||||||
|
|
||||||
<input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeResponse"
|
<input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeResponse"
|
||||||
x-model="apiInterface.requestType"
|
x-model="apiInterface.requestType"
|
||||||
value="R" :disabled="apiInterface.syncAsyncType !== 'async' || $store.formState.isReqResDisabled">
|
value="R" :disabled="$store.formState.isReqResDisabled">
|
||||||
<label class="btn btn-outline-primary" for="btnTypeResponse"><i class="bi bi-arrow-bar-left"></i> 응답</label>
|
<label class="btn btn-outline-primary" for="btnTypeResponse"><i class="bi bi-arrow-bar-left"></i> 응답</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1771,36 +1773,34 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group col-md-8">
|
<div class="form-group col-md-8">
|
||||||
<div id="slideInboundRoutingRule" class="carousel slide" data-ride="carousel" data-interval="false">
|
<div id="slideInboundRoutingRule">
|
||||||
<div class="carousel-inner" style="margin-bottom:-20px">
|
<div class="row">
|
||||||
<div class="carousel-item">
|
<div class="col-md-12">
|
||||||
<label>
|
<label for="inboundRestPath">
|
||||||
<span class="material-icons justify-content-md-start">http</span>
|
<span class="material-icons">link</span>
|
||||||
수신 라우팅 정보
|
수신 REST PATH(URL)
|
||||||
</label>
|
</label>
|
||||||
<div class="d-grid gap-2">
|
<input type="text" class="form-control" id="inboundRestPath"
|
||||||
<button type="button" id="headerRoutingButton" class="btn btn-primary"
|
name="inboundRestPath" disabled>
|
||||||
data-bs-toggle="modal" data-bs-target="#routingModal">
|
|
||||||
수정
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="carousel-item active">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label for="inboundRestPath">
|
|
||||||
<span class="material-icons">link</span>
|
|
||||||
수신 REST PATH(URL)
|
|
||||||
</label>
|
|
||||||
<input type="text" class="form-control" id="inboundRestPath"
|
|
||||||
name="inboundRestPath" disabled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="collapse mt-2" id="collapseInboundRoutingInfo">
|
||||||
|
<div class="form-group col-md-12">
|
||||||
|
<label>
|
||||||
|
<span class="material-icons justify-content-md-start">http</span>
|
||||||
|
수신 라우팅 정보
|
||||||
|
</label>
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="button" id="headerRoutingButton" class="btn btn-primary"
|
||||||
|
data-bs-toggle="modal" data-bs-target="#routingModal">
|
||||||
|
수정
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="inboundResponseMethodPathRow"
|
<div id="inboundResponseMethodPathRow"
|
||||||
x-show="showInboundResponse">
|
x-show="showInboundResponse">
|
||||||
<div id="inboundResponseRestPart" class="row">
|
<div id="inboundResponseRestPart" class="row">
|
||||||
|
|||||||
+12
-12
@@ -15,7 +15,7 @@ def quartzVersion = "2.2.1"
|
|||||||
def queryDslVersion = "5.0.0"
|
def queryDslVersion = "5.0.0"
|
||||||
def hibernateVersion = "5.6.15.Final"
|
def hibernateVersion = "5.6.15.Final"
|
||||||
|
|
||||||
def reposiliteUrl = "http://localhost:8080"
|
def nexusUrl = "https://nexus.eactive.synology.me:8090"
|
||||||
//def useOnJboss = false
|
//def useOnJboss = false
|
||||||
|
|
||||||
def generatedJavaDir = "$buildDir/generated/java"
|
def generatedJavaDir = "$buildDir/generated/java"
|
||||||
@@ -23,12 +23,8 @@ def generatedJavaDir = "$buildDir/generated/java"
|
|||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
url "${reposiliteUrl}/private"
|
url "${nexusUrl}/repository/maven-public/"
|
||||||
allowInsecureProtocol = true
|
allowInsecureProtocol = true
|
||||||
credentials {
|
|
||||||
username = reposiliteUser
|
|
||||||
password = reposilitePassword
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +125,7 @@ dependencies {
|
|||||||
implementation "org.snmp4j:snmp4j:1.10.1"
|
implementation "org.snmp4j:snmp4j:1.10.1"
|
||||||
implementation "com.googlecode.json-simple:json-simple:1.1.1"
|
implementation "com.googlecode.json-simple:json-simple:1.1.1"
|
||||||
|
|
||||||
implementation "io.netty:netty-all:4.1.0.Final"
|
implementation "io.netty:netty-all:4.1.94.Final"
|
||||||
|
|
||||||
compileOnly "org.apache.mina:mina-filter-ssl:1.1.6"
|
compileOnly "org.apache.mina:mina-filter-ssl:1.1.6"
|
||||||
compileOnly "org.apache.mina:mina-core:1.1.6"
|
compileOnly "org.apache.mina:mina-core:1.1.6"
|
||||||
@@ -206,13 +202,12 @@ dependencies {
|
|||||||
implementation 'software.amazon.awssdk:sso:2.20.142'
|
implementation 'software.amazon.awssdk:sso:2.20.142'
|
||||||
implementation 'software.amazon.awssdk:sts:2.20.142'
|
implementation 'software.amazon.awssdk:sts:2.20.142'
|
||||||
|
|
||||||
implementation ('io.kubernetes:client-java:18.0.1') {
|
|
||||||
exclude group: 'org.slf4j', module: 'slf4j-api'
|
|
||||||
exclude group: 'org.slf4j', module: 'logback-classic'
|
|
||||||
}
|
|
||||||
|
|
||||||
implementation group: 'commons-net', name: 'commons-net', version: '3.5'
|
implementation group: 'commons-net', name: 'commons-net', version: '3.5'
|
||||||
|
|
||||||
|
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없어 xercesImpl 가 NCDFE 를 일으킴.
|
||||||
|
// xml-apis 1.4.01 에 그 클래스가 포함됨. 직접 의존성으로 묶어 WAR 에 패키징되도록 함.
|
||||||
|
implementation 'xml-apis:xml-apis:1.4.01'
|
||||||
|
|
||||||
testRuntimeOnly 'com.h2database:h2:2.1.214'
|
testRuntimeOnly 'com.h2database:h2:2.1.214'
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
|
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
|
||||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
||||||
@@ -232,6 +227,11 @@ configurations.all {
|
|||||||
cacheDynamicVersionsFor 10, 'minutes'
|
cacheDynamicVersionsFor 10, 'minutes'
|
||||||
// Do not cache changing modules
|
// Do not cache changing modules
|
||||||
cacheChangingModulesFor 0, 'seconds'
|
cacheChangingModulesFor 0, 'seconds'
|
||||||
|
|
||||||
|
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없음.
|
||||||
|
// 일부 transitive 가 끌어오는 xml-apis:1.0.b2 (2002, DOM L2) 는 이 클래스를 포함하지 않아
|
||||||
|
// xercesImpl 가 NoClassDefFoundError 를 일으킴 → 1.4.01 로 강제 통일.
|
||||||
|
force 'xml-apis:xml-apis:1.4.01'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,3 @@ org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryEr
|
|||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.configureondemand=true
|
org.gradle.configureondemand=true
|
||||||
|
|
||||||
reposiliteUser=admin
|
|
||||||
reposilitePassword=BFoZsHrQU4lmJf9rruDdcsPgE0gltKfRdTjAx04IgvFhrd5q07QwMFZqeLHFv+5n
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
## 기술 스택
|
## 기술 스택
|
||||||
|
|
||||||
- Spring Framework 5.3.27
|
- Spring Framework 5.3.27
|
||||||
- Java 8
|
- Java 17
|
||||||
- Gradle 8.7
|
- Gradle 8.7
|
||||||
- Oracle 19c
|
- Oracle 19c
|
||||||
- Spring Data JPA 2.5.2
|
- Spring Data JPA 2.5.2
|
||||||
@@ -23,251 +23,61 @@
|
|||||||
|
|
||||||
### 사전 요구사항
|
### 사전 요구사항
|
||||||
|
|
||||||
- JDK 8 이상
|
- JDK 17 이상
|
||||||
- Gradle 8.7
|
- Gradle 8.7
|
||||||
- Git
|
- Git
|
||||||
- Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB)
|
- Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB)
|
||||||
|
|
||||||
### Git Bash에서 초기 세팅
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# ====================================
|
|
||||||
# 1. 변수 설정 (프로젝트 경로 및 브랜치)
|
|
||||||
# ====================================
|
|
||||||
export WORKSPACE_DIR="/c/eactive/workspaces/kjb-eapim"
|
|
||||||
export PROJECT_BRANCH="jenkins_with_weblogic"
|
|
||||||
|
|
||||||
# Git Repository URL 설정
|
|
||||||
# 기본: ssh://git@192.168.240.178:18081/eapim
|
|
||||||
# 대체: https://git.eactive.synology.me:8090/kjb-eapim
|
|
||||||
export GIT_REPO_BASE="ssh://git@192.168.240.178:18081/eapim"
|
|
||||||
# export GIT_REPO_BASE="https://git.eactive.synology.me:8090/kjb-eapim"
|
|
||||||
|
|
||||||
# Gradle 명령어 설정
|
|
||||||
# 일반 환경: gradle
|
|
||||||
# msi-gf63 환경: /c/eactive/workspaces/shell-scripts/kjb-gradle.sh
|
|
||||||
export GRADLE_CMD="gradle"
|
|
||||||
# export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
|
|
||||||
|
|
||||||
# ====================================
|
|
||||||
# 2. 작업 디렉토리 생성
|
|
||||||
# ====================================
|
|
||||||
mkdir -p $WORKSPACE_DIR
|
|
||||||
cd $WORKSPACE_DIR
|
|
||||||
|
|
||||||
# ====================================
|
|
||||||
# 3. eapim-admin 프로젝트 클론
|
|
||||||
# ====================================
|
|
||||||
git clone ${GIT_REPO_BASE}/eapim-admin.git
|
|
||||||
cd eapim-admin
|
|
||||||
|
|
||||||
# 프로젝트 브랜치로 체크아웃
|
|
||||||
git checkout $PROJECT_BRANCH
|
|
||||||
|
|
||||||
# ====================================
|
|
||||||
# 4. eapim-online 프로젝트 클론 (코어 모듈 포함)
|
|
||||||
# ====================================
|
|
||||||
cd $WORKSPACE_DIR
|
|
||||||
git clone ${GIT_REPO_BASE}/eapim-online.git
|
|
||||||
cd eapim-online
|
|
||||||
git checkout $PROJECT_BRANCH
|
|
||||||
|
|
||||||
# ====================================
|
|
||||||
# 5. 의존 모듈 클론
|
|
||||||
# ====================================
|
|
||||||
cd $WORKSPACE_DIR
|
|
||||||
|
|
||||||
# elink-portal-common 클론
|
|
||||||
git clone ${GIT_REPO_BASE}/elink-portal-common.git
|
|
||||||
cd elink-portal-common
|
|
||||||
git checkout master # 또는 필요한 브랜치
|
|
||||||
|
|
||||||
# kjb-safedb 클론
|
|
||||||
cd $WORKSPACE_DIR
|
|
||||||
git clone ${GIT_REPO_BASE}/kjb-safedb.git
|
|
||||||
cd kjb-safedb
|
|
||||||
git checkout master # 또는 필요한 브랜치
|
|
||||||
|
|
||||||
# ====================================
|
|
||||||
# 6. 최종 폴더 구조 확인
|
|
||||||
# ====================================
|
|
||||||
# kjb-eapim/
|
|
||||||
# ├── eapim-admin/ # Admin 관리 콘솔 (현재 프로젝트)
|
|
||||||
# ├── eapim-online/ # Online 게이트웨이 코어 모듈
|
|
||||||
# │ ├── elink-online-core/
|
|
||||||
# │ ├── elink-online-core-jpa/
|
|
||||||
# │ ├── elink-online-transformer/
|
|
||||||
# │ ├── elink-online-common/
|
|
||||||
# │ ├── elink-online-emsclient/
|
|
||||||
# │ └── elink-online-adapter/
|
|
||||||
# ├── elink-portal-common/ # 포털 공통 컴포넌트 (JPA 엔티티)
|
|
||||||
# └── kjb-safedb/ # SafeDB 암호화 라이브러리
|
|
||||||
|
|
||||||
# ====================================
|
|
||||||
# 7. eapim-admin 프로젝트로 이동 및 디펜던시 다운로드
|
|
||||||
# ====================================
|
|
||||||
cd $WORKSPACE_DIR/eapim-admin
|
|
||||||
|
|
||||||
# Gradle 디펜던시 다운로드
|
|
||||||
$GRADLE_CMD dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows CMD에서 초기 세팅
|
|
||||||
|
|
||||||
```cmd
|
|
||||||
REM ====================================
|
|
||||||
REM 1. 변수 설정 (프로젝트 경로 및 브랜치)
|
|
||||||
REM ====================================
|
|
||||||
set WORKSPACE_DIR=C:\eactive\workspaces\kjb-eapim
|
|
||||||
set PROJECT_BRANCH=jenkins_with_weblogic
|
|
||||||
|
|
||||||
REM Git Repository URL 설정
|
|
||||||
REM 기본: ssh://git@192.168.240.178:18081/eapim
|
|
||||||
REM 대체: https://git.eactive.synology.me:8090/kjb-eapim
|
|
||||||
set GIT_REPO_BASE=ssh://git@192.168.240.178:18081/eapim
|
|
||||||
REM set GIT_REPO_BASE=https://git.eactive.synology.me:8090/kjb-eapim
|
|
||||||
|
|
||||||
REM Gradle 명령어 설정
|
|
||||||
REM 일반 환경: gradle
|
|
||||||
REM msi-gf63 환경: C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
|
||||||
set GRADLE_CMD=gradle
|
|
||||||
REM set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
|
||||||
|
|
||||||
REM ====================================
|
|
||||||
REM 2. 작업 디렉토리 생성
|
|
||||||
REM ====================================
|
|
||||||
if not exist "%WORKSPACE_DIR%" mkdir "%WORKSPACE_DIR%"
|
|
||||||
cd /d "%WORKSPACE_DIR%"
|
|
||||||
|
|
||||||
REM ====================================
|
|
||||||
REM 3. eapim-admin 프로젝트 클론
|
|
||||||
REM ====================================
|
|
||||||
git clone %GIT_REPO_BASE%/eapim-admin.git
|
|
||||||
cd eapim-admin
|
|
||||||
|
|
||||||
REM 프로젝트 브랜치로 체크아웃
|
|
||||||
git checkout %PROJECT_BRANCH%
|
|
||||||
|
|
||||||
REM ====================================
|
|
||||||
REM 4. eapim-online 프로젝트 클론 (코어 모듈 포함)
|
|
||||||
REM ====================================
|
|
||||||
cd /d "%WORKSPACE_DIR%"
|
|
||||||
git clone %GIT_REPO_BASE%/eapim-online.git
|
|
||||||
cd eapim-online
|
|
||||||
git checkout %PROJECT_BRANCH%
|
|
||||||
|
|
||||||
REM ====================================
|
|
||||||
REM 5. 의존 모듈 클론
|
|
||||||
REM ====================================
|
|
||||||
cd /d "%WORKSPACE_DIR%"
|
|
||||||
|
|
||||||
REM elink-portal-common 클론
|
|
||||||
git clone %GIT_REPO_BASE%/elink-portal-common.git
|
|
||||||
cd elink-portal-common
|
|
||||||
git checkout master
|
|
||||||
|
|
||||||
REM kjb-safedb 클론
|
|
||||||
cd /d "%WORKSPACE_DIR%"
|
|
||||||
git clone %GIT_REPO_BASE%/kjb-safedb.git
|
|
||||||
cd kjb-safedb
|
|
||||||
git checkout master
|
|
||||||
|
|
||||||
REM ====================================
|
|
||||||
REM 6. 최종 폴더 구조 확인
|
|
||||||
REM ====================================
|
|
||||||
REM kjb-eapim\
|
|
||||||
REM ├── eapim-admin\ # Admin 관리 콘솔 (현재 프로젝트)
|
|
||||||
REM ├── eapim-online\ # Online 게이트웨이 코어 모듈
|
|
||||||
REM │ ├── elink-online-core\
|
|
||||||
REM │ ├── elink-online-core-jpa\
|
|
||||||
REM │ ├── elink-online-transformer\
|
|
||||||
REM │ ├── elink-online-common\
|
|
||||||
REM │ ├── elink-online-emsclient\
|
|
||||||
REM │ └── elink-online-adapter\
|
|
||||||
REM ├── elink-portal-common\ # 포털 공통 컴포넌트 (JPA 엔티티)
|
|
||||||
REM └── kjb-safedb\ # SafeDB 암호화 라이브러리
|
|
||||||
|
|
||||||
REM ====================================
|
|
||||||
REM 7. eapim-admin 프로젝트로 이동 및 디펜던시 다운로드
|
|
||||||
REM ====================================
|
|
||||||
cd /d "%WORKSPACE_DIR%\eapim-admin"
|
|
||||||
|
|
||||||
REM Gradle 디펜던시 다운로드
|
|
||||||
%GRADLE_CMD% dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
### 환경별 설정 파일
|
### 환경별 설정 파일
|
||||||
|
|
||||||
프로젝트 클론 후 필요한 환경 설정을 수정하세요:
|
프로젝트 클론 후 필요한 환경 설정을 수정하세요:
|
||||||
|
|
||||||
- `WebContent/WEB-INF/properties/env.D.properties` - 개발 환경
|
- `WebContent/WEB-INF/properties/env.D.properties` - 개발 환경
|
||||||
- `WebContent/WEB-INF/properties/env.T.properties` - 테스트/스테이징 환경
|
- `WebContent/WEB-INF/properties/env.T.properties` - 테스트/스테이징 환경
|
||||||
- `WebContent/WEB-INF/properties/env.P.properties` - 운영 환경
|
- `WebContent/WEB-INF/properties/env.P.properties` - 운영 환경
|
||||||
|
|
||||||
### 주의사항
|
|
||||||
|
|
||||||
- **gradlew 사용 금지**: 반드시 시스템에 설치된 gradle을 직접 사용하거나 환경에 맞는 스크립트를 사용하세요
|
### 주의사항
|
||||||
- **Gradle 명령어 변수화**: 위의 `GRADLE_CMD` 변수를 설정하면 환경별로 다른 gradle 명령어 사용 가능
|
|
||||||
- **SafeDB**: 암호화 기능이 필요한 경우 `-Dkjb_safedb.mode=fake` 설정 (개발 환경) 또는 실제 SafeDB 라이브러리 설치
|
- **SafeDB**: 암호화 기능이 필요한 경우 `-Dkjb_safedb.mode=fake` 설정 (개발 환경) 또는 실제 SafeDB 라이브러리 설치
|
||||||
- **멀티 모듈 구조**: eapim-online의 여러 모듈들을 참조하므로 반드시 eapim-online도 클론해야 합니다
|
- **멀티 모듈 구조**: eapim-online의 여러 모듈들을 참조하므로 반드시 eapim-online도 클론해야 합니다
|
||||||
|
|
||||||
|
|
||||||
## 빌드 및 실행
|
## 빌드 및 실행
|
||||||
|
|
||||||
### Gradle 명령어 설정
|
### Gradle 명령어
|
||||||
|
|
||||||
사용 환경에 따라 적절한 gradle 명령어를 설정하세요:
|
프로젝트 루트의 Gradle wrapper(`./gradlew`) 를 사용합니다.
|
||||||
|
|
||||||
**Git Bash:**
|
|
||||||
```bash
|
|
||||||
# 일반 환경
|
|
||||||
export GRADLE_CMD="gradle"
|
|
||||||
|
|
||||||
# msi-gf63 환경
|
|
||||||
export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Windows CMD:**
|
|
||||||
```cmd
|
|
||||||
REM 일반 환경
|
|
||||||
set GRADLE_CMD=gradle
|
|
||||||
|
|
||||||
REM msi-gf63 환경
|
|
||||||
set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 빌드 및 실행 명령어
|
### 빌드 및 실행 명령어
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 표준 빌드
|
# 표준 빌드
|
||||||
$GRADLE_CMD build
|
./gradlew build
|
||||||
|
|
||||||
# Weblogic 배포용 빌드 (테스트 제외)
|
# Weblogic 배포용 빌드 (테스트 제외)
|
||||||
$GRADLE_CMD build -x test -Pprofile=weblogic
|
./gradlew build -x test -Pprofile=weblogic
|
||||||
|
|
||||||
# WAR 파일 빌드
|
# WAR 파일 빌드[ems_stdout.log](../../logs/emsSvr-RinjaeMA/ems_stdout.log)
|
||||||
$GRADLE_CMD war
|
./gradlew war
|
||||||
|
|
||||||
# 클린 빌드
|
# 클린 빌드
|
||||||
$GRADLE_CMD clean build
|
./gradlew clean build
|
||||||
|
|
||||||
# 테스트 실행
|
# 테스트 실행
|
||||||
$GRADLE_CMD test
|
./gradlew test
|
||||||
|
|
||||||
# 패키징 없이 클래스만 컴파일
|
# 패키징 없이 클래스만 컴파일
|
||||||
$GRADLE_CMD classes
|
./gradlew classes
|
||||||
|
|
||||||
# QueryDSL Q-classes 및 기타 애노테이션 생성
|
# QueryDSL Q-classes 및 기타 애노테이션 생성
|
||||||
$GRADLE_CMD compileJava
|
./gradlew compileJava
|
||||||
|
|
||||||
# 의존성 트리 보기
|
# 의존성 트리 보기
|
||||||
$GRADLE_CMD dependencies
|
./gradlew dependencies
|
||||||
|
|
||||||
# 사용 가능한 모든 태스크 목록
|
# 사용 가능한 모든 태스크 목록
|
||||||
$GRADLE_CMD tasks --all
|
./gradlew tasks --all
|
||||||
```
|
```
|
||||||
|
|
||||||
**주의**: gradlew 사용 금지 - offline gradle 단독 실행
|
|
||||||
|
|
||||||
## 프로젝트 구조
|
## 프로젝트 구조
|
||||||
|
|
||||||
### 전체 프로젝트 구조
|
### 전체 프로젝트 구조
|
||||||
|
|||||||
@@ -252,6 +252,11 @@ public interface MonitoringContext {
|
|||||||
|
|
||||||
public static final String KEYSTORE_UPLOAD_PATH = "keystore.upload.path";
|
public static final String KEYSTORE_UPLOAD_PATH = "keystore.upload.path";
|
||||||
|
|
||||||
|
// 이중 로그인 허용여부
|
||||||
|
public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public abstract String getStringProperty(String propertyName);
|
public abstract String getStringProperty(String propertyName);
|
||||||
|
|
||||||
public abstract String getStringProperty(String propertyName, String defaultString);
|
public abstract String getStringProperty(String propertyName, String defaultString);
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package com.eactive.eai.rms.common.interceptor;
|
package com.eactive.eai.rms.common.interceptor;
|
||||||
|
|
||||||
|
|
||||||
import com.eactive.eai.rms.common.login.LoginVo;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import com.eactive.eai.rms.common.login.SessionManager;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.apache.commons.lang3.ClassUtils;
|
import org.apache.commons.lang3.ClassUtils;
|
||||||
import org.apache.log4j.Logger;
|
import org.apache.log4j.Logger;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import com.eactive.eai.rms.common.login.LoginVo;
|
||||||
|
import com.eactive.eai.rms.common.login.SessionManager;
|
||||||
|
|
||||||
|
|
||||||
public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
|
public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
|
||||||
@@ -20,6 +23,8 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
|
|||||||
private static final Logger logger = Logger
|
private static final Logger logger = Logger
|
||||||
.getLogger(SessionCheckInterceptor.class);
|
.getLogger(SessionCheckInterceptor.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MonitoringContext monitoringContext;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(HttpServletRequest request,
|
public boolean preHandle(HttpServletRequest request,
|
||||||
@@ -39,6 +44,7 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
|
|||||||
|
|
||||||
//craft
|
//craft
|
||||||
boolean isSkippable = ClassUtils.isAssignable(clazz, InterceptorSkipController.class);
|
boolean isSkippable = ClassUtils.isAssignable(clazz, InterceptorSkipController.class);
|
||||||
|
boolean isDualLogin = monitoringContext.getBooleanProperty(MonitoringContext.RMS_DUAL_LOGIN_ENABLED, true);
|
||||||
|
|
||||||
if (isSkippable) {
|
if (isSkippable) {
|
||||||
valid = true;
|
valid = true;
|
||||||
@@ -46,7 +52,7 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
|
|||||||
SessionManager.setLoginVo(loginVo); // ThreadLocal에 설정
|
SessionManager.setLoginVo(loginVo); // ThreadLocal에 설정
|
||||||
|
|
||||||
// 이중 로그인 검사
|
// 이중 로그인 검사
|
||||||
if (!SessionManager.isValidSession(request)) {
|
if (!SessionManager.isValidSession(request, isDualLogin)) {
|
||||||
logger.warn("Duplicate login detected for user: " + loginVo.getUserId());
|
logger.warn("Duplicate login detected for user: " + loginVo.getUserId());
|
||||||
valid = false;
|
valid = false;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -391,20 +391,24 @@ public class MainController implements InterceptorSkipController {
|
|||||||
|
|
||||||
session.setAttribute("dualLogin", "N");
|
session.setAttribute("dualLogin", "N");
|
||||||
|
|
||||||
// 이중 로그인 확인
|
boolean isDualLogin = monitoringContext.getBooleanProperty(MonitoringContext.RMS_DUAL_LOGIN_ENABLED, true);
|
||||||
boolean isNewLogin = SessionManager.registerUserSession(request, dto);
|
|
||||||
|
|
||||||
if (!isNewLogin) {
|
if (!isDualLogin) {
|
||||||
|
// 이중 로그인 확인
|
||||||
|
boolean isNewLogin = SessionManager.registerUserSession(request, dto);
|
||||||
|
|
||||||
// 1.
|
if (!isNewLogin) {
|
||||||
SessionManager.forceRegisterUserSession(request, dto);
|
|
||||||
session.setAttribute("dualLogin", "Y");
|
|
||||||
|
|
||||||
// 또는
|
// 1.
|
||||||
// 2. 이중 로그인 에러 표시
|
SessionManager.forceRegisterUserSession(request, dto);
|
||||||
// model.addAttribute("error", "다른 기기에서 이미 로그인 중입니다.");
|
session.setAttribute("dualLogin", "Y");
|
||||||
// return "/";
|
|
||||||
}
|
// 또는
|
||||||
|
// 2. 이중 로그인 에러 표시
|
||||||
|
// model.addAttribute("error", "다른 기기에서 이미 로그인 중입니다.");
|
||||||
|
// return "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 세션에 로그인 정보 저장
|
// 세션에 로그인 정보 저장
|
||||||
request.getSession().setAttribute(CommonConstants.LOGIN, dto);
|
request.getSession().setAttribute(CommonConstants.LOGIN, dto);
|
||||||
@@ -665,7 +669,8 @@ public class MainController implements InterceptorSkipController {
|
|||||||
|
|
||||||
if (loginVo != null) {
|
if (loginVo != null) {
|
||||||
// 로그인 세션 제거
|
// 로그인 세션 제거
|
||||||
SessionManager.removeUserSession(loginVo.getUserId());
|
//SessionManager.removeUserSession(loginVo.getUserId());
|
||||||
|
SessionManager.removeUserSessionBySessionId(request.getSession().getId()); //2026.04.29 이중로그인 로그아웃 버그 수정
|
||||||
|
|
||||||
request.getSession().removeAttribute(CommonConstants.LOGIN);
|
request.getSession().removeAttribute(CommonConstants.LOGIN);
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class SessionDestructionListener implements HttpSessionListener {
|
|||||||
HttpSession session = se.getSession();
|
HttpSession session = se.getSession();
|
||||||
|
|
||||||
String userId = (String) session.getAttribute("userId");
|
String userId = (String) session.getAttribute("userId");
|
||||||
|
String sessionId = session.getId();
|
||||||
|
|
||||||
// 로그인 한 사용자의 세션정보 정리
|
// 로그인 한 사용자의 세션정보 정리
|
||||||
if (userId != null) {
|
if (userId != null) {
|
||||||
@@ -34,7 +35,8 @@ public class SessionDestructionListener implements HttpSessionListener {
|
|||||||
logger.debug("[SessionListener] 세션만료 감지. 사용자 제거: " + userId);
|
logger.debug("[SessionListener] 세션만료 감지. 사용자 제거: " + userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionManager.removeUserSession(userId);
|
//SessionManager.removeUserSession(userId);
|
||||||
|
SessionManager.removeUserSessionBySessionId(sessionId); //2026.04.29 이중로그인 로그아웃 버그 수정
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||||
import com.eactive.eai.rms.common.util.CommonConstants;
|
import com.eactive.eai.rms.common.util.CommonConstants;
|
||||||
|
|
||||||
//craft
|
//craft
|
||||||
@@ -206,23 +207,38 @@ public class SessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자의 로그인 세션을 제거 (이중로그인 버그 수정용)
|
||||||
|
* @param sessionId 로그인한 session ID
|
||||||
|
*/
|
||||||
|
public static void removeUserSessionBySessionId(String sessionId) {
|
||||||
|
loggedInUsers.entrySet().removeIf(entry -> sessionId.equals(entry.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 현재 세션이 유효한 로그인 세션인지 확인
|
* 현재 세션이 유효한 로그인 세션인지 확인
|
||||||
* @param request HTTP 요청
|
* @param request HTTP 요청
|
||||||
|
* @param isDualLogin 이중 로그인 허용여부
|
||||||
* @return true: 유효한 세션, false: 유효하지 않은 세션
|
* @return true: 유효한 세션, false: 유효하지 않은 세션
|
||||||
*/
|
*/
|
||||||
public static boolean isValidSession(HttpServletRequest request) {
|
public static boolean isValidSession(HttpServletRequest request, boolean isDualLogin) {
|
||||||
LoginVo loginVo = getLoginVo(request);
|
LoginVo loginVo = getLoginVo(request);
|
||||||
if (loginVo == null) {
|
if (loginVo == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String userId = loginVo.getUserId();
|
if (isDualLogin) {
|
||||||
String currentSessionId = request.getSession().getId();
|
return true;
|
||||||
String registeredSessionId = loggedInUsers.get(userId);
|
} else {
|
||||||
|
String userId = loginVo.getUserId();
|
||||||
|
String currentSessionId = request.getSession().getId();
|
||||||
|
String registeredSessionId = loggedInUsers.get(userId);
|
||||||
|
|
||||||
// 등록된 세션이 없거나 현재 세션과 다른 경우
|
// 등록된 세션이 없거나 현재 세션과 다른 경우
|
||||||
return registeredSessionId != null && registeredSessionId.equals(currentSessionId);
|
return registeredSessionId != null && registeredSessionId.equals(currentSessionId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
package com.eactive.eai.rms.common.scheduler;
|
package com.eactive.eai.rms.common.scheduler;
|
||||||
|
|
||||||
import com.eactive.eai.rms.common.base.BaseService;
|
import java.text.SimpleDateFormat;
|
||||||
import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper;
|
import java.util.Date;
|
||||||
import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI;
|
import java.util.List;
|
||||||
import com.eactive.eai.rms.common.scheduler.ui.ScheduleInfo;
|
import java.util.stream.Collectors;
|
||||||
import com.eactive.eai.rms.common.scheduler.ui.SchedulerUISearch;
|
import java.util.stream.StreamSupport;
|
||||||
import com.eactive.eai.rms.data.entity.man.monitoringCode.service.MonitoringCodeService;
|
|
||||||
import com.eactive.eai.rms.data.entity.man.scheduler.JobHistoryService;
|
|
||||||
import com.eactive.eai.rms.data.entity.man.scheduler.JobInfoService;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.quartz.JobDetail;
|
import org.quartz.JobDetail;
|
||||||
import org.quartz.JobKey;
|
import org.quartz.JobKey;
|
||||||
@@ -20,9 +18,14 @@ import org.springframework.data.domain.Pageable;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.List;
|
import com.eactive.eai.rms.common.base.BaseService;
|
||||||
import java.util.stream.Collectors;
|
import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper;
|
||||||
import java.util.stream.StreamSupport;
|
import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI;
|
||||||
|
import com.eactive.eai.rms.common.scheduler.ui.ScheduleInfo;
|
||||||
|
import com.eactive.eai.rms.common.scheduler.ui.SchedulerUISearch;
|
||||||
|
import com.eactive.eai.rms.data.entity.man.monitoringCode.service.MonitoringCodeService;
|
||||||
|
import com.eactive.eai.rms.data.entity.man.scheduler.JobHistoryService;
|
||||||
|
import com.eactive.eai.rms.data.entity.man.scheduler.JobInfoService;
|
||||||
|
|
||||||
@Service("schedulerService")
|
@Service("schedulerService")
|
||||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||||
@@ -122,10 +125,10 @@ public class SchedulerManService extends BaseService {
|
|||||||
|
|
||||||
List<? extends Trigger> triggersOfJob = scheduler.getTriggersOfJob(new JobKey(jobName, Scheduler.DEFAULT_GROUP));
|
List<? extends Trigger> triggersOfJob = scheduler.getTriggersOfJob(new JobKey(jobName, Scheduler.DEFAULT_GROUP));
|
||||||
if(triggersOfJob.size() > 0) {
|
if(triggersOfJob.size() > 0) {
|
||||||
result.setStartTime(String.valueOf(triggersOfJob.get(0).getStartTime()));
|
result.setStartTime(dateFormat(triggersOfJob.get(0).getStartTime()));
|
||||||
result.setEndTime(String.valueOf(triggersOfJob.get(0).getEndTime()));
|
result.setEndTime(dateFormat(triggersOfJob.get(0).getEndTime()));
|
||||||
result.setPreviousFireTime(String.valueOf(triggersOfJob.get(0).getPreviousFireTime()));
|
result.setPreviousFireTime(dateFormat(triggersOfJob.get(0).getPreviousFireTime()));
|
||||||
result.setNextFireTime(String.valueOf(triggersOfJob.get(0).getNextFireTime()));
|
result.setNextFireTime(dateFormat(triggersOfJob.get(0).getNextFireTime()));
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -151,4 +154,13 @@ public class SchedulerManService extends BaseService {
|
|||||||
jobInfoService.deleteById(jobName);
|
jobInfoService.deleteById(jobName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String dateFormat(Date date) {
|
||||||
|
if (date == null) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
return sdf.format(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.eactive.eai.rms.common.util;
|
package com.eactive.eai.rms.common.util;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.log4j.Logger;
|
import org.apache.log4j.Logger;
|
||||||
@@ -191,6 +192,20 @@ public class StringUtils
|
|||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String toString(Object val) {
|
||||||
|
return val != null ? val.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Long toLong(Object val) {
|
||||||
|
return val != null ? ((Number) val).longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BigDecimal toDecimal(Object val) {
|
||||||
|
if (val == null) return null;
|
||||||
|
if (val instanceof BigDecimal) return (BigDecimal) val;
|
||||||
|
return new BigDecimal(val.toString());
|
||||||
|
}
|
||||||
//
|
//
|
||||||
// /**
|
// /**
|
||||||
// * - "" 나 Null 을 입력받아 SPACE 로 변환한다.
|
// * - "" 나 Null 을 입력받아 SPACE 로 변환한다.
|
||||||
|
|||||||
+13
-1
@@ -10,6 +10,9 @@ import org.springframework.data.domain.Pageable;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||||
public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, String, PortalNoticeRepository> {
|
public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, String, PortalNoticeRepository> {
|
||||||
@@ -19,6 +22,10 @@ public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, St
|
|||||||
|
|
||||||
BooleanBuilder predicate = new BooleanBuilder();
|
BooleanBuilder predicate = new BooleanBuilder();
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(portalNoticeUISearch.getSearchNoticeType())) {
|
||||||
|
predicate.and(qPortalNotice.noticeType.eq(portalNoticeUISearch.getSearchNoticeType()));
|
||||||
|
}
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(portalNoticeUISearch.getSearchUseYn())) {
|
if (StringUtils.isNotBlank(portalNoticeUISearch.getSearchUseYn())) {
|
||||||
predicate.and(qPortalNotice.useYn.eq(portalNoticeUISearch.getSearchUseYn()));
|
predicate.and(qPortalNotice.useYn.eq(portalNoticeUISearch.getSearchUseYn()));
|
||||||
}
|
}
|
||||||
@@ -28,7 +35,12 @@ public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, St
|
|||||||
.or(qPortalNotice.noticeDetail.containsIgnoreCase(portalNoticeUISearch.getSearchSubjectDetail())));
|
.or(qPortalNotice.noticeDetail.containsIgnoreCase(portalNoticeUISearch.getSearchSubjectDetail())));
|
||||||
}
|
}
|
||||||
|
|
||||||
return repository.findAll(predicate, pageable);
|
//return repository.findAll(predicate, pageable);
|
||||||
|
|
||||||
|
Sort fixYnFirst = Sort.by(Sort.Direction.DESC, "fixYn");
|
||||||
|
Sort combinedSort = fixYnFirst.and(pageable.getSort());
|
||||||
|
Pageable fixedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), combinedSort);
|
||||||
|
return repository.findAll(predicate, fixedPageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+1
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class PortalNoticeUISearch {
|
public class PortalNoticeUISearch {
|
||||||
|
|
||||||
|
private String searchNoticeType;
|
||||||
private String searchUseYn;
|
private String searchUseYn;
|
||||||
private String searchSubjectDetail;
|
private String searchSubjectDetail;
|
||||||
|
|
||||||
|
|||||||
+109
-2
@@ -1,6 +1,9 @@
|
|||||||
package com.eactive.eai.rms.data.entity.onl.inflow;
|
package com.eactive.eai.rms.data.entity.onl.inflow;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
@@ -13,6 +16,7 @@ import org.springframework.data.domain.PageImpl;
|
|||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.app.entity.QCredential;
|
||||||
import com.eactive.eai.data.entity.onl.adapter.QAdapterGroup;
|
import com.eactive.eai.data.entity.onl.adapter.QAdapterGroup;
|
||||||
import com.eactive.eai.data.entity.onl.inflow.InflowControl;
|
import com.eactive.eai.data.entity.onl.inflow.InflowControl;
|
||||||
import com.eactive.eai.data.entity.onl.inflow.InflowControlId;
|
import com.eactive.eai.data.entity.onl.inflow.InflowControlId;
|
||||||
@@ -33,10 +37,15 @@ import com.querydsl.jpa.impl.JPAQueryFactory;
|
|||||||
public class InflowControlService extends AbstractDataService<InflowControl, InflowControlId, InflowControlRepository> {
|
public class InflowControlService extends AbstractDataService<InflowControl, InflowControlId, InflowControlRepository> {
|
||||||
|
|
||||||
private static final String ADAPTER_TYPE_INFLOW = "01";
|
private static final String ADAPTER_TYPE_INFLOW = "01";
|
||||||
|
private static final String INTERFACE_TYPE_INFLOW = "02";
|
||||||
|
private static final String CLIENT_TYPE_INFLOW = "03";
|
||||||
|
|
||||||
@PersistenceContext
|
@PersistenceContext
|
||||||
EntityManager entityManager;
|
EntityManager entityManager;
|
||||||
|
|
||||||
|
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
|
||||||
|
EntityManager entityManagerForEMS;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
InflowControlManMapper mapper;
|
InflowControlManMapper mapper;
|
||||||
|
|
||||||
@@ -124,7 +133,7 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
|
|||||||
.select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc)
|
.select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc)
|
||||||
.from(qEAIMessageEntity)
|
.from(qEAIMessageEntity)
|
||||||
.leftJoin(qInflowControl)
|
.leftJoin(qInflowControl)
|
||||||
.on(qInflowControl.id.type.eq("02").and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
|
.on(qInflowControl.id.type.eq(INTERFACE_TYPE_INFLOW).and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
|
||||||
.where(qEAIMessageEntity.eaisvcname.contains(searchName))
|
.where(qEAIMessageEntity.eaisvcname.contains(searchName))
|
||||||
.orderBy(qEAIMessageEntity.eaisvcname.asc())
|
.orderBy(qEAIMessageEntity.eaisvcname.asc())
|
||||||
.offset(pageable.getOffset())
|
.offset(pageable.getOffset())
|
||||||
@@ -144,6 +153,8 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
|
|||||||
return new PageImpl<>(dtoList, pageable, totalCount);
|
return new PageImpl<>(dtoList, pageable, totalCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private InflowControlServiceDto toDto(QEAIMessageEntity qEAIMessageEntity, QInflowControl qInflowControl,
|
private InflowControlServiceDto toDto(QEAIMessageEntity qEAIMessageEntity, QInflowControl qInflowControl,
|
||||||
Tuple tuple) {
|
Tuple tuple) {
|
||||||
InflowControlServiceDto dto = null;
|
InflowControlServiceDto dto = null;
|
||||||
@@ -167,13 +178,109 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
|
|||||||
.select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc)
|
.select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc)
|
||||||
.from(qEAIMessageEntity)
|
.from(qEAIMessageEntity)
|
||||||
.leftJoin(qInflowControl)
|
.leftJoin(qInflowControl)
|
||||||
.on(qInflowControl.id.type.eq("02").and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
|
.on(qInflowControl.id.type.eq(INTERFACE_TYPE_INFLOW).and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
|
||||||
.where(qEAIMessageEntity.eaisvcname.eq(eaisvcname))
|
.where(qEAIMessageEntity.eaisvcname.eq(eaisvcname))
|
||||||
.fetchOne();
|
.fetchOne();
|
||||||
|
|
||||||
return toDto(qEAIMessageEntity, qInflowControl, tuple);
|
return toDto(qEAIMessageEntity, qInflowControl, tuple);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InflowControlServiceDto findByIdForClient(String clientId) {
|
||||||
|
QCredential qCredential = QCredential.credential;
|
||||||
|
QInflowControl qInflowControl = QInflowControl.inflowControl;
|
||||||
|
|
||||||
|
// EMSADM: Credential 조회
|
||||||
|
Tuple credTuple = new JPAQueryFactory(entityManagerForEMS)
|
||||||
|
.select(qCredential.clientid, qCredential.clientname)
|
||||||
|
.from(qCredential)
|
||||||
|
.where(qCredential.clientid.eq(clientId))
|
||||||
|
.fetchOne();
|
||||||
|
|
||||||
|
// AGWADM: InflowControl 조회
|
||||||
|
InflowControl inflowControl = new JPAQueryFactory(entityManager)
|
||||||
|
.selectFrom(qInflowControl)
|
||||||
|
.where(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
|
||||||
|
.and(qInflowControl.id.name.eq(clientId)))
|
||||||
|
.fetchOne();
|
||||||
|
|
||||||
|
InflowControlServiceDto dto;
|
||||||
|
if (inflowControl != null) {
|
||||||
|
dto = mapper.toDto(inflowControl);
|
||||||
|
} else {
|
||||||
|
dto = new InflowControlServiceDto();
|
||||||
|
dto.setName(clientId);
|
||||||
|
dto.setType(CLIENT_TYPE_INFLOW);
|
||||||
|
}
|
||||||
|
if (credTuple != null) {
|
||||||
|
dto.setDesc(credTuple.get(qCredential.clientname));
|
||||||
|
}
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<InflowControlServiceDto> findAllForClient(Pageable pageable, String searchName) {
|
||||||
|
QCredential qCredential = QCredential.credential;
|
||||||
|
QInflowControl qInflowControl = QInflowControl.inflowControl;
|
||||||
|
|
||||||
|
// EMSADM: Credential 목록 및 건수 조회
|
||||||
|
JPAQueryFactory emsFactory = new JPAQueryFactory(entityManagerForEMS);
|
||||||
|
BooleanBuilder credPredicate = new BooleanBuilder();
|
||||||
|
if (!StringUtils.isEmpty(searchName)) {
|
||||||
|
credPredicate.and(qCredential.clientname.contains(searchName));
|
||||||
|
}
|
||||||
|
|
||||||
|
long totalCount = emsFactory
|
||||||
|
.select(qCredential.clientid.count())
|
||||||
|
.from(qCredential)
|
||||||
|
.where(credPredicate)
|
||||||
|
.fetchOne();
|
||||||
|
|
||||||
|
List<Tuple> credList = emsFactory
|
||||||
|
.select(qCredential.clientid, qCredential.clientname)
|
||||||
|
.from(qCredential)
|
||||||
|
.where(credPredicate)
|
||||||
|
.orderBy(qCredential.clientname.asc())
|
||||||
|
.offset(pageable.getOffset())
|
||||||
|
.limit(pageable.getPageSize())
|
||||||
|
.fetch();
|
||||||
|
|
||||||
|
if (credList.isEmpty()) {
|
||||||
|
return new PageImpl<>(Collections.emptyList(), pageable, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AGWADM: 해당 clientId의 InflowControl 일괄 조회
|
||||||
|
List<String> clientIds = credList.stream()
|
||||||
|
.map(t -> t.get(qCredential.clientid))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
Map<String, InflowControl> inflowMap = new JPAQueryFactory(entityManager)
|
||||||
|
.selectFrom(qInflowControl)
|
||||||
|
.where(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
|
||||||
|
.and(qInflowControl.id.name.in(clientIds)))
|
||||||
|
.fetch()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(ic -> ic.getId().getName(), ic -> ic));
|
||||||
|
|
||||||
|
// Java에서 병합
|
||||||
|
List<InflowControlServiceDto> dtoList = credList.stream()
|
||||||
|
.map(tuple -> {
|
||||||
|
String clientId = tuple.get(qCredential.clientid);
|
||||||
|
InflowControl inflowControl = inflowMap.get(clientId);
|
||||||
|
InflowControlServiceDto dto;
|
||||||
|
if (inflowControl != null) {
|
||||||
|
dto = mapper.toDto(inflowControl);
|
||||||
|
} else {
|
||||||
|
dto = new InflowControlServiceDto();
|
||||||
|
dto.setName(clientId);
|
||||||
|
dto.setType(CLIENT_TYPE_INFLOW);
|
||||||
|
}
|
||||||
|
dto.setDesc(tuple.get(qCredential.clientname));
|
||||||
|
return dto;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new PageImpl<>(dtoList, pageable, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
public Page<Tuple> selectLogList(Pageable pageable, InflowControlHistoryManUISearch uiSearch) {
|
public Page<Tuple> selectLogList(Pageable pageable, InflowControlHistoryManUISearch uiSearch) {
|
||||||
QInflowControlLog qlog = QInflowControlLog.inflowControlLog;
|
QInflowControlLog qlog = QInflowControlLog.inflowControlLog;
|
||||||
QInflowControlGroup qgroup = QInflowControlGroup.inflowControlGroup;
|
QInflowControlGroup qgroup = QInflowControlGroup.inflowControlGroup;
|
||||||
|
|||||||
+4
-4
@@ -120,11 +120,11 @@ public class ApiStatsDayService
|
|||||||
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
||||||
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
||||||
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
||||||
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
|
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
|
||||||
Expressions.cases()
|
Expressions.cases()
|
||||||
.when(q.successCnt.sum().gt(0))
|
.when(q.totalCnt.sum().gt(0))
|
||||||
.then(q.avgRespTime.multiply(q.successCnt).sum()
|
.then(q.avgRespTime.multiply(q.totalCnt).sum()
|
||||||
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
|
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
|
||||||
.otherwise((BigDecimal) null)
|
.otherwise((BigDecimal) null)
|
||||||
.as("avgRespTime")))
|
.as("avgRespTime")))
|
||||||
.from(q)
|
.from(q)
|
||||||
|
|||||||
+6
-7
@@ -211,11 +211,10 @@ public class ApiStatsHourService
|
|||||||
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
||||||
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
||||||
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
||||||
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
|
|
||||||
Expressions.cases()
|
Expressions.cases()
|
||||||
.when(q.successCnt.sum().gt(0))
|
.when(q.totalCnt.sum().gt(0))
|
||||||
.then(q.avgRespTime.multiply(q.successCnt).sum()
|
.then(q.avgRespTime.multiply(q.totalCnt).sum()
|
||||||
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
|
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
|
||||||
.otherwise((BigDecimal) null)
|
.otherwise((BigDecimal) null)
|
||||||
.as("avgRespTime")))
|
.as("avgRespTime")))
|
||||||
.from(q)
|
.from(q)
|
||||||
@@ -293,9 +292,9 @@ public class ApiStatsHourService
|
|||||||
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
||||||
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
||||||
Expressions.cases()
|
Expressions.cases()
|
||||||
.when(q.successCnt.sum().gt(0))
|
.when(q.totalCnt.sum().gt(0))
|
||||||
.then(q.avgRespTime.multiply(q.successCnt).sum()
|
.then(q.avgRespTime.multiply(q.totalCnt).sum()
|
||||||
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
|
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
|
||||||
.otherwise((BigDecimal) null)
|
.otherwise((BigDecimal) null)
|
||||||
.as("avgRespTime")))
|
.as("avgRespTime")))
|
||||||
.from(q)
|
.from(q)
|
||||||
|
|||||||
+4
-4
@@ -214,11 +214,11 @@ public class ApiStatsMinuteService
|
|||||||
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
||||||
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
||||||
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
||||||
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
|
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
|
||||||
Expressions.cases()
|
Expressions.cases()
|
||||||
.when(q.successCnt.sum().gt(0))
|
.when(q.totalCnt.sum().gt(0))
|
||||||
.then(q.avgRespTime.multiply(q.successCnt).sum()
|
.then(q.avgRespTime.multiply(q.totalCnt).sum()
|
||||||
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
|
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
|
||||||
.otherwise((BigDecimal) null)
|
.otherwise((BigDecimal) null)
|
||||||
.as("avgRespTime")))
|
.as("avgRespTime")))
|
||||||
.from(q)
|
.from(q)
|
||||||
|
|||||||
+7
-7
@@ -120,11 +120,11 @@ public class ApiStatsMonthService
|
|||||||
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
||||||
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
||||||
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
||||||
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
|
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
|
||||||
Expressions.cases()
|
Expressions.cases()
|
||||||
.when(q.successCnt.sum().gt(0))
|
.when(q.totalCnt.sum().gt(0))
|
||||||
.then(q.avgRespTime.multiply(q.successCnt).sum()
|
.then(q.avgRespTime.multiply(q.totalCnt).sum()
|
||||||
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
|
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
|
||||||
.otherwise((BigDecimal) null)
|
.otherwise((BigDecimal) null)
|
||||||
.as("avgRespTime")))
|
.as("avgRespTime")))
|
||||||
.from(q)
|
.from(q)
|
||||||
@@ -172,9 +172,9 @@ public class ApiStatsMonthService
|
|||||||
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
||||||
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
||||||
Expressions.cases()
|
Expressions.cases()
|
||||||
.when(q.successCnt.sum().gt(0))
|
.when(q.totalCnt.sum().gt(0))
|
||||||
.then(q.avgRespTime.multiply(q.successCnt).sum()
|
.then(q.avgRespTime.multiply(q.totalCnt).sum()
|
||||||
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
|
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
|
||||||
.otherwise((BigDecimal) null)
|
.otherwise((BigDecimal) null)
|
||||||
.as("avgRespTime")))
|
.as("avgRespTime")))
|
||||||
.from(q)
|
.from(q)
|
||||||
|
|||||||
+7
-7
@@ -117,11 +117,11 @@ public class ApiStatsYearService
|
|||||||
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
||||||
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
||||||
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
||||||
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
|
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
|
||||||
Expressions.cases()
|
Expressions.cases()
|
||||||
.when(q.successCnt.sum().gt(0))
|
.when(q.totalCnt.sum().gt(0))
|
||||||
.then(q.avgRespTime.multiply(q.successCnt).sum()
|
.then(q.avgRespTime.multiply(q.totalCnt).sum()
|
||||||
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
|
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
|
||||||
.otherwise((BigDecimal) null)
|
.otherwise((BigDecimal) null)
|
||||||
.as("avgRespTime")))
|
.as("avgRespTime")))
|
||||||
.from(q)
|
.from(q)
|
||||||
@@ -169,9 +169,9 @@ public class ApiStatsYearService
|
|||||||
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
||||||
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
|
||||||
Expressions.cases()
|
Expressions.cases()
|
||||||
.when(q.successCnt.sum().gt(0))
|
.when(q.totalCnt.sum().gt(0))
|
||||||
.then(q.avgRespTime.multiply(q.successCnt).sum()
|
.then(q.avgRespTime.multiply(q.totalCnt).sum()
|
||||||
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
|
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
|
||||||
.otherwise((BigDecimal) null)
|
.otherwise((BigDecimal) null)
|
||||||
.as("avgRespTime")))
|
.as("avgRespTime")))
|
||||||
.from(q)
|
.from(q)
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package com.eactive.eai.rms.data.entity.onl.security;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
|
import com.eactive.eai.data.DataService;
|
||||||
|
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||||
|
|
||||||
|
public interface CryptoModuleConfigDataService extends DataService<CryptoModuleConfig, String> {
|
||||||
|
|
||||||
|
Page<CryptoModuleConfig> findAll(Pageable pageable, String searchName, String algType, String keySourceType, String useYn);
|
||||||
|
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package com.eactive.eai.rms.data.entity.onl.security;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||||
|
import com.eactive.eai.data.entity.onl.security.QCryptoModuleConfig;
|
||||||
|
import com.eactive.eai.data.jpa.AbstractDataService;
|
||||||
|
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class CryptoModuleConfigDataServiceImpl
|
||||||
|
extends AbstractDataService<CryptoModuleConfig, String, CryptoModuleConfigRepository>
|
||||||
|
implements CryptoModuleConfigDataService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<CryptoModuleConfig> findAll(Pageable pageable, String searchName, String algType,
|
||||||
|
String keySourceType, String useYn) {
|
||||||
|
QCryptoModuleConfig q = QCryptoModuleConfig.cryptoModuleConfig;
|
||||||
|
|
||||||
|
BooleanExpression predicate = q.cryptoName.containsIgnoreCase(searchName != null ? searchName : "");
|
||||||
|
|
||||||
|
if (algType != null && !algType.isEmpty()) {
|
||||||
|
predicate = predicate.and(q.algType.eq(algType));
|
||||||
|
}
|
||||||
|
if (keySourceType != null && !keySourceType.isEmpty()) {
|
||||||
|
predicate = predicate.and(q.keySourceType.eq(keySourceType));
|
||||||
|
}
|
||||||
|
if (useYn != null && !useYn.isEmpty()) {
|
||||||
|
predicate = predicate.and(q.useYn.eq(useYn));
|
||||||
|
}
|
||||||
|
|
||||||
|
return repository.findAll(predicate, pageable);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package com.eactive.eai.rms.data.entity.onl.security;
|
||||||
|
|
||||||
|
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
|
||||||
|
|
||||||
|
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||||
|
import com.eactive.eai.data.jpa.BaseRepository;
|
||||||
|
|
||||||
|
interface CryptoModuleConfigRepository
|
||||||
|
extends BaseRepository<CryptoModuleConfig, String>, QuerydslPredicateExecutor<CryptoModuleConfig> {
|
||||||
|
}
|
||||||
+1
-1
@@ -26,7 +26,7 @@ public class ExtendedColumnDefinitionService
|
|||||||
public List<ExtendedColumnDefinition> findAll() {
|
public List<ExtendedColumnDefinition> findAll() {
|
||||||
QExtendedColumnDefinition q = QExtendedColumnDefinition.extendedColumnDefinition;
|
QExtendedColumnDefinition q = QExtendedColumnDefinition.extendedColumnDefinition;
|
||||||
List<ExtendedColumnDefinition> list = new ArrayList<>();
|
List<ExtendedColumnDefinition> list = new ArrayList<>();
|
||||||
repository.findAll(q.isKey.desc(), q.keySeq.asc(), q.columnName.asc()).iterator().forEachRemaining(list::add);
|
repository.findAll(q.orderSeq.asc(), q.isKey.desc(), q.keySeq.asc(), q.columnName.asc()).iterator().forEachRemaining(list::add);
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.apistatus;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.LastModifiedBy;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
|
||||||
|
import com.eactive.eai.data.entity.AbstractEntity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
|
||||||
|
@Entity
|
||||||
|
@Table(name = "API_STATUS")
|
||||||
|
public class ApiStatus extends AbstractEntity<String> {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "EAISVCNAME", length = 100)
|
||||||
|
private String eaisvcname;
|
||||||
|
|
||||||
|
@Column(name = "STATUS_CODE", length = 1)
|
||||||
|
private String statusCode;
|
||||||
|
|
||||||
|
@LastModifiedBy // ← EMSAuditorAware가 자동으로 채움
|
||||||
|
@Column(name = "MODIFIED_BY", length = 50)
|
||||||
|
private String modifiedBy = "SCHEDULER";
|
||||||
|
|
||||||
|
@LastModifiedDate // ← save() 시 자동으로 sysdate 채움
|
||||||
|
@Column(name = "MODIFIED_DATE")
|
||||||
|
private LocalDateTime modifiedDate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String getId() { return eaisvcname; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.apistatus;
|
||||||
|
|
||||||
|
public interface ApiStatusEvent {
|
||||||
|
String getEaisvcname();
|
||||||
|
String getEaisvcdesc();
|
||||||
|
String getEvent();
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.apistatus;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import com.eactive.eai.data.jpa.BaseRepository;
|
||||||
|
|
||||||
|
public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 상태 판단. 정상(N), 점검(C), 지연(D), 장애(E)
|
||||||
|
*
|
||||||
|
* 1. 정상/지연/장애 -> 점검중 : CONTROL_START
|
||||||
|
* 2. 점검 -> 점검X : CONTROL_END
|
||||||
|
* 3. 정상/지연 -> 장애 : ERROR_START
|
||||||
|
* 4. 장애 -> 장애X : ERROR_END
|
||||||
|
* 5. 정상 -> 지연 : DELAY_START
|
||||||
|
* 6. 지연 -> 지연X : DELAY_END
|
||||||
|
* 7. 기타 : STAY
|
||||||
|
*/
|
||||||
|
@Query(nativeQuery = true, value =
|
||||||
|
" SELECT EAISVCNAME"
|
||||||
|
+ " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC"
|
||||||
|
+ " , EVENT"
|
||||||
|
+ " FROM ("
|
||||||
|
+ " SELECT EAISVCNAME"
|
||||||
|
+ " , CASE WHEN STATUS_CODE != 'C' AND CTRL_YN = 'Y' THEN 'CONTROL_START'"
|
||||||
|
+ " WHEN STATUS_CODE = 'C' AND CTRL_YN = 'N' THEN 'CONTROL_END'"
|
||||||
|
+ " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'"
|
||||||
|
+ " WHEN STATUS_CODE = 'E' AND ERR_YN = 'N' THEN 'ERROR_END'"
|
||||||
|
+ " WHEN STATUS_CODE = 'N' AND DELAY_YN = 'Y' THEN 'DELAY_START'"
|
||||||
|
+ " WHEN STATUS_CODE = 'D' AND DELAY_YN = 'N' THEN 'DELAY_END'"
|
||||||
|
+ " ELSE 'STAY' END AS EVENT"
|
||||||
|
+ " FROM ("
|
||||||
|
+ " SELECT A.EAISVCNAME"
|
||||||
|
+ " , NVL(B.STATUS_CODE, '1') AS STATUS_CODE"
|
||||||
|
+ " , NVL((SELECT 'Y' FROM TSEAITI01"
|
||||||
|
+ " WHERE (TO_CHAR(SYSDATE,'HH24MI') BETWEEN SUBSTR(EAICTRLDSTICCTNT,16,4) AND SUBSTR(EAICTRLDSTICCTNT,21,4)"
|
||||||
|
+ " OR SUBSTR(EAICTRLDSTICCTNT,16,9) = '0000|0000')"
|
||||||
|
+ " AND A.EAISVCNAME = EAICTRLNAME),'N') AS CTRL_YN"
|
||||||
|
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
|
||||||
|
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
|
||||||
|
+ " ELSE 'N' END"
|
||||||
|
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
|
||||||
|
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
|
||||||
|
+ " FROM API_STATS_MINUTE"
|
||||||
|
+ " WHERE A.EAISVCNAME = API_NAME"
|
||||||
|
+ " AND TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')"
|
||||||
|
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:errorRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')"
|
||||||
|
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS ERR_YN"
|
||||||
|
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
|
||||||
|
+ " WHEN RESP_SUM / TOTAL > :delayAvgRespTime THEN 'Y'"
|
||||||
|
+ " ELSE 'N' END"
|
||||||
|
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
|
||||||
|
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
|
||||||
|
+ " , SUM((SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) * AVG_RESP_TIME) AS RESP_SUM"
|
||||||
|
+ " FROM API_STATS_MINUTE"
|
||||||
|
+ " WHERE A.EAISVCNAME = API_NAME"
|
||||||
|
+ " AND TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')"
|
||||||
|
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:delayRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')"
|
||||||
|
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS DELAY_YN"
|
||||||
|
+ " FROM TSEAIHE01 A LEFT OUTER JOIN API_STATUS B ON A.EAISVCNAME = B.EAISVCNAME"
|
||||||
|
+ " )"
|
||||||
|
+ " ) A WHERE EVENT != 'STAY'")
|
||||||
|
List<ApiStatusEvent> findApiStatusEvents(
|
||||||
|
@Param("errorRate") int errorRate,
|
||||||
|
@Param("errorRangeMinute") int errorRangeMinute,
|
||||||
|
@Param("delayRangeMinute") int delayRangeMinute,
|
||||||
|
@Param("delayAvgRespTime") int delayAvgRespTime
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.apistatus;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.ext.djb.event.ApiStatusChangedEvent;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class ApiStatusService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApiStatusRepository apiStatusRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void updateApiStatus(HashMap<String, String> param) {
|
||||||
|
|
||||||
|
List<ApiStatusEvent> list = apiStatusRepository.findApiStatusEvents(
|
||||||
|
Integer.parseInt(param.get("errorRate")),
|
||||||
|
Integer.parseInt(param.get("errorRangeMinute")),
|
||||||
|
Integer.parseInt(param.get("delayRangeMinute")),
|
||||||
|
Integer.parseInt(param.get("delayAvgRespTime"))
|
||||||
|
);
|
||||||
|
|
||||||
|
for (ApiStatusEvent event : list) {
|
||||||
|
String newStatusCode = resolveStatusCode(event.getEvent());
|
||||||
|
if (newStatusCode == null) continue;
|
||||||
|
|
||||||
|
ApiStatus apiStatus = apiStatusRepository.findById(event.getEaisvcname()).orElse(new ApiStatus());
|
||||||
|
|
||||||
|
apiStatus.setEaisvcname(event.getEaisvcname());
|
||||||
|
apiStatus.setStatusCode(newStatusCode);
|
||||||
|
|
||||||
|
apiStatusRepository.save(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT
|
||||||
|
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(ApiStatusChangedEvent.from(event)); // 트랜잭션 커밋 후 리스너 실행
|
||||||
|
log.debug("이벤트 전파");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String resolveStatusCode(String event) {
|
||||||
|
switch (event) {
|
||||||
|
case "CONTROL_START": return "C"; //점검
|
||||||
|
case "CONTROL_END": return "N"; //정상
|
||||||
|
case "ERROR_START": return "E"; //장애
|
||||||
|
case "ERROR_END": return "N"; //정상
|
||||||
|
case "DELAY_START": return "D"; //지연
|
||||||
|
case "DELAY_END": return "N"; //정상
|
||||||
|
default:
|
||||||
|
log.warn("알 수 없는 이벤트: {}", event);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.inflow;
|
||||||
|
|
||||||
|
public interface InflowTokenInsufficient {
|
||||||
|
String getEaisvcname();
|
||||||
|
String getEaisvcdesc();
|
||||||
|
Long getCnt();
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.inflow;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.EmbeddedId;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
import com.eactive.eai.data.entity.AbstractEntity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
|
||||||
|
@Entity
|
||||||
|
@Table(name = "TSEAIFR11")
|
||||||
|
public class InflowTokenInsufficientLog extends AbstractEntity<InflowTokenInsufficientLogId> implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@EmbeddedId
|
||||||
|
@EqualsAndHashCode.Include
|
||||||
|
private InflowTokenInsufficientLogId id;
|
||||||
|
|
||||||
|
@Column(name = "EAISEVRINSTNCNAME", length = 20)
|
||||||
|
private String eaisevrinstncname;
|
||||||
|
|
||||||
|
@Column(name = "EAISVCNAME", length = 30)
|
||||||
|
private String eaisvcname;
|
||||||
|
|
||||||
|
@Column(name = "ADPTRBZWKGROUPNAME", length = 50)
|
||||||
|
private String adptrbzwkgroupname;
|
||||||
|
|
||||||
|
@Column(name = "THRESHOLDPERSECOND")
|
||||||
|
private Long thresholdpersecond;
|
||||||
|
|
||||||
|
@Column(name = "THRESHOLD")
|
||||||
|
private Long threshold;
|
||||||
|
|
||||||
|
@Column(name = "THRESHOLDTIMEUNIT", length = 12)
|
||||||
|
private String thresholdtimeunit;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull InflowTokenInsufficientLogId getId() { return id; }
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.inflow;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Embeddable;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Embeddable
|
||||||
|
public class InflowTokenInsufficientLogId implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Column(name = "EAIBZWKDSTCD", length = 4)
|
||||||
|
private String eaibzwkdstcd;
|
||||||
|
|
||||||
|
@Column(name = "MSGDPSTYMS", length = 17)
|
||||||
|
private String msgdpstyms;
|
||||||
|
|
||||||
|
@Column(name = "EAISVCSERNO", length = 41)
|
||||||
|
private String eaisvcserno;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.inflow;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import com.eactive.eai.data.jpa.BaseRepository;
|
||||||
|
|
||||||
|
public interface InflowTokenRepository extends BaseRepository<InflowTokenInsufficientLog, InflowTokenInsufficientLogId> {
|
||||||
|
|
||||||
|
@Query(nativeQuery = true, value =
|
||||||
|
" SELECT EAISVCNAME" +
|
||||||
|
" , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC" +
|
||||||
|
" , COUNT(*) AS CNT" +
|
||||||
|
" FROM TSEAIFR11 A" +
|
||||||
|
" WHERE MSGDPSTYMS >= TO_CHAR(SYSTIMESTAMP - NUMTODSINTERVAL(:rangeMinute, 'MINUTE'), 'YYYYMMDDHH24MISSFF3')" +
|
||||||
|
" GROUP BY EAISVCNAME")
|
||||||
|
List<InflowTokenInsufficient> countTokenInsufficient(@Param("rangeMinute") long rangeMinute);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.inflow;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.ext.djb.event.InflowTokenInsufficientEvent;
|
||||||
|
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class InflowTokenService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(InflowTokenService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private InflowTokenRepository inflowTokenRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
public void checkRecentFails(long rangeMinute) {
|
||||||
|
List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute);
|
||||||
|
|
||||||
|
for (InflowTokenInsufficient info : rows) {
|
||||||
|
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
|
||||||
|
|
||||||
|
//내부직원 알림 발송
|
||||||
|
eventPublisher.publishEvent(InflowTokenInsufficientEvent.from(info, rangeMinute));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+256
@@ -0,0 +1,256 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.statistics;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.apache.poi.ss.usermodel.BorderStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.Cell;
|
||||||
|
import org.apache.poi.ss.usermodel.CellStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||||
|
import org.apache.poi.ss.usermodel.Font;
|
||||||
|
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
||||||
|
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||||
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 통계 Excel Export 서비스
|
||||||
|
* Hour/Minute 통계 데이터를 Excel 파일로 생성
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ApiUseStatsExcelExportService {
|
||||||
|
|
||||||
|
private static final String[] COLUMN_HEADERS = {
|
||||||
|
"구분", "총건수", "성공", "성공율(%)", "실패율(%)", "Timeout", "시스템오류",
|
||||||
|
"평균응답(ms)", "최소응답(ms)", "최대응답(ms)"
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 통계 데이터를 Excel 파일로 생성하여 HTTP 응답으로 전송
|
||||||
|
*
|
||||||
|
* @param dataList API 통계 데이터 리스트
|
||||||
|
* @param fileName 다운로드 파일명
|
||||||
|
* @param response HTTP 응답 객체
|
||||||
|
* @throws IOException 파일 생성 실패 시
|
||||||
|
*/
|
||||||
|
public void exportApiStats(List<ApiStatsUI> dataList, String fileName, HttpServletResponse response)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
log.info("Starting Excel export - rows: {}, filename: {}", dataList.size(), fileName);
|
||||||
|
|
||||||
|
try (Workbook workbook = new XSSFWorkbook()) {
|
||||||
|
Sheet sheet = workbook.createSheet("API통계");
|
||||||
|
log.debug("Excel sheet created");
|
||||||
|
|
||||||
|
// 스타일 생성
|
||||||
|
CellStyle headerStyle = createHeaderStyle(workbook);
|
||||||
|
CellStyle stringStyle = createStringStyle(workbook);
|
||||||
|
CellStyle numberStyle = createNumberStyle(workbook);
|
||||||
|
CellStyle decimalStyle = createDecimalStyle(workbook);
|
||||||
|
log.debug("Excel styles created");
|
||||||
|
|
||||||
|
// 헤더 행 생성
|
||||||
|
createHeaderRow(sheet, headerStyle);
|
||||||
|
log.debug("Header row created");
|
||||||
|
|
||||||
|
// 데이터 행 생성
|
||||||
|
int rowNum = 1;
|
||||||
|
for (ApiStatsUI data : dataList) {
|
||||||
|
createDataRow(sheet, rowNum++, data, stringStyle, numberStyle, decimalStyle);
|
||||||
|
}
|
||||||
|
log.info("Data rows created - count: {}", dataList.size());
|
||||||
|
|
||||||
|
// 컬럼 너비 자동 조정
|
||||||
|
autoSizeColumns(sheet);
|
||||||
|
log.debug("Column widths adjusted");
|
||||||
|
|
||||||
|
// HTTP 응답 설정
|
||||||
|
setHttpResponse(response, fileName, workbook);
|
||||||
|
log.info("Excel file sent to response");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error creating Excel file", e);
|
||||||
|
throw new IOException("Excel file creation failed: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 헤더 스타일 생성 (회색 배경, 볼드, 테두리)
|
||||||
|
*/
|
||||||
|
private CellStyle createHeaderStyle(Workbook workbook) {
|
||||||
|
CellStyle style = workbook.createCellStyle();
|
||||||
|
|
||||||
|
// 배경색
|
||||||
|
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
||||||
|
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
|
||||||
|
// 테두리
|
||||||
|
style.setBorderTop(BorderStyle.THIN);
|
||||||
|
style.setBorderBottom(BorderStyle.THIN);
|
||||||
|
style.setBorderLeft(BorderStyle.THIN);
|
||||||
|
style.setBorderRight(BorderStyle.THIN);
|
||||||
|
|
||||||
|
// 정렬
|
||||||
|
style.setAlignment(HorizontalAlignment.CENTER);
|
||||||
|
|
||||||
|
// 폰트 (볼드)
|
||||||
|
Font font = workbook.createFont();
|
||||||
|
font.setBold(true);
|
||||||
|
style.setFont(font);
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열 데이터 스타일 생성 (기본 테두리)
|
||||||
|
*/
|
||||||
|
private CellStyle createStringStyle(Workbook workbook) {
|
||||||
|
CellStyle style = workbook.createCellStyle();
|
||||||
|
style.setBorderTop(BorderStyle.THIN);
|
||||||
|
style.setBorderBottom(BorderStyle.THIN);
|
||||||
|
style.setBorderLeft(BorderStyle.THIN);
|
||||||
|
style.setBorderRight(BorderStyle.THIN);
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 숫자 데이터 스타일 생성 (우측정렬 + 쉼표 구분)
|
||||||
|
*/
|
||||||
|
private CellStyle createNumberStyle(Workbook workbook) {
|
||||||
|
CellStyle style = workbook.createCellStyle();
|
||||||
|
style.setBorderTop(BorderStyle.THIN);
|
||||||
|
style.setBorderBottom(BorderStyle.THIN);
|
||||||
|
style.setBorderLeft(BorderStyle.THIN);
|
||||||
|
style.setBorderRight(BorderStyle.THIN);
|
||||||
|
style.setAlignment(HorizontalAlignment.RIGHT);
|
||||||
|
style.setDataFormat(workbook.createDataFormat().getFormat("#,##0"));
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 소수점 데이터 스타일 생성 (우측정렬 + 소수점 2자리)
|
||||||
|
*/
|
||||||
|
private CellStyle createDecimalStyle(Workbook workbook) {
|
||||||
|
CellStyle style = workbook.createCellStyle();
|
||||||
|
style.setBorderTop(BorderStyle.THIN);
|
||||||
|
style.setBorderBottom(BorderStyle.THIN);
|
||||||
|
style.setBorderLeft(BorderStyle.THIN);
|
||||||
|
style.setBorderRight(BorderStyle.THIN);
|
||||||
|
style.setAlignment(HorizontalAlignment.RIGHT);
|
||||||
|
style.setDataFormat(workbook.createDataFormat().getFormat("0.00"));
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 헤더 행 생성
|
||||||
|
*/
|
||||||
|
private void createHeaderRow(Sheet sheet, CellStyle headerStyle) {
|
||||||
|
Row headerRow = sheet.createRow(0);
|
||||||
|
|
||||||
|
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
|
||||||
|
Cell cell = headerRow.createCell(i);
|
||||||
|
cell.setCellValue(COLUMN_HEADERS[i]);
|
||||||
|
cell.setCellStyle(headerStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데이터 행 생성
|
||||||
|
*/
|
||||||
|
private void createDataRow(Sheet sheet, int rowNum, ApiStatsUI data,
|
||||||
|
CellStyle stringStyle, CellStyle numberStyle, CellStyle decimalStyle) {
|
||||||
|
|
||||||
|
Row row = sheet.createRow(rowNum);
|
||||||
|
int colNum = 0;
|
||||||
|
|
||||||
|
createStringCell(row, colNum++, data.getOrgName(), stringStyle);
|
||||||
|
createLongCell(row, colNum++, data.getTotalCnt(), numberStyle);
|
||||||
|
createLongCell(row, colNum++, data.getSuccessCnt(), numberStyle);
|
||||||
|
createDecimalCell(row, colNum++, data.getSuccessRate(), decimalStyle);
|
||||||
|
createDecimalCell(row, colNum++, data.getFailRate(), decimalStyle);
|
||||||
|
createLongCell(row, colNum++, data.getTimeoutCnt(), numberStyle);
|
||||||
|
createLongCell(row, colNum++, data.getSystemErrCnt(), numberStyle);
|
||||||
|
createDecimalCell(row, colNum++, data.getAvgRespTime(), numberStyle);
|
||||||
|
createDecimalCell(row, colNum++, data.getMinRespTime(), numberStyle);
|
||||||
|
createDecimalCell(row, colNum++, data.getMaxRespTime(), numberStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열 셀 생성
|
||||||
|
*/
|
||||||
|
private void createStringCell(Row row, int colNum, String value, CellStyle style) {
|
||||||
|
Cell cell = row.createCell(colNum);
|
||||||
|
cell.setCellValue(value != null ? value : "");
|
||||||
|
cell.setCellStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Long 타입 숫자 셀 생성
|
||||||
|
*/
|
||||||
|
private void createLongCell(Row row, int colNum, Long value, CellStyle style) {
|
||||||
|
Cell cell = row.createCell(colNum);
|
||||||
|
if (value != null) {
|
||||||
|
cell.setCellValue(value.doubleValue());
|
||||||
|
} else {
|
||||||
|
cell.setCellValue(0);
|
||||||
|
}
|
||||||
|
cell.setCellStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BigDecimal 타입 소수점 셀 생성
|
||||||
|
*/
|
||||||
|
private void createDecimalCell(Row row, int colNum, BigDecimal value, CellStyle style) {
|
||||||
|
Cell cell = row.createCell(colNum);
|
||||||
|
if (value != null) {
|
||||||
|
cell.setCellValue(value.doubleValue());
|
||||||
|
} else {
|
||||||
|
cell.setCellValue(0.0);
|
||||||
|
}
|
||||||
|
cell.setCellStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 너비 자동 조정
|
||||||
|
*/
|
||||||
|
private void autoSizeColumns(Sheet sheet) {
|
||||||
|
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
|
||||||
|
sheet.autoSizeColumn(i);
|
||||||
|
// 한글 문자 고려하여 약간 여유 공간 추가
|
||||||
|
sheet.setColumnWidth(i, sheet.getColumnWidth(i) + 512);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP 응답 설정 및 파일 전송
|
||||||
|
*/
|
||||||
|
private void setHttpResponse(HttpServletResponse response, String fileName, Workbook workbook)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
// Content-Type 설정
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
|
||||||
|
// Content-Disposition 설정 (파일명 UTF-8 인코딩)
|
||||||
|
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString())
|
||||||
|
.replaceAll("\\+", "%20");
|
||||||
|
response.setHeader("Content-Disposition",
|
||||||
|
"attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
|
||||||
|
|
||||||
|
// Workbook을 응답 스트림으로 전송
|
||||||
|
workbook.write(response.getOutputStream());
|
||||||
|
response.getOutputStream().flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.statistics;
|
||||||
|
|
||||||
|
import com.eactive.eai.data.jpa.BaseRepository;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 사용현황 Repository
|
||||||
|
*/
|
||||||
|
interface ApiUseStatsRepository extends BaseRepository<ApiStatsDay, ApiStatsDayId> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.statistics;
|
||||||
|
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
import javax.persistence.PersistenceContext;
|
||||||
|
import javax.persistence.Query;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.eactive.eai.data.jpa.AbstractDataService;
|
||||||
|
import com.eactive.eai.rms.common.util.StringUtils;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayId;
|
||||||
|
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
|
||||||
|
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class ApiUseStatsService
|
||||||
|
extends AbstractDataService<ApiStatsDay, ApiStatsDayId, ApiUseStatsRepository> {
|
||||||
|
|
||||||
|
private static final int MAX_DAYS = 31;
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
private EntityManager entityManager;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Page<ApiStatsUI> selectList(ApiStatsSearch search, Pageable pageable) {
|
||||||
|
Query dataQuery = getDataQuery(search);
|
||||||
|
dataQuery.setFirstResult((int) pageable.getOffset());
|
||||||
|
dataQuery.setMaxResults(pageable.getPageSize());
|
||||||
|
|
||||||
|
List<Object[]> rows = dataQuery.getResultList();
|
||||||
|
List<ApiStatsUI> results = rows.stream()
|
||||||
|
.map(this::toVO)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
long records = rows.size() > 0 ? StringUtils.toLong(rows.get(0)[0]) : 0;
|
||||||
|
|
||||||
|
return new PageImpl<>(results, pageable, records);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<ApiStatsUI> selectList(ApiStatsSearch search) {
|
||||||
|
Query dataQuery = getDataQuery(search);
|
||||||
|
List<Object[]> rows = dataQuery.getResultList();
|
||||||
|
return rows.stream()
|
||||||
|
.map(this::toVO)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Query getDataQuery(ApiStatsSearch search) {
|
||||||
|
|
||||||
|
String schemaEms = System.getProperty("eai.tableowner", "EMSADM").toUpperCase();
|
||||||
|
|
||||||
|
StringBuilder where = buildNativeWhere(search);
|
||||||
|
|
||||||
|
String dataSql = "";
|
||||||
|
if ("ORG".equals(search.getSearchType())) {
|
||||||
|
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
|
||||||
|
+ ", B.ORGNAME"
|
||||||
|
+ ", SUM(A.TOTAL_CNT)"
|
||||||
|
+ ", SUM(A.SUCCESS_CNT)"
|
||||||
|
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
|
||||||
|
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
|
||||||
|
+ ", SUM(A.TIMEOUT_CNT)"
|
||||||
|
+ ", SUM(A.SYSTEM_ERR_CNT)"
|
||||||
|
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
|
||||||
|
+ ", MIN(A.MIN_RESP_TIME)"
|
||||||
|
+ ", MAX(A.MAX_RESP_TIME)"
|
||||||
|
+ " FROM API_STATS_DAY A"
|
||||||
|
+ " LEFT OUTER JOIN " + schemaEms + ".PTL_CREDENTIAL B ON A.CLIENT_ID = B.CLIENTID"
|
||||||
|
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
|
||||||
|
+ where
|
||||||
|
+ " GROUP BY B.ORGNAME"
|
||||||
|
+ " ORDER BY ORGNAME";
|
||||||
|
} else if ("API".equals(search.getSearchType())) {
|
||||||
|
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
|
||||||
|
+ ", C.EAISVCDESC"
|
||||||
|
+ ", SUM(A.TOTAL_CNT)"
|
||||||
|
+ ", SUM(A.SUCCESS_CNT)"
|
||||||
|
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
|
||||||
|
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
|
||||||
|
+ ", SUM(A.TIMEOUT_CNT)"
|
||||||
|
+ ", SUM(A.SYSTEM_ERR_CNT)"
|
||||||
|
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
|
||||||
|
+ ", MIN(A.MIN_RESP_TIME)"
|
||||||
|
+ ", MAX(A.MAX_RESP_TIME)"
|
||||||
|
+ " FROM API_STATS_DAY A "
|
||||||
|
+ " LEFT OUTER JOIN " + schemaEms + ".PTL_CREDENTIAL B ON A.CLIENT_ID = B.CLIENTID"
|
||||||
|
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
|
||||||
|
+ where
|
||||||
|
+ " GROUP BY C.EAISVCDESC"
|
||||||
|
+ " ORDER BY EAISVCDESC";
|
||||||
|
} else if ("DATE".equals(search.getSearchType())) {
|
||||||
|
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
|
||||||
|
+ ", TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME "
|
||||||
|
+ ", SUM(A.TOTAL_CNT)"
|
||||||
|
+ ", SUM(A.SUCCESS_CNT)"
|
||||||
|
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
|
||||||
|
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN (SUM(A.TOTAL_CNT) - SUM(A.SUCCESS_CNT)) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
|
||||||
|
+ ", SUM(A.TIMEOUT_CNT)"
|
||||||
|
+ ", SUM(A.SYSTEM_ERR_CNT)"
|
||||||
|
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN TRUNC(SUM(A.TOTAL_CNT * A.AVG_RESP_TIME) / SUM(A.TOTAL_CNT)) ELSE NULL END"
|
||||||
|
+ ", MIN(A.MIN_RESP_TIME)"
|
||||||
|
+ ", MAX(A.MAX_RESP_TIME)"
|
||||||
|
+ " FROM API_STATS_DAY A "
|
||||||
|
+ " LEFT OUTER JOIN " + schemaEms + ".PTL_CREDENTIAL B ON A.CLIENT_ID = B.CLIENTID"
|
||||||
|
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
|
||||||
|
+ where
|
||||||
|
+ " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')"
|
||||||
|
+ " ORDER BY STAT_TIME";
|
||||||
|
}
|
||||||
|
|
||||||
|
Query dataQuery = entityManager.createNativeQuery(dataSql);
|
||||||
|
if (search.getParsedStartDate() != null) {
|
||||||
|
dataQuery.setParameter("searchStartDate", search.getParsedStartDate());
|
||||||
|
dataQuery.setParameter("searchEndDate", search.getParsedEndDate());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
|
||||||
|
dataQuery.setParameter("searchOrgName", "%" + search.getSearchOrgName().toUpperCase() + "%");
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(search.getSearchApiName())) {
|
||||||
|
dataQuery.setParameter("searchApiName", "%" + search.getSearchApiName().toUpperCase() + "%");
|
||||||
|
}
|
||||||
|
return dataQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native Query 결과를 ApiStatsUI로 변환
|
||||||
|
*/
|
||||||
|
private ApiStatsUI toVO(Object[] row) {
|
||||||
|
ApiStatsUI vo = new ApiStatsUI();
|
||||||
|
vo.setOrgName(StringUtils.toString(row[1]));
|
||||||
|
vo.setTotalCnt(StringUtils.toLong(row[2]));
|
||||||
|
vo.setSuccessCnt(StringUtils.toLong(row[3]));
|
||||||
|
vo.setSuccessRate(StringUtils.toDecimal(row[4]).setScale(2, RoundingMode.HALF_UP));
|
||||||
|
vo.setFailRate(StringUtils.toDecimal(row[5]).setScale(2, RoundingMode.HALF_UP));
|
||||||
|
vo.setTimeoutCnt(StringUtils.toLong(row[6]));
|
||||||
|
vo.setSystemErrCnt(StringUtils.toLong(row[7]));
|
||||||
|
vo.setAvgRespTime(StringUtils.toDecimal(row[8]));
|
||||||
|
vo.setMinRespTime(StringUtils.toDecimal(row[9]));
|
||||||
|
vo.setMaxRespTime(StringUtils.toDecimal(row[10]));
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* native SQL용 동적 WHERE 절 생성
|
||||||
|
*/
|
||||||
|
private StringBuilder buildNativeWhere(ApiStatsSearch search) {
|
||||||
|
StringBuilder where = new StringBuilder(" WHERE 1=1");
|
||||||
|
|
||||||
|
calculateDateRange(search);
|
||||||
|
|
||||||
|
if (search.getParsedStartDate() != null) {
|
||||||
|
where.append(" AND A.STAT_TIME >= :searchStartDate ");
|
||||||
|
where.append(" AND A.STAT_TIME <= :searchEndDate ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
|
||||||
|
where.append(" AND UPPER(B.ORGNAME) LIKE :searchOrgName ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(search.getSearchApiName())) {
|
||||||
|
where.append(" AND UPPER(C.EAISVCDESC) LIKE :searchApiName ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return where;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조회 기간 계산 (최대 31일 제한)
|
||||||
|
*/
|
||||||
|
private void calculateDateRange(ApiStatsSearch search) {
|
||||||
|
if (StringUtils.isBlank(search.getSearchStartDateTime())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate startDate = LocalDate.parse(search.getSearchStartDateTime(),
|
||||||
|
DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||||
|
LocalDate endDate;
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
|
||||||
|
endDate = LocalDate.parse(search.getSearchEndDateTime(),
|
||||||
|
DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||||
|
if (java.time.Period.between(startDate, endDate).getDays() > MAX_DAYS) {
|
||||||
|
endDate = startDate.plusDays(MAX_DAYS);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
endDate = startDate.plusDays(MAX_DAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
search.setParsedStartDate(startDate);
|
||||||
|
search.setParsedEndDate(endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.eactive.eai.rms.data.ext.djb.webhook;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "PTL_WEBHOOK_SEND_LOG")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
public class WebhookSendLog {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "webhook_send_log_seq")
|
||||||
|
@SequenceGenerator(
|
||||||
|
name = "webhook_send_log_seq",
|
||||||
|
sequenceName = "SEQ_PTL_WEBHOOK_SEND_LOG",
|
||||||
|
allocationSize = 1
|
||||||
|
)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "TARGET_URL", nullable = false, length = 500)
|
||||||
|
private String targetUrl;
|
||||||
|
|
||||||
|
@Column(name = "EVENT_TYPE", length = 100)
|
||||||
|
private String eventType;
|
||||||
|
|
||||||
|
@Lob
|
||||||
|
@Column(name = "PAYLOAD")
|
||||||
|
private String payload;
|
||||||
|
|
||||||
|
@Column(name = "SIGNATURE", length = 500)
|
||||||
|
private String signature;
|
||||||
|
|
||||||
|
@Column(name = "STATUS_CODE")
|
||||||
|
private Integer statusCode;
|
||||||
|
|
||||||
|
@Lob
|
||||||
|
@Column(name = "RESPONSE_BODY")
|
||||||
|
private String responseBody;
|
||||||
|
|
||||||
|
@Column(name = "SUCCESS", length = 1)
|
||||||
|
private String success; // 오라클 CHAR(1) : 'Y' / 'N'
|
||||||
|
|
||||||
|
@Lob
|
||||||
|
@Column(name = "ERROR_MESSAGE")
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
@Column(name = "RETRY_COUNT")
|
||||||
|
private Integer retryCount = 0;
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "CREATED_AT", updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "SENT_AT")
|
||||||
|
private LocalDateTime sentAt;
|
||||||
|
|
||||||
|
/* Boolean 편의 메서드 */
|
||||||
|
public void setSuccess(Boolean success) {
|
||||||
|
this.success = (success != null && success) ? "Y" : "N";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getSuccess() {
|
||||||
|
return "Y".equalsIgnoreCase(this.success);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.async;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
public class AsyncConfig {
|
||||||
|
|
||||||
|
@Bean("asyncExecutor")
|
||||||
|
public Executor asyncExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(2);
|
||||||
|
executor.setMaxPoolSize(5);
|
||||||
|
executor.setQueueCapacity(20);
|
||||||
|
executor.setThreadNamePrefix("async-");
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.async;
|
||||||
|
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.ext.djb.event.ApiStatusChangedEvent;
|
||||||
|
import com.eactive.eai.rms.ext.djb.event.InflowTokenInsufficientEvent;
|
||||||
|
import com.eactive.eai.rms.ext.djb.swing.service.SwingSendManager;
|
||||||
|
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookSendManager;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AsyncEventListener {
|
||||||
|
|
||||||
|
private final WebhookSendManager webhookSendManager;
|
||||||
|
|
||||||
|
private final SwingSendManager swingSendManager;
|
||||||
|
|
||||||
|
@Async("asyncExecutor")
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
|
public void onApiStatusChanged(ApiStatusChangedEvent event) {
|
||||||
|
log.debug("API 상태 변경 이벤트 수신: {} {}", event.getEaisvcname(), event.getEvent());
|
||||||
|
webhookSendManager.send(event.getEaisvcname(), event.getEvent());
|
||||||
|
swingSendManager.send(null, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async("asyncExecutor")
|
||||||
|
@EventListener // 트랜잭션 write 없이 read만 하므로 TransactionalEventListener 불필요
|
||||||
|
public void onInflowTokenFail(InflowTokenInsufficientEvent event) {
|
||||||
|
log.debug("유량제어 토큰획득 실패 이벤트 수신: {} {}건", event.getEaisvcname(), event.getCnt());
|
||||||
|
swingSendManager.send(event.getEaisvcname(), event.getCnt(), event.getRangeMinute());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.event;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.data.ext.djb.apistatus.ApiStatusEvent;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ApiStatusChangedEvent {
|
||||||
|
private final String eaisvcname;
|
||||||
|
private final String eaisvcdesc;
|
||||||
|
private final String event;
|
||||||
|
|
||||||
|
public static ApiStatusChangedEvent from(ApiStatusEvent source) {
|
||||||
|
return new ApiStatusChangedEvent(source.getEaisvcname(), source.getEaisvcdesc(), source.getEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.event;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.data.ext.djb.inflow.InflowTokenInsufficient;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InflowTokenInsufficientEvent {
|
||||||
|
private final String eaisvcname;
|
||||||
|
private final String eaisvcdesc;
|
||||||
|
private final long cnt;
|
||||||
|
private final long rangeMinute;
|
||||||
|
|
||||||
|
public static InflowTokenInsufficientEvent from(InflowTokenInsufficient summary, long rangeMinute) {
|
||||||
|
return new InflowTokenInsufficientEvent(summary.getEaisvcname(), summary.getEaisvcdesc(), summary.getCnt(), rangeMinute);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.job;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
import javax.persistence.PersistenceContext;
|
||||||
|
|
||||||
|
import org.quartz.Job;
|
||||||
|
import org.quartz.JobExecutionContext;
|
||||||
|
import org.quartz.JobExecutionException;
|
||||||
|
import org.quartz.SchedulerException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
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.kjb.statistics.ApiStatsHourService;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsHour;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 로그 테이블을 시간별 통계로 집계하는 배치 작업
|
||||||
|
* TSEAILGXX → API_STATS_HOUR
|
||||||
|
* 실행 주기: 매시 00:10 (당일 데이터 집계)
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ApiStatsHourlyAggregationJob implements Job {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ApiStatsHourlyAggregationJob.class);
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
private EntityManager entityManager;
|
||||||
|
|
||||||
|
private ApiStatsHourService apiStatsHourService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setApiStatsHourService(ApiStatsHourService apiStatsHourService) {
|
||||||
|
this.apiStatsHourService = apiStatsHourService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||||
|
ApplicationContext appContext = null;
|
||||||
|
final ApiStatsHourlyAggregationJob selfJob;
|
||||||
|
try {
|
||||||
|
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
|
||||||
|
selfJob = appContext.getBean(ApiStatsHourlyAggregationJob.class);
|
||||||
|
} catch (SchedulerException e) {
|
||||||
|
log.error("applicationContext get module error", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataSourceType dataType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
|
||||||
|
DataSourceContextHolder.setDataSourceType(dataType);
|
||||||
|
try {
|
||||||
|
LocalDate targetDate = LocalDate.now();
|
||||||
|
if (LocalTime.now().isBefore(LocalTime.of(1, 0))) {
|
||||||
|
targetDate = targetDate.minusDays(1);
|
||||||
|
}
|
||||||
|
selfJob.executeManual(targetDate);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new JobExecutionException(e);
|
||||||
|
} finally {
|
||||||
|
DataSourceContextHolder.clearDataSourceType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수동 실행 메서드 (날짜 지정)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public int executeManual(LocalDate targetDate) {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
log.info("=== 시간별 통계 집계 작업 시작 === 대상: {}", targetDate);
|
||||||
|
|
||||||
|
String searchDate = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||||
|
String schemaEMS = System.getProperty("eai.tableowner", "EMSADM").toUpperCase();
|
||||||
|
String logTableName = CommonUtil.getLogTable(searchDate, true);
|
||||||
|
|
||||||
|
// 1. 기존 데이터 삭제
|
||||||
|
QApiStatsHour q = QApiStatsHour.apiStatsHour;
|
||||||
|
long deletedCount = apiStatsHourService.getJPAQueryFactory()
|
||||||
|
.delete(q)
|
||||||
|
.where(q.statTime.goe(targetDate.atStartOfDay())
|
||||||
|
.and(q.statTime.lt(targetDate.plusDays(1).atStartOfDay())))
|
||||||
|
.execute();
|
||||||
|
log.info("기존 데이터 삭제: {} 건", deletedCount);
|
||||||
|
|
||||||
|
// 2. 집계 및 데이터 입력
|
||||||
|
/**
|
||||||
|
* EAISVCSERNO로 그룹핑하여 1행으로 집계를 한 다음 (한거래 안에서 adapter가 여러개 있을 경우 seq=100의 adapter를 기준)
|
||||||
|
* 총건수 : EAISVCSERNO로 그룹핑한 총 건수
|
||||||
|
* 성공건수 : seq = 400 and EAIERRCD = null 인 건수
|
||||||
|
* Timeout : EAIERRCD in (공통코드 CODEGROUP = 'ERRCODE_TIMEOUT') 인 건수
|
||||||
|
* 시스템오류 : (seq400 = null or EAIERRCD is not null) and 공통코드(ERRCODE_TIMEOUT)에 정의되지 않은 errorcode 인 건수
|
||||||
|
*/
|
||||||
|
String sql =
|
||||||
|
"INSERT INTO API_STATS_HOUR" +
|
||||||
|
" (STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID" +
|
||||||
|
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
|
||||||
|
", TOTAL_CNT, SUCCESS_CNT, TIMEOUT_CNT, SYSTEM_ERR_CNT, BIZ_ERR_CNT" +
|
||||||
|
", SEQ900_TIMEOUT_CNT, SEQ900_SYSTEM_ERR_CNT, SEQ900_BIZ_ERR_CNT" +
|
||||||
|
", AVG_RESP_TIME, MIN_RESP_TIME, MAX_RESP_TIME, P50_RESP_TIME, P95_RESP_TIME)" +
|
||||||
|
" SELECT TO_TIMESTAMP(SUBSTR(DT,1,10) || '0000', 'YYYYMMDDHH24MISS')" +
|
||||||
|
" , API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
|
||||||
|
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
|
||||||
|
" , COUNT(EAISVCSERNO)" +
|
||||||
|
" , SUM(CASE WHEN A.ERROR_CODE IS NULL AND E400 IS NOT NULL THEN 1 ELSE 0 END)" +
|
||||||
|
" , SUM(CASE WHEN B.CODE IS NOT NULL THEN 1 ELSE 0 END)" +
|
||||||
|
" , SUM(CASE WHEN (A.E400 IS NULL OR A.ERROR_CODE IS NOT NULL) AND B.CODE IS NULL THEN 1 ELSE 0 END)" +
|
||||||
|
" , 0, 0, 0, 0" +
|
||||||
|
" , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" +
|
||||||
|
" FROM (" +
|
||||||
|
" SELECT EAISVCSERNO" +
|
||||||
|
" , MAX(EAISVCNAME) AS API_NAME, MAX(EAISEVRINSTNCNAME) AS GW_INSTANCE_ID" +
|
||||||
|
" , MAX(EAIBZWKDSTCD) AS BIZ_DIV_CODE, MAX(CLIENTID) AS CLIENT_ID" +
|
||||||
|
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN GSTATSYSADPTRBZWKGROUPNAME ELSE '' END) AS INBOUND_ADAPTER" +
|
||||||
|
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN PSVSYSADPTRBZWKGROUPNAME ELSE '' END) AS OUTBOUND_ADAPTER" +
|
||||||
|
" , MIN(MSGDPSTYMS) AS DT" +
|
||||||
|
" , MAX(MSGPRCSSYMS) - MIN(MSGDPSTYMS) AS RESP_TIME" +
|
||||||
|
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" +
|
||||||
|
" , MAX(EAIERRCD) AS ERROR_CODE" +
|
||||||
|
" FROM " + logTableName +
|
||||||
|
" WHERE MSGDPSTYMS LIKE :searchDate || '%'" +
|
||||||
|
" GROUP BY EAISVCSERNO" +
|
||||||
|
" ) A LEFT OUTER JOIN " + schemaEMS + ".TSEAIRM28 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT'" +
|
||||||
|
" GROUP BY SUBSTR(DT,1,10), API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
|
||||||
|
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER";
|
||||||
|
|
||||||
|
int savedCount = entityManager.createNativeQuery(sql)
|
||||||
|
.setParameter("searchDate", searchDate)
|
||||||
|
.executeUpdate();
|
||||||
|
|
||||||
|
long elapsedTime = System.currentTimeMillis() - startTime;
|
||||||
|
log.info("=== 집계 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime);
|
||||||
|
return savedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.job;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.quartz.DisallowConcurrentExecution;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
||||||
|
import com.eactive.eai.rms.common.util.CommonUtil;
|
||||||
|
import com.eactive.eai.rms.data.ext.djb.apistatus.ApiStatusService;
|
||||||
|
import com.eactive.eai.rms.onl.common.util.DateUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job - Quartz Job
|
||||||
|
* API_STATS_MINUTE 데이터를 1분마다 조회하여 Api 상태를 판단하여 API_STATUS 테이블을 insert/update 한다.
|
||||||
|
* & api 상태가 변경된 경우, 알림 테이블에 저장한다
|
||||||
|
*
|
||||||
|
* <p>Cron Schedule 권장:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>기본: 0 * * * * ? (매분 1회 실행)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Job 파라미터 (JobDataMap):</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>
|
||||||
|
* <b>api.status.error.range_minute</b>: API 장애기준 판단 시간간격 (단위: 분, 기본값: 1)
|
||||||
|
* <b>api.status.error.rate</b>: API 장애기준 오류건수 비율(%) (단위: 시간, 기본값: 100)
|
||||||
|
* <b>api.status.delay.range_minute</b>: API 지연기준 판단 시간간격 (단위: 분, 기본값: 1)
|
||||||
|
* <b>api.status.delay.avg_resp_time</b>: API 지연기준 평균응답시간 (단위: 밀리세컨드, 기본값: 10000)
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@DisallowConcurrentExecution
|
||||||
|
public class ApiStatusMonitorJob implements Job {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ApiStatusMonitorJob.class);
|
||||||
|
|
||||||
|
/** Job 파라미터 키: API 장애기준 시간간격(분) */
|
||||||
|
public static final String KEY_API_STATUS_ERROR_RANGE_MINUTE = "api.status.error.range_minute";
|
||||||
|
|
||||||
|
/** Job 파라미터 키: API 장애기준 오류건수 비율(%) */
|
||||||
|
public static final String KEY_API_STATUS_ERROR_RATE = "api.status.error.rate";
|
||||||
|
|
||||||
|
/** Job 파라미터 키: API 지연기준 시간간격(분) */
|
||||||
|
public static final String KEY_API_STATUS_DELAY_RANGE_MINUTE = "api.status.delay.range_minute";
|
||||||
|
|
||||||
|
/** Job 파라미터 키: API 지연기준 평균응답시간(ms) */
|
||||||
|
public static final String KEY_API_STATUS_DELAY_AVG_RESP_TIME = "api.status.delay.avg_resp_time";
|
||||||
|
|
||||||
|
public static final String DEFAULT_ERROR_RANGE_MINUTE = "1";
|
||||||
|
public static final String DEFAULT_ERROR_RATE = "100";
|
||||||
|
public static final String DEFAULT_DELAY_RANGE_MINUTE = "1";
|
||||||
|
public static final String DEFAULT_DELAY_AVG_RESP_TIME = "10000";
|
||||||
|
|
||||||
|
private transient MonitoringContext monitoringContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||||
|
log.info("*** START ApiStatusUpdateJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
||||||
|
|
||||||
|
// Job 파라미터 로깅
|
||||||
|
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
|
||||||
|
logJobParameters(jobDataMap);
|
||||||
|
|
||||||
|
HashMap<String, String> param = this.checkParameters(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");
|
||||||
|
ApiStatusService apiStatusUpdateService = appContext.getBean(ApiStatusService.class);
|
||||||
|
|
||||||
|
|
||||||
|
DataSourceContextHolder.setDataSourceType(
|
||||||
|
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
|
||||||
|
try {
|
||||||
|
apiStatusUpdateService.updateApiStatus(param);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("ApiStatusUpdateJob execution failed", e);
|
||||||
|
throw new JobExecutionException(e);
|
||||||
|
} finally {
|
||||||
|
DataSourceContextHolder.clearDataSourceType();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("*** END ApiStatusUpdateJob 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 HashMap<String,String> checkParameters(JobDataMap jobDataMap) {
|
||||||
|
|
||||||
|
HashMap<String, String> param = new HashMap<String, String>();
|
||||||
|
|
||||||
|
if (jobDataMap == null) {
|
||||||
|
param.put("errorRangeMinute", DEFAULT_ERROR_RANGE_MINUTE); //1분동안
|
||||||
|
param.put("errorRate", DEFAULT_ERROR_RATE); //에러가 100% 발생시 '장애'로 판단
|
||||||
|
param.put("delayRangeMinute", DEFAULT_DELAY_RANGE_MINUTE); //1분동안
|
||||||
|
param.put("delayAvgRespTime", DEFAULT_DELAY_AVG_RESP_TIME); //평균 응답속도가 10초 이상이면 '지연'으로 판단
|
||||||
|
return param;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_ERROR_RANGE_MINUTE))) {
|
||||||
|
param.put("errorRangeMinute", DEFAULT_ERROR_RANGE_MINUTE);
|
||||||
|
} else {
|
||||||
|
param.put("errorRangeMinute", jobDataMap.getString(KEY_API_STATUS_ERROR_RANGE_MINUTE));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_ERROR_RATE))) {
|
||||||
|
param.put("errorRate", DEFAULT_ERROR_RATE);
|
||||||
|
} else {
|
||||||
|
param.put("errorRate", jobDataMap.getString(KEY_API_STATUS_ERROR_RATE));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_DELAY_RANGE_MINUTE))) {
|
||||||
|
param.put("delayRangeMinute", DEFAULT_DELAY_RANGE_MINUTE);
|
||||||
|
} else {
|
||||||
|
param.put("delayRangeMinute", jobDataMap.getString(KEY_API_STATUS_DELAY_RANGE_MINUTE));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_DELAY_AVG_RESP_TIME))) {
|
||||||
|
param.put("delayAvgRespTime", DEFAULT_DELAY_AVG_RESP_TIME);
|
||||||
|
} else {
|
||||||
|
param.put("delayAvgRespTime", jobDataMap.getString(KEY_API_STATUS_DELAY_AVG_RESP_TIME));
|
||||||
|
}
|
||||||
|
|
||||||
|
return param;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개발 모드 여부 확인
|
||||||
|
* @return eai.systemmode=D 이면 true
|
||||||
|
*/
|
||||||
|
private boolean isDevMode() {
|
||||||
|
String systemMode = System.getProperty("eai.systemmode", "");
|
||||||
|
return "D".equalsIgnoreCase(systemMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.job;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.quartz.DisallowConcurrentExecution;
|
||||||
|
import org.quartz.Job;
|
||||||
|
import org.quartz.JobDataMap;
|
||||||
|
import org.quartz.JobExecutionContext;
|
||||||
|
import org.quartz.JobExecutionException;
|
||||||
|
import org.quartz.SchedulerException;
|
||||||
|
import org.quartz.Trigger;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
||||||
|
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
||||||
|
import com.eactive.eai.rms.common.util.CommonUtil;
|
||||||
|
import com.eactive.eai.rms.data.ext.djb.inflow.InflowTokenService;
|
||||||
|
import com.eactive.eai.rms.onl.common.util.DateUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job - Quartz Job
|
||||||
|
* TSEAIFR11 데이터를 10분마다 조회하여 데이터가 있는 경우(유량제어 토큰 획득 실패), 알림을 발송한다
|
||||||
|
*
|
||||||
|
* <p>Cron Schedule 권장:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>기본: 0 0/10 * * * ? (매10분 1회 실행)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Job 파라미터 (JobDataMap):</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>
|
||||||
|
* NONE
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
@DisallowConcurrentExecution
|
||||||
|
public class InflowTokenMonitorJob implements Job {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(InflowTokenMonitorJob.class);
|
||||||
|
|
||||||
|
private transient MonitoringContext monitoringContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||||
|
log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
||||||
|
|
||||||
|
long execMinute = 1; //default 배치 실행주기(분)
|
||||||
|
|
||||||
|
Trigger trigger = context.getTrigger();
|
||||||
|
Date prevFireTime = trigger.getPreviousFireTime();
|
||||||
|
Date nextFireTime = trigger.getNextFireTime();
|
||||||
|
|
||||||
|
//1회 이상 배치가 실행된 경우, 배치 간격을 구하여 유량제어조회 구간 파라미터로 전달한다
|
||||||
|
if (prevFireTime != null && nextFireTime != null) {
|
||||||
|
long diffMillis = nextFireTime.getTime() - prevFireTime.getTime();
|
||||||
|
execMinute = TimeUnit.MILLISECONDS.toMinutes(diffMillis);
|
||||||
|
log.info("실행 주기: {}분", execMinute);
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
InflowTokenService service = appContext.getBean(InflowTokenService.class);
|
||||||
|
|
||||||
|
DataSourceContextHolder.setDataSourceType(
|
||||||
|
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
|
||||||
|
try {
|
||||||
|
service.checkRecentFails(execMinute);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("InflowTokenFailMonitorJob execution failed", e);
|
||||||
|
throw new JobExecutionException(e);
|
||||||
|
} finally {
|
||||||
|
DataSourceContextHolder.clearDataSourceType();
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("*** END InflowTokenFailMonitorJob run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개발 모드 여부 확인
|
||||||
|
* @return eai.systemmode=D 이면 true
|
||||||
|
*/
|
||||||
|
private boolean isDevMode() {
|
||||||
|
String systemMode = System.getProperty("eai.systemmode", "");
|
||||||
|
return "D".equalsIgnoreCase(systemMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.statistics;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
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.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||||
|
import com.eactive.eai.rms.data.ext.djb.statistics.ApiUseStatsExcelExportService;
|
||||||
|
import com.eactive.eai.rms.data.ext.djb.statistics.ApiUseStatsService;
|
||||||
|
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
|
||||||
|
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Controller
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ApiUseStatsController {
|
||||||
|
|
||||||
|
private final ApiUseStatsService service;
|
||||||
|
private final ApiUseStatsExcelExportService excelExportService;
|
||||||
|
|
||||||
|
@GetMapping(value = "/onl/kjb/statistics/apiUseStatsMan.view")
|
||||||
|
public String view() {
|
||||||
|
return "/onl/kjb/statistics/apiUseStatsMan";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=LIST")
|
||||||
|
public ResponseEntity<GridResponse<ApiStatsUI>> selectList(ApiStatsSearch search, Pageable pageable) {
|
||||||
|
Page<ApiStatsUI> page = service.selectList(search, pageable);
|
||||||
|
return ResponseEntity.ok(new GridResponse<>(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=EXCEL_EXPORT")
|
||||||
|
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
|
||||||
|
log.info("Excel export started - search: {}", search);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<ApiStatsUI> uiList = service.selectList(search);
|
||||||
|
log.info("Data retrieved - count: {}", uiList.size());
|
||||||
|
|
||||||
|
if (uiList.isEmpty()) {
|
||||||
|
log.warn("No data found for export");
|
||||||
|
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||||
|
response.setContentType("application/json; charset=UTF-8");
|
||||||
|
response.getWriter().write("{\"message\":\"조회된 데이터가 없습니다.\"}");
|
||||||
|
response.getWriter().flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String fileName = generateFileName(search);
|
||||||
|
log.info("Generating Excel file: {}", fileName);
|
||||||
|
|
||||||
|
excelExportService.exportApiStats(uiList, fileName, response);
|
||||||
|
log.info("Excel export completed successfully");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Excel export failed - error: {}", e.getMessage(), e);
|
||||||
|
|
||||||
|
if (!response.isCommitted()) {
|
||||||
|
response.reset();
|
||||||
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
response.setContentType("application/json; charset=UTF-8");
|
||||||
|
response.getWriter().write("{\"message\":\"Excel 파일 생성 중 오류가 발생했습니다: " + e.getMessage() + "\"}");
|
||||||
|
response.getWriter().flush();
|
||||||
|
} else {
|
||||||
|
log.error("Cannot send error response - response already committed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateFileName(ApiStatsSearch search) {
|
||||||
|
return "API사용현황_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.swing.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SwingSendManager {
|
||||||
|
|
||||||
|
public void send(String eaisvcname, long cnt, long rangeMinute) {
|
||||||
|
log.info("[Swing] 발송 준비: {} 최근 {}분 {}건", eaisvcname, rangeMinute, cnt);
|
||||||
|
// TODO: 알림 발송 로직
|
||||||
|
}
|
||||||
|
}
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.webhook.controller;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
|
||||||
|
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
|
||||||
|
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookReceiveService;
|
||||||
|
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/webhook")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebhookSendController {
|
||||||
|
|
||||||
|
private final WebhookService webhookService;
|
||||||
|
private final WebhookReceiveService webhookReceiveService;
|
||||||
|
|
||||||
|
private static final int KEY_BYTE_LENGTH = 32; // 256bit
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 발송 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
@PostMapping("/send.json")
|
||||||
|
public ResponseEntity<Map<String, Object>> sendWebhook(
|
||||||
|
@RequestBody WebhookSendRequest request) {
|
||||||
|
|
||||||
|
WebhookSendLog result = webhookService.send(
|
||||||
|
request.getTargetUrl(),
|
||||||
|
request.getEventType(),
|
||||||
|
request.getData()
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("logId", result.getId());
|
||||||
|
response.put("success", result.getSuccess());
|
||||||
|
response.put("statusCode", result.getStatusCode());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 수신 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 웹훅 수신 엔드포인트
|
||||||
|
*
|
||||||
|
* 헤더 예시:
|
||||||
|
* X-Webhook-Signature : sha256={hmac값}
|
||||||
|
* X-Webhook-Event : ORDER_CREATED
|
||||||
|
* X-Webhook-Timestamp : 1712345678901
|
||||||
|
*/
|
||||||
|
@PostMapping("/receive")
|
||||||
|
public ResponseEntity<Map<String, Object>> receiveWebhook(
|
||||||
|
@RequestHeader(value = "X-Webhook-Signature", required = false) String signature,
|
||||||
|
@RequestHeader(value = "X-Webhook-Event", required = false) String eventType,
|
||||||
|
@RequestHeader(value = "X-Webhook-Timestamp", required = false) String timestamp,
|
||||||
|
@RequestBody String rawPayload) {
|
||||||
|
|
||||||
|
log.info("[Webhook] 수신 - eventType: {}, timestamp: {}", eventType, timestamp);
|
||||||
|
|
||||||
|
// 1. 필수 헤더 누락 체크
|
||||||
|
if (signature == null || eventType == null || timestamp == null) {
|
||||||
|
log.warn("[Webhook] 수신 거부 - 필수 헤더 누락");
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(errorResponse("필수 헤더가 누락되었습니다."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 서명 검증
|
||||||
|
boolean isValid = webhookReceiveService.verifySignature(rawPayload, signature);
|
||||||
|
if (!isValid) {
|
||||||
|
log.warn("[Webhook] 수신 거부 - 서명 불일치 / eventType: {}", eventType);
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
.body(errorResponse("서명 검증에 실패하였습니다."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 타임스탬프 유효성 검증 (5분 이내 요청만 허용)
|
||||||
|
boolean isTimestampValid = webhookReceiveService.verifyTimestamp(timestamp);
|
||||||
|
if (!isTimestampValid) {
|
||||||
|
log.warn("[Webhook] 수신 거부 - 타임스탬프 만료 / eventType: {}", eventType);
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
.body(errorResponse("요청이 만료되었습니다."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 이벤트 처리
|
||||||
|
try {
|
||||||
|
webhookReceiveService.process(eventType, rawPayload);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[Webhook] 이벤트 처리 실패 - eventType: {}, error: {}", eventType, e.getMessage(), e);
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(errorResponse("이벤트 처리 중 오류가 발생하였습니다."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 정상 응답
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("result", "OK");
|
||||||
|
response.put("eventType", eventType);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 공통 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
private Map<String, Object> errorResponse(String message) {
|
||||||
|
Map<String, Object> error = new HashMap<>();
|
||||||
|
error.put("result", "ERROR");
|
||||||
|
error.put("message", message);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.webhook.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class WebhookPayload {
|
||||||
|
|
||||||
|
private String eventType;
|
||||||
|
private String eventId;
|
||||||
|
private long timestamp;
|
||||||
|
private Object data;
|
||||||
|
|
||||||
|
public static WebhookPayload of(String eventType, Object data) {
|
||||||
|
return WebhookPayload.builder()
|
||||||
|
.eventType(eventType)
|
||||||
|
.eventId(UUID.randomUUID().toString())
|
||||||
|
.timestamp(System.currentTimeMillis())
|
||||||
|
.data(data)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.webhook.dto;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class WebhookSendRequest {
|
||||||
|
|
||||||
|
private String targetUrl;
|
||||||
|
private String eventType;
|
||||||
|
private Object data;
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.webhook.repository;
|
||||||
|
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.data.EMSDataSource;
|
||||||
|
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
@EMSDataSource
|
||||||
|
public interface WebhookSendLogRepository extends JpaRepository<WebhookSendLog, Long> {
|
||||||
|
|
||||||
|
// 오라클 CHAR(1) Y/N 기준 조회
|
||||||
|
@Query("SELECT l FROM WebhookSendLog l " +
|
||||||
|
"WHERE l.success = 'N' AND l.retryCount < :maxRetry")
|
||||||
|
List<WebhookSendLog> findFailedLogs(@Param("maxRetry") int maxRetry);
|
||||||
|
|
||||||
|
@Query("SELECT l FROM WebhookSendLog l " +
|
||||||
|
"WHERE l.eventType = :eventType " +
|
||||||
|
"AND l.createdAt BETWEEN :from AND :to")
|
||||||
|
List<WebhookSendLog> findByEventTypeAndPeriod(
|
||||||
|
@Param("eventType") String eventType,
|
||||||
|
@Param("from") LocalDateTime from,
|
||||||
|
@Param("to") LocalDateTime to);
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.webhook.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebhookReceiveService {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
|
||||||
|
private String secretKey = "HW8JtFpkPmQqsVmr0Rb81P4qDypaNIVbGf3ZNMMPfyerhOHshoKABLaMBo1HA4BldTxhw1NjYmVnoPu9IIV7cyRXjecL3b2UctyR7DnVaJausltZLLr8Qm4Bzs4wRmgf";
|
||||||
|
|
||||||
|
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||||
|
private static final long TIMESTAMP_LIMIT = 5 * 60 * 1000L; // 5분 (ms)
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 서명 검증 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수신된 서명과 payload 로 재계산한 서명을 비교
|
||||||
|
* 헤더값 형식 : "sha256={hex값}"
|
||||||
|
*/
|
||||||
|
public boolean verifySignature(String rawPayload, String receivedSignature) {
|
||||||
|
try {
|
||||||
|
// "sha256=" 접두어 제거
|
||||||
|
String receivedHex = receivedSignature.startsWith("sha256=")
|
||||||
|
? receivedSignature.substring(7)
|
||||||
|
: receivedSignature;
|
||||||
|
|
||||||
|
String expectedHex = generateSignature(rawPayload);
|
||||||
|
|
||||||
|
// 타이밍 공격 방지 : MessageDigest.isEqual 사용
|
||||||
|
return MessageDigest.isEqual(
|
||||||
|
expectedHex.getBytes(StandardCharsets.UTF_8),
|
||||||
|
receivedHex.getBytes(StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[Webhook] 서명 검증 중 오류 발생", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 타임스탬프 검증 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 요청 타임스탬프가 현재 시각 기준 5분 이내인지 확인 (Replay Attack 방지)
|
||||||
|
*/
|
||||||
|
public boolean verifyTimestamp(String timestampStr) {
|
||||||
|
try {
|
||||||
|
long requestTime = Long.parseLong(timestampStr);
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
return Math.abs(now - requestTime) <= TIMESTAMP_LIMIT;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("[Webhook] 타임스탬프 파싱 실패: {}", timestampStr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 이벤트 처리 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* eventType 에 따라 분기 처리
|
||||||
|
*/
|
||||||
|
public void process(String eventType, String rawPayload) throws Exception {
|
||||||
|
JsonNode root = objectMapper.readTree(rawPayload);
|
||||||
|
JsonNode data = root.path("data");
|
||||||
|
|
||||||
|
log.info("[Webhook] 이벤트 처리 시작 - eventType: {}", eventType);
|
||||||
|
|
||||||
|
switch (eventType) {
|
||||||
|
case "ORDER_CREATED":
|
||||||
|
handleOrderCreated(data);
|
||||||
|
break;
|
||||||
|
case "ORDER_CANCELLED":
|
||||||
|
handleOrderCancelled(data);
|
||||||
|
break;
|
||||||
|
case "PAYMENT_COMPLETED":
|
||||||
|
handlePaymentCompleted(data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warn("[Webhook] 처리되지 않은 이벤트 - eventType: {}", eventType);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[Webhook] 이벤트 처리 완료 - eventType: {}", eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 이벤트별 핸들러 (비즈니스 로직 구현) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
private void handleOrderCreated(JsonNode data) {
|
||||||
|
log.info("[Webhook] ORDER_CREATED 처리 - data: {}", data);
|
||||||
|
// TODO: 주문 생성 처리 로직
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleOrderCancelled(JsonNode data) {
|
||||||
|
log.info("[Webhook] ORDER_CANCELLED 처리 - data: {}", data);
|
||||||
|
// TODO: 주문 취소 처리 로직
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePaymentCompleted(JsonNode data) {
|
||||||
|
log.info("[Webhook] PAYMENT_COMPLETED 처리 - data: {}", data);
|
||||||
|
// TODO: 결제 완료 처리 로직
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 공통 유틸 */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
private String generateSignature(String payload) throws Exception {
|
||||||
|
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(
|
||||||
|
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
|
||||||
|
mac.init(keySpec);
|
||||||
|
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return bytesToHex(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String bytesToHex(byte[] bytes) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : bytes) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.webhook.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebhookSendManager {
|
||||||
|
|
||||||
|
public void send(String eaisvcname, String eventType) {
|
||||||
|
log.info("[Webhook] 발송 준비: {} {}", eaisvcname, eventType);
|
||||||
|
// TODO: PTL_WEBHOOK_REQ 조인 조회 후 WebhookService.send() 호출
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.eactive.eai.rms.ext.djb.webhook.service;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.client.HttpClientErrorException;
|
||||||
|
import org.springframework.web.client.HttpServerErrorException;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
|
||||||
|
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookPayload;
|
||||||
|
import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebhookService {
|
||||||
|
|
||||||
|
private final WebhookSendLogRepository sendLogRepository;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
|
|
||||||
|
private String secretKey = "HW8JtFpkPmQqsVmr0Rb81P4qDypaNIVbGf3ZNMMPfyerhOHshoKABLaMBo1HA4BldTxhw1NjYmVnoPu9IIV7cyRXjecL3b2UctyR7DnVaJausltZLLr8Qm4Bzs4wRmgf";
|
||||||
|
|
||||||
|
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||||
|
private static final int MAX_RETRY = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 웹훅 발송 메인 메서드
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public WebhookSendLog send(String targetUrl, String eventType, Object data) {
|
||||||
|
WebhookSendLog sendLog = new WebhookSendLog();
|
||||||
|
sendLog.setTargetUrl(targetUrl);
|
||||||
|
sendLog.setEventType(eventType);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Payload 생성
|
||||||
|
WebhookPayload webhookPayload = WebhookPayload.of(eventType, data);
|
||||||
|
String payloadJson = objectMapper.writeValueAsString(webhookPayload);
|
||||||
|
sendLog.setPayload(payloadJson);
|
||||||
|
|
||||||
|
// 2. Secret Key로 HMAC-SHA256 서명 생성
|
||||||
|
String signature = generateSignature(payloadJson);
|
||||||
|
sendLog.setSignature(signature);
|
||||||
|
|
||||||
|
// 3. HTTP 요청 헤더 구성
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.set("X-Webhook-Signature", "sha256=" + signature);
|
||||||
|
headers.set("X-Webhook-Event", eventType);
|
||||||
|
headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp()));
|
||||||
|
|
||||||
|
// 4. 발송
|
||||||
|
sendLog.setSentAt(LocalDateTime.now());
|
||||||
|
HttpEntity<String> request = new HttpEntity<>(payloadJson, headers);
|
||||||
|
ResponseEntity<String> response = restTemplate.postForEntity(targetUrl, request, String.class);
|
||||||
|
|
||||||
|
// 5. 성공 로그
|
||||||
|
sendLog.setStatusCode(response.getStatusCode().value());
|
||||||
|
sendLog.setResponseBody(response.getBody());
|
||||||
|
sendLog.setSuccess(response.getStatusCode().is2xxSuccessful());
|
||||||
|
|
||||||
|
log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}",
|
||||||
|
eventType, targetUrl, response.getStatusCode());
|
||||||
|
|
||||||
|
} catch (HttpClientErrorException | HttpServerErrorException e) {
|
||||||
|
sendLog.setStatusCode(e.getStatusCode().value());
|
||||||
|
sendLog.setResponseBody(e.getResponseBodyAsString());
|
||||||
|
sendLog.setSuccess(false);
|
||||||
|
sendLog.setErrorMessage(e.getMessage());
|
||||||
|
log.error("[Webhook] HTTP 오류 - eventType: {}, url: {}, status: {}",
|
||||||
|
eventType, targetUrl, e.getStatusCode());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
sendLog.setSuccess(false);
|
||||||
|
sendLog.setErrorMessage(e.getMessage());
|
||||||
|
log.error("[Webhook] 발송 실패 - eventType: {}, url: {}, error: {}",
|
||||||
|
eventType, targetUrl, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendLogRepository.save(sendLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실패 건 재시도
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void retryFailedWebhooks() {
|
||||||
|
List<WebhookSendLog> failedLogs =
|
||||||
|
sendLogRepository.findFailedLogs(MAX_RETRY);
|
||||||
|
|
||||||
|
for (WebhookSendLog failedLog : failedLogs) {
|
||||||
|
try {
|
||||||
|
log.info("[Webhook] 재시도 - id: {}, retry: {}", failedLog.getId(), failedLog.getRetryCount());
|
||||||
|
failedLog.setRetryCount(failedLog.getRetryCount() + 1);
|
||||||
|
sendLogRepository.save(failedLog);
|
||||||
|
|
||||||
|
send(failedLog.getTargetUrl(), failedLog.getEventType(),
|
||||||
|
objectMapper.readValue(failedLog.getPayload(), Object.class));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[Webhook] 재시도 실패 - id: {}", failedLog.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HMAC-SHA256 서명 생성
|
||||||
|
* Java 17 미만은 HexFormat 미지원이므로 직접 변환
|
||||||
|
*/
|
||||||
|
private String generateSignature(String payload) throws Exception {
|
||||||
|
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(
|
||||||
|
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
|
||||||
|
mac.init(keySpec);
|
||||||
|
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return bytesToHex(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String bytesToHex(byte[] bytes) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : bytes) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ public class PortalApprovalAuthorizer implements ApprovalAuthorizer {
|
|||||||
return approval.getApprovers().stream()
|
return approval.getApprovers().stream()
|
||||||
.filter(this::isCurrentApprover)
|
.filter(this::isCurrentApprover)
|
||||||
.map(this::getApproverId)
|
.map(this::getApproverId)
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
.anyMatch(id -> id.equals(approverId));
|
.anyMatch(id -> id.equals(approverId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +23,6 @@ public class PortalApprovalAuthorizer implements ApprovalAuthorizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getApproverId(Approver approver) {
|
private String getApproverId(Approver approver) {
|
||||||
return approver.getUser().getId();
|
return approver.getUser() != null ? approver.getUser().getId() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-2
@@ -13,10 +13,16 @@ import org.springframework.data.domain.Sort;
|
|||||||
import org.springframework.data.web.SortDefault;
|
import org.springframework.data.web.SortDefault;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PortalApprovalManController extends BaseAnnotationController {
|
public class PortalApprovalManController extends BaseAnnotationController {
|
||||||
@@ -24,8 +30,8 @@ public class PortalApprovalManController extends BaseAnnotationController {
|
|||||||
private final PortalApprovalManService portalApprovalManService;
|
private final PortalApprovalManService portalApprovalManService;
|
||||||
|
|
||||||
@GetMapping(value = "/onl/apim/approval/portalApprovalMan.view")
|
@GetMapping(value = "/onl/apim/approval/portalApprovalMan.view")
|
||||||
public void view() {
|
public void view(Model model) {
|
||||||
// view
|
model.addAttribute("hardDeleteEnabled", portalApprovalManService.isHardDeleteEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/onl/apim/approval/portalApprovalMan.view", params = "cmd=DETAIL")
|
@GetMapping(value = "/onl/apim/approval/portalApprovalMan.view", params = "cmd=DETAIL")
|
||||||
@@ -76,4 +82,46 @@ public class PortalApprovalManController extends BaseAnnotationController {
|
|||||||
portalApprovalManService.redeploy(id);
|
portalApprovalManService.redeploy(id);
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=HARD_DELETE_MULTIPLE")
|
||||||
|
public ResponseEntity<Map<String, Object>> hardDeleteMultiple(String ids) {
|
||||||
|
Map<String, Object> resultMap = new HashMap<>();
|
||||||
|
try {
|
||||||
|
if (StringUtils.isBlank(ids)) {
|
||||||
|
resultMap.put("status", "fail");
|
||||||
|
resultMap.put("message", "삭제할 승인 요청을 선택해주세요.");
|
||||||
|
return ResponseEntity.ok(resultMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] idArray = ids.split(",");
|
||||||
|
int successCount = 0;
|
||||||
|
List<String> failMessages = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String id : idArray) {
|
||||||
|
if (StringUtils.isNotBlank(id.trim())) {
|
||||||
|
try {
|
||||||
|
portalApprovalManService.hardDelete(id.trim());
|
||||||
|
successCount++;
|
||||||
|
} catch (BizException e) {
|
||||||
|
failMessages.add(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
failMessages.add(id.trim() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failMessages.isEmpty()) {
|
||||||
|
resultMap.put("status", "success");
|
||||||
|
resultMap.put("message", successCount + "건의 승인 요청이 완전삭제되었습니다.");
|
||||||
|
} else {
|
||||||
|
resultMap.put("status", "fail");
|
||||||
|
resultMap.put("message", "완전삭제 실패: " + String.join(", ", failMessages)
|
||||||
|
+ (successCount > 0 ? " (" + successCount + "건 성공)" : ""));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
resultMap.put("status", "fail");
|
||||||
|
resultMap.put("message", "완전삭제 중 오류가 발생했습니다: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(resultMap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
|||||||
import com.eactive.apim.portal.approval.entity.Approval;
|
import com.eactive.apim.portal.approval.entity.Approval;
|
||||||
import com.eactive.apim.portal.approval.entity.ApprovalType;
|
import com.eactive.apim.portal.approval.entity.ApprovalType;
|
||||||
import com.eactive.apim.portal.approval.statemachine.*;
|
import com.eactive.apim.portal.approval.statemachine.*;
|
||||||
|
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
||||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
|
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
|
||||||
@@ -25,6 +26,7 @@ import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgManService;
|
|||||||
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserManService;
|
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserManService;
|
||||||
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI;
|
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI;
|
||||||
import com.eactive.eai.rms.common.acl.user.ui.UserUI;
|
import com.eactive.eai.rms.common.acl.user.ui.UserUI;
|
||||||
|
import com.eactive.eai.rms.onl.common.exception.BizException;
|
||||||
import com.eactive.eai.rms.onl.transaction.apim.ApiInterfaceService;
|
import com.eactive.eai.rms.onl.transaction.apim.ApiInterfaceService;
|
||||||
import com.eactive.eai.rms.onl.transaction.apim.mapping.ApiSpecUIMapper;
|
import com.eactive.eai.rms.onl.transaction.apim.mapping.ApiSpecUIMapper;
|
||||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI;
|
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI;
|
||||||
@@ -36,6 +38,8 @@ import org.springframework.data.domain.Pageable;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
import javax.persistence.PersistenceContext;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -43,6 +47,9 @@ import java.util.*;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PortalApprovalManService extends BaseService {
|
public class PortalApprovalManService extends BaseService {
|
||||||
|
|
||||||
|
private static final String PORTAL_PROPERTY_GROUP = "Portal";
|
||||||
|
private static final String HARD_DELETE_FLAG_KEY = "approval.hard-delete.enabled";
|
||||||
|
|
||||||
private final ApprovalService approvalService;
|
private final ApprovalService approvalService;
|
||||||
private final ApprovalStateMachine approvalStateMachine;
|
private final ApprovalStateMachine approvalStateMachine;
|
||||||
private final AppRequestRepository appRequestRepository;
|
private final AppRequestRepository appRequestRepository;
|
||||||
@@ -59,6 +66,10 @@ public class PortalApprovalManService extends BaseService {
|
|||||||
private final ApiGroupUIMapper apiGroupUIMapper;
|
private final ApiGroupUIMapper apiGroupUIMapper;
|
||||||
private final PortalApprovalAuthorizer portalApprovalAuthorizer;
|
private final PortalApprovalAuthorizer portalApprovalAuthorizer;
|
||||||
private final PortalOrgManService portalOrgManService;
|
private final PortalOrgManService portalOrgManService;
|
||||||
|
private final PortalPropertyService portalPropertyService;
|
||||||
|
|
||||||
|
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
|
||||||
|
private EntityManager entityManager;
|
||||||
|
|
||||||
public Page<PortalApprovalUI> selectList(Pageable pageable, PortalApprovalUISearch portalApprovalUISearch) {
|
public Page<PortalApprovalUI> selectList(Pageable pageable, PortalApprovalUISearch portalApprovalUISearch) {
|
||||||
Page<Approval> approval = approvalService.findAll(pageable, portalApprovalUISearch);
|
Page<Approval> approval = approvalService.findAll(pageable, portalApprovalUISearch);
|
||||||
@@ -330,6 +341,28 @@ public class PortalApprovalManService extends BaseService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isHardDeleteEnabled() {
|
||||||
|
Map<String, String> properties = portalPropertyService.getPortalPropertiesAsMap(PORTAL_PROPERTY_GROUP);
|
||||||
|
return Boolean.parseBoolean(properties.getOrDefault(HARD_DELETE_FLAG_KEY, "false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hardDelete(String id) {
|
||||||
|
if (!isHardDeleteEnabled()) {
|
||||||
|
throw new BizException("승인 요청 완전삭제 기능이 비활성화되어 있습니다. (PTL_PROPERTY: " + HARD_DELETE_FLAG_KEY + ")");
|
||||||
|
}
|
||||||
|
// Approval.approvers 가 EAGER + Approver.user 가 NotFound 처리 없음 → 사라진 UserInfo 로 인해
|
||||||
|
// findById/deleteById 가 hydration 단계에서 실패하는 row 가 존재. 네이티브 SQL 로 우회.
|
||||||
|
entityManager.createNativeQuery("DELETE FROM PTL_APPROVER WHERE APPROVAL_ID = :id")
|
||||||
|
.setParameter("id", id)
|
||||||
|
.executeUpdate();
|
||||||
|
int affected = entityManager.createNativeQuery("DELETE FROM PTL_APPROVAL WHERE ID = :id")
|
||||||
|
.setParameter("id", id)
|
||||||
|
.executeUpdate();
|
||||||
|
if (affected == 0) {
|
||||||
|
throw new BizException("승인 정보를 찾을 수 없습니다. ID: " + id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void sendEvent(String id, ApprovalEvent event, Map<String, Object> options) {
|
public void sendEvent(String id, ApprovalEvent event, Map<String, Object> options) {
|
||||||
approvalService.findById(id).ifPresent(approval -> {
|
approvalService.findById(id).ifPresent(approval -> {
|
||||||
options.put("authorizer", portalApprovalAuthorizer);
|
options.put("authorizer", portalApprovalAuthorizer);
|
||||||
|
|||||||
+10
-17
@@ -6,7 +6,6 @@ import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
|
|||||||
import com.eactive.apim.portal.approval.entity.Approval;
|
import com.eactive.apim.portal.approval.entity.Approval;
|
||||||
import com.eactive.apim.portal.approval.service.ApprovalDeployException;
|
import com.eactive.apim.portal.approval.service.ApprovalDeployException;
|
||||||
import com.eactive.apim.portal.approval.statemachine.listener.ApprovalListener;
|
import com.eactive.apim.portal.approval.statemachine.listener.ApprovalListener;
|
||||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
|
||||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||||
import com.eactive.apim.portal.template.service.MessageRecipient;
|
import com.eactive.apim.portal.template.service.MessageRecipient;
|
||||||
import com.eactive.apim.portal.template.service.MessageSendService;
|
import com.eactive.apim.portal.template.service.MessageSendService;
|
||||||
@@ -19,10 +18,8 @@ import com.eactive.eai.rms.onl.apim.approval.RandomStringGenerator;
|
|||||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialManService;
|
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialManService;
|
||||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUI;
|
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUI;
|
||||||
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUIMapper;
|
import com.eactive.eai.rms.onl.apim.approval.credential.CredentialUIMapper;
|
||||||
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI;
|
|
||||||
import com.eactive.eai.rms.onl.common.service.AgentUtilService;
|
import com.eactive.eai.rms.onl.common.service.AgentUtilService;
|
||||||
import com.eactive.eai.rms.onl.manage.authserver.client.ClientManService;
|
import com.eactive.eai.rms.onl.manage.authserver.client.ClientManService;
|
||||||
import com.eactive.eai.rms.onl.manage.authserver.client.ClientUI;
|
|
||||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -35,22 +32,12 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.apache.http.client.HttpClient;
|
|
||||||
import org.apache.http.client.config.RequestConfig;
|
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.http.HttpEntity;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -116,9 +103,15 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void sendApprovalResult(AppRequest appRequest) {
|
private void sendApprovalResult(AppRequest appRequest) {
|
||||||
|
com.eactive.apim.portal.portaluser.entity.PortalUser requester =
|
||||||
|
appRequest.getApproval() != null ? appRequest.getApproval().getRequester() : null;
|
||||||
|
if (requester == null) {
|
||||||
|
logger.warn("승인 결과 통지 생략 - 요청자 정보 없음. appRequestId: {}", appRequest.getId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
MessageRecipient recipient = new MessageRecipient();
|
MessageRecipient recipient = new MessageRecipient();
|
||||||
recipient.setPhone(appRequest.getApproval().getRequester().getMobileNumber());
|
recipient.setPhone(requester.getMobileNumber());
|
||||||
recipient.setUserId(appRequest.getApproval().getRequester().getEmailAddr());
|
recipient.setUserId(requester.getEmailAddr());
|
||||||
Map<String, String> params = new HashMap<>();
|
Map<String, String> params = new HashMap<>();
|
||||||
params.put("apiKey", appRequest.getClientName());
|
params.put("apiKey", appRequest.getClientName());
|
||||||
messageSendService.sendMessage(MessageCode.APP_REGISTER_APPROVED, recipient, params);
|
messageSendService.sendMessage(MessageCode.APP_REGISTER_APPROVED, recipient, params);
|
||||||
@@ -165,8 +158,8 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
|||||||
credentialUI.setClientname(appRequest.getClientName());
|
credentialUI.setClientname(appRequest.getClientName());
|
||||||
credentialUI.setScope("api");
|
credentialUI.setScope("api");
|
||||||
credentialUI.setGranttypes("client_credentials");
|
credentialUI.setGranttypes("client_credentials");
|
||||||
credentialUI.setOrgid(appRequest.getOrg().getId());
|
credentialUI.setOrgid(appRequest.getOrg() != null ? appRequest.getOrg().getId() : null);
|
||||||
credentialUI.setOrgname(appRequest.getOrg().getOrgName());
|
credentialUI.setOrgname(appRequest.getOrg() != null ? appRequest.getOrg().getOrgName() : null);
|
||||||
credentialUI.setAllowedips(appRequest.getIpWhitelist());
|
credentialUI.setAllowedips(appRequest.getIpWhitelist());
|
||||||
credentialUI.setRedirecturi(appRequest.getCallbackUrl());
|
credentialUI.setRedirecturi(appRequest.getCallbackUrl());
|
||||||
credentialUI.setAppIconFileId(appRequest.getAppIconFileId());
|
credentialUI.setAppIconFileId(appRequest.getAppIconFileId());
|
||||||
|
|||||||
+14
@@ -1,6 +1,8 @@
|
|||||||
package com.eactive.eai.rms.onl.apim.portalnotice;
|
package com.eactive.eai.rms.onl.apim.portalnotice;
|
||||||
|
|
||||||
import com.eactive.eai.rms.common.base.BaseAnnotationController;
|
import com.eactive.eai.rms.common.base.BaseAnnotationController;
|
||||||
|
import com.eactive.eai.rms.common.combo.ComboService;
|
||||||
|
import com.eactive.eai.rms.common.combo.ComboVo;
|
||||||
import com.eactive.eai.rms.common.login.SessionManager;
|
import com.eactive.eai.rms.common.login.SessionManager;
|
||||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||||
import com.eactive.eai.rms.data.entity.onl.apim.portalnotice.PortalNoticeUISearch;
|
import com.eactive.eai.rms.data.entity.onl.apim.portalnotice.PortalNoticeUISearch;
|
||||||
@@ -16,12 +18,16 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PortalNoticeManController extends BaseAnnotationController {
|
public class PortalNoticeManController extends BaseAnnotationController {
|
||||||
|
|
||||||
private final PortalNoticeManService portalNoticeManService;
|
private final PortalNoticeManService portalNoticeManService;
|
||||||
|
private final ComboService comboService;
|
||||||
|
|
||||||
@GetMapping(value = "/onl/apim/portalnotice/portalNoticeMan.view")
|
@GetMapping(value = "/onl/apim/portalnotice/portalNoticeMan.view")
|
||||||
public void view() {
|
public void view() {
|
||||||
@@ -41,6 +47,14 @@ public class PortalNoticeManController extends BaseAnnotationController {
|
|||||||
return ResponseEntity.ok(new GridResponse<>(page));
|
return ResponseEntity.ok(new GridResponse<>(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=LIST_INIT_COMBO")
|
||||||
|
public ResponseEntity<Map<String, Object>> initCombo() {
|
||||||
|
List<ComboVo> noticeTypeList = comboService.getMonitoringCodeSortedBySeq("NOTICE_TYPE");
|
||||||
|
Map<String, Object> resultMap = new HashMap<>();
|
||||||
|
resultMap.put("noticeTypeList", noticeTypeList);
|
||||||
|
return ResponseEntity.ok(resultMap);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=DETAIL")
|
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=DETAIL")
|
||||||
public ResponseEntity<PortalNoticeUI> selectDetail(String id) {
|
public ResponseEntity<PortalNoticeUI> selectDetail(String id) {
|
||||||
PortalNoticeUI PortalNoticeUI = portalNoticeManService.selectDetail(id);
|
PortalNoticeUI PortalNoticeUI = portalNoticeManService.selectDetail(id);
|
||||||
|
|||||||
@@ -93,8 +93,9 @@ public class PortalNoticeManService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void insert(PortalNoticeUI portalNoticeUI) throws IOException {
|
public void insert(PortalNoticeUI portalNoticeUI) throws IOException {
|
||||||
portalNoticeUI.setNoticeSubject(decodeString(portalNoticeUI.getNoticeSubject()));
|
//portalNoticeUI.setNoticeSubject(decodeString(portalNoticeUI.getNoticeSubject()));
|
||||||
portalNoticeUI.setNoticeDetail(decodeString(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail())));
|
//portalNoticeUI.setNoticeDetail(decodeString(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail())));
|
||||||
|
portalNoticeUI.setNoticeDetail(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail()));
|
||||||
|
|
||||||
PortalNotice portalNotice = portalNoticeUIMapper.toEntity(portalNoticeUI);
|
PortalNotice portalNotice = portalNoticeUIMapper.toEntity(portalNoticeUI);
|
||||||
portalNotice.setId(null);
|
portalNotice.setId(null);
|
||||||
@@ -123,8 +124,9 @@ public class PortalNoticeManService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void update(PortalNoticeUI portalNoticeUI) throws IOException {
|
public void update(PortalNoticeUI portalNoticeUI) throws IOException {
|
||||||
portalNoticeUI.setNoticeSubject(decodeString(portalNoticeUI.getNoticeSubject()));
|
//portalNoticeUI.setNoticeSubject(decodeString(portalNoticeUI.getNoticeSubject()));
|
||||||
portalNoticeUI.setNoticeDetail(decodeString(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail())));
|
//portalNoticeUI.setNoticeDetail(decodeString(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail())));
|
||||||
|
portalNoticeUI.setNoticeDetail(StringEscapeUtils.unescapeHtml(portalNoticeUI.getNoticeDetail()));
|
||||||
|
|
||||||
PortalNotice portalNotice = portalNoticeService.getById(portalNoticeUI.getId());
|
PortalNotice portalNotice = portalNoticeService.getById(portalNoticeUI.getId());
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ public class PortalNoticeUI {
|
|||||||
|
|
||||||
private String inquirerName;
|
private String inquirerName;
|
||||||
|
|
||||||
|
private String fixYn;
|
||||||
|
|
||||||
|
private String noticeType;
|
||||||
|
|
||||||
|
|
||||||
private String createdBy;
|
private String createdBy;
|
||||||
|
|
||||||
@JsonFormat(pattern = LocalDateTimeFormatters.FORMAT_YYYYMMDDHHMMSS_14)
|
@JsonFormat(pattern = LocalDateTimeFormatters.FORMAT_YYYYMMDDHHMMSS_14)
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.persistence.EntityManager;
|
||||||
|
import javax.persistence.PersistenceContext;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -48,6 +50,9 @@ public class PortalOrgManService extends BaseService {
|
|||||||
private final PortalUserCorpManagerUIMapper portalUserCorpManagerUIMapper;
|
private final PortalUserCorpManagerUIMapper portalUserCorpManagerUIMapper;
|
||||||
private final FileService fileService;
|
private final FileService fileService;
|
||||||
|
|
||||||
|
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
|
||||||
|
private EntityManager entityManager;
|
||||||
|
|
||||||
public Page<PortalOrgUI> selectList(Pageable pageable, PortalOrgUISearch portalOrgUISearch) {
|
public Page<PortalOrgUI> selectList(Pageable pageable, PortalOrgUISearch portalOrgUISearch) {
|
||||||
Page<PortalOrg> portalOrg = portalOrgService.findAll(pageable, portalOrgUISearch);
|
Page<PortalOrg> portalOrg = portalOrgService.findAll(pageable, portalOrgUISearch);
|
||||||
return portalOrg.map(entity -> convertToUIWithMaskOption(entity, true));
|
return portalOrg.map(entity -> convertToUIWithMaskOption(entity, true));
|
||||||
@@ -146,19 +151,37 @@ public class PortalOrgManService extends BaseService {
|
|||||||
throw new BizException("삭제(탈퇴) 상태인 법인만 완전삭제할 수 있습니다.");
|
throw new BizException("삭제(탈퇴) 상태인 법인만 완전삭제할 수 있습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> dependencies = new ArrayList<>();
|
// 사용자(PortalUser)는 별도 화면에서 정리해야 하므로 cascade 하지 않고 차단
|
||||||
long userCount = portalUserService.countByOrgId(id);
|
long userCount = portalUserService.countByOrgId(id);
|
||||||
if (userCount > 0) dependencies.add("사용자 " + userCount + "건");
|
if (userCount > 0) {
|
||||||
long appRequestCount = portalOrgService.countAppRequestByOrgId(id);
|
throw new BizException("의존 데이터가 존재하여 완전삭제할 수 없습니다: 사용자 " + userCount + "건");
|
||||||
if (appRequestCount > 0) dependencies.add("앱 신청 " + appRequestCount + "건");
|
|
||||||
|
|
||||||
if (!dependencies.isEmpty()) {
|
|
||||||
throw new BizException("의존 데이터가 존재하여 완전삭제할 수 없습니다: " + String.join(", ", dependencies));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 앱 신청(AppRequest) + 연결된 승인(Approval/Approver) 은 cascade 삭제
|
||||||
|
cascadeDeleteAppRequestsByOrg(id);
|
||||||
|
|
||||||
portalOrgService.deleteById(id);
|
portalOrgService.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cascadeDeleteAppRequestsByOrg(String orgId) {
|
||||||
|
// 1) 해당 org 의 app_request 가 참조하는 approval 의 approver 제거
|
||||||
|
entityManager.createNativeQuery(
|
||||||
|
"DELETE FROM PTL_APPROVER WHERE APPROVAL_ID IN " +
|
||||||
|
"(SELECT APPROVAL_ID FROM PTL_APP_REQUEST WHERE ORG_ID = :orgId AND APPROVAL_ID IS NOT NULL)"
|
||||||
|
).setParameter("orgId", orgId).executeUpdate();
|
||||||
|
|
||||||
|
// 2) approval 제거
|
||||||
|
entityManager.createNativeQuery(
|
||||||
|
"DELETE FROM PTL_APPROVAL WHERE ID IN " +
|
||||||
|
"(SELECT APPROVAL_ID FROM PTL_APP_REQUEST WHERE ORG_ID = :orgId AND APPROVAL_ID IS NOT NULL)"
|
||||||
|
).setParameter("orgId", orgId).executeUpdate();
|
||||||
|
|
||||||
|
// 3) app_request 제거
|
||||||
|
entityManager.createNativeQuery(
|
||||||
|
"DELETE FROM PTL_APP_REQUEST WHERE ORG_ID = :orgId"
|
||||||
|
).setParameter("orgId", orgId).executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, Object> selectDetailWithUsers(String id) {
|
public Map<String, Object> selectDetailWithUsers(String id) {
|
||||||
return getDetailWithUsers(id, true);
|
return getDetailWithUsers(id, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
package com.eactive.eai.rms.onl.common.util;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.apache.log4j.Logger;
|
|
||||||
|
|
||||||
import com.eactive.eai.rms.onl.common.service.OnlAgentUtilServiceImpl;
|
|
||||||
|
|
||||||
import io.kubernetes.client.openapi.ApiClient;
|
|
||||||
import io.kubernetes.client.openapi.Configuration;
|
|
||||||
import io.kubernetes.client.openapi.apis.CoreV1Api;
|
|
||||||
import io.kubernetes.client.openapi.models.V1EndpointAddress;
|
|
||||||
import io.kubernetes.client.openapi.models.V1EndpointSubset;
|
|
||||||
import io.kubernetes.client.openapi.models.V1Endpoints;
|
|
||||||
import io.kubernetes.client.util.ClientBuilder;
|
|
||||||
|
|
||||||
public class K8sUtil {
|
|
||||||
private static final Logger logger = Logger.getLogger(K8sUtil.class);
|
|
||||||
|
|
||||||
public static List<Map<String, String>> getServerInfoByK8sUrl(String namespace, String serviceName, int port, String extUrl) throws Exception {
|
|
||||||
|
|
||||||
List<Map<String, String>> resultList = getServerInfoByK8sApi(namespace,serviceName);
|
|
||||||
|
|
||||||
for (Map<String,String> map : resultList) {
|
|
||||||
String EAISEVRINSTNCNAME = map.get("EAISEVRINSTNCNAME");
|
|
||||||
String EAISEVRIP = map.get("EAISEVRIP");
|
|
||||||
Map<String, String> serverInfo = new HashMap<>();
|
|
||||||
|
|
||||||
serverInfo.put(OnlAgentUtilServiceImpl.FIELD_URL, new StringBuilder().append("http://" + EAISEVRIP + ":"+String.valueOf(port) + extUrl).toString());
|
|
||||||
serverInfo.put(OnlAgentUtilServiceImpl.FIELD_INST_NAME, EAISEVRINSTNCNAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultList;
|
|
||||||
}
|
|
||||||
public static List<Map<String, String>> getServerInfoByK8sApi(String namespace, String serviceName) throws Exception {
|
|
||||||
|
|
||||||
logger.info("K8sAgentUtilService getServerInfoByK8sApi start.");
|
|
||||||
|
|
||||||
List<Map<String, String>> resultList = new ArrayList<>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
ApiClient client = ClientBuilder.cluster().build();
|
|
||||||
Configuration.setDefaultApiClient(client);
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(namespace)) {
|
|
||||||
new RuntimeException("Can't find kubernetes_namespace in Engine Property");
|
|
||||||
}
|
|
||||||
if (StringUtils.isBlank(serviceName)) {
|
|
||||||
new RuntimeException("Can't find kubernetes_servicename in Engine Property");
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("getServerInfoByK8sApi Codes [ namespace: " + namespace + ", serviceName:" + serviceName + " ]");
|
|
||||||
|
|
||||||
V1Endpoints endpoints = new CoreV1Api().readNamespacedEndpoints(serviceName, namespace, null);
|
|
||||||
|
|
||||||
for (V1EndpointSubset subset : endpoints.getSubsets()) {
|
|
||||||
for (V1EndpointAddress address : subset.getAddresses()) {
|
|
||||||
Map<String, String> serverInfo = new HashMap<>();
|
|
||||||
serverInfo.put("EAISEVRINSTNCNAME", address.getTargetRef().getName());
|
|
||||||
serverInfo.put("EAISEVRIP", address.getIp());
|
|
||||||
resultList.add(serverInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("getServerInfoByK8sApi resultList [ " + resultList.toString() + " ]");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error(e);
|
|
||||||
throw new RuntimeException("K8S Mode로 AgentUtilService를 기동중 실패하였습니다.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultList;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
package com.eactive.eai.rms.onl.manage.crypto;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.propertyeditors.CustomNumberEditor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.web.SortDefault;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.InitBinder;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import com.eactive.eai.agent.command.CommandResult;
|
||||||
|
import com.eactive.eai.agent.command.CommonCommand;
|
||||||
|
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
|
||||||
|
import com.eactive.eai.rms.common.login.SessionManager;
|
||||||
|
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class CryptoModuleConfigManController extends OnlBaseAnnotationController {
|
||||||
|
|
||||||
|
private static final String RELOAD_CRYPTO_MODULE_COMMAND = "com.eactive.eai.agent.security.ReloadCryptoModuleCommand";
|
||||||
|
|
||||||
|
private final CryptoModuleConfigManService service;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CryptoModuleConfigManController(CryptoModuleConfigManService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitBinder
|
||||||
|
public void initBinder(WebDataBinder binder) {
|
||||||
|
binder.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/onl/admin/security/cryptoModuleMan.view")
|
||||||
|
public String viewList() {
|
||||||
|
return "/onl/admin/security/cryptoModuleMan";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/onl/admin/security/cryptoModuleMan.view", params = "cmd=DETAIL")
|
||||||
|
public String detailView() {
|
||||||
|
return "/onl/admin/security/cryptoModuleManDetail";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=LIST")
|
||||||
|
public ResponseEntity<GridResponse<CryptoModuleConfigUI>> selectList(
|
||||||
|
@SortDefault("cryptoName") Pageable pageable,
|
||||||
|
String searchName, String algType, String keySourceType, String useYn) {
|
||||||
|
Page<CryptoModuleConfigUI> page = service.selectList(pageable, searchName, algType, keySourceType, useYn);
|
||||||
|
return ResponseEntity.ok(new GridResponse<>(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=DETAIL")
|
||||||
|
public ResponseEntity<CryptoModuleConfigUI> selectDetail(String cryptoId) {
|
||||||
|
return ResponseEntity.ok(service.selectDetail(cryptoId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=INSERT")
|
||||||
|
public ResponseEntity<Map<String, String>> insert(@Valid CryptoModuleConfigUI ui, BindingResult bindingResult,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
if (bindingResult.hasErrors()) {
|
||||||
|
Map<String, String> errors = new HashMap<>();
|
||||||
|
for (FieldError error : bindingResult.getFieldErrors()) {
|
||||||
|
errors.put(error.getField(), error.getDefaultMessage());
|
||||||
|
}
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
|
||||||
|
}
|
||||||
|
String cryptoId = service.insert(ui, SessionManager.getUserId(request));
|
||||||
|
CommandResult broadcastResult = CommonCommand.builder()
|
||||||
|
.name(RELOAD_CRYPTO_MODULE_COMMAND)
|
||||||
|
.args(cryptoId)
|
||||||
|
.build()
|
||||||
|
.broadcast(agentUtilService);
|
||||||
|
Map<String, String> body = new HashMap<>();
|
||||||
|
body.put("broadcastResult", broadcastResult.getMessage(true));
|
||||||
|
return ResponseEntity.ok(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=UPDATE")
|
||||||
|
public ResponseEntity<Map<String, String>> update(@Valid CryptoModuleConfigUI ui, BindingResult bindingResult,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
if (bindingResult.hasErrors()) {
|
||||||
|
Map<String, String> errors = new HashMap<>();
|
||||||
|
for (FieldError error : bindingResult.getFieldErrors()) {
|
||||||
|
errors.put(error.getField(), error.getDefaultMessage());
|
||||||
|
}
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
|
||||||
|
}
|
||||||
|
service.update(ui, SessionManager.getUserId(request));
|
||||||
|
CommandResult broadcastResult = CommonCommand.builder()
|
||||||
|
.name(RELOAD_CRYPTO_MODULE_COMMAND)
|
||||||
|
.args(ui.getCryptoId())
|
||||||
|
.build()
|
||||||
|
.broadcast(agentUtilService);
|
||||||
|
Map<String, String> body = new HashMap<>();
|
||||||
|
body.put("broadcastResult", broadcastResult.getMessage(true));
|
||||||
|
return ResponseEntity.ok(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/onl/admin/security/cryptoModuleMan.json", params = "cmd=DELETE")
|
||||||
|
public ResponseEntity<Map<String, String>> delete(String cryptoId) {
|
||||||
|
service.delete(cryptoId);
|
||||||
|
CommandResult broadcastResult = CommonCommand.builder()
|
||||||
|
.name(RELOAD_CRYPTO_MODULE_COMMAND)
|
||||||
|
.args(cryptoId)
|
||||||
|
.build()
|
||||||
|
.broadcast(agentUtilService);
|
||||||
|
Map<String, String> body = new HashMap<>();
|
||||||
|
body.put("broadcastResult", broadcastResult.getMessage(true));
|
||||||
|
return ResponseEntity.ok(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.eactive.eai.rms.onl.manage.crypto;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||||
|
import com.eactive.eai.rms.common.base.BaseService;
|
||||||
|
import com.eactive.eai.rms.data.entity.onl.security.CryptoModuleConfigDataService;
|
||||||
|
import com.eactive.eai.rms.onl.common.exception.BizException;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class CryptoModuleConfigManService extends BaseService {
|
||||||
|
|
||||||
|
private final CryptoModuleConfigDataService dataService;
|
||||||
|
private final CryptoModuleConfigUiMapper mapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CryptoModuleConfigManService(CryptoModuleConfigDataService dataService,
|
||||||
|
CryptoModuleConfigUiMapper mapper) {
|
||||||
|
this.dataService = dataService;
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<CryptoModuleConfigUI> selectList(Pageable pageable, String searchName,
|
||||||
|
String algType, String keySourceType, String useYn) {
|
||||||
|
Page<CryptoModuleConfig> page = dataService.findAll(pageable, searchName, algType, keySourceType, useYn);
|
||||||
|
return page.map(mapper::toVo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CryptoModuleConfigUI selectDetail(String cryptoId) {
|
||||||
|
CryptoModuleConfig entity = dataService.findById(cryptoId)
|
||||||
|
.orElseThrow(() -> new BizException("암호화 모듈을 찾을 수 없습니다: " + cryptoId));
|
||||||
|
return mapper.toVo(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String insert(CryptoModuleConfigUI ui, String loginId) {
|
||||||
|
CryptoModuleConfig entity = mapper.toEntity(ui);
|
||||||
|
entity.setCryptoId(null);
|
||||||
|
entity.setModifiedBy(loginId);
|
||||||
|
entity.setModifiedAt(LocalDateTime.now());
|
||||||
|
dataService.save(entity);
|
||||||
|
return entity.getCryptoId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(CryptoModuleConfigUI ui, String loginId) {
|
||||||
|
CryptoModuleConfig entity = dataService.findById(ui.getCryptoId())
|
||||||
|
.orElseThrow(() -> new BizException("암호화 모듈을 찾을 수 없습니다: " + ui.getCryptoId()));
|
||||||
|
mapper.updateToEntity(ui, entity);
|
||||||
|
entity.setModifiedBy(loginId);
|
||||||
|
entity.setModifiedAt(LocalDateTime.now());
|
||||||
|
dataService.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String cryptoId) {
|
||||||
|
dataService.deleteById(cryptoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.eactive.eai.rms.onl.manage.crypto;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CryptoModuleConfigUI {
|
||||||
|
|
||||||
|
@JsonProperty("CRYPTO_ID")
|
||||||
|
private String cryptoId;
|
||||||
|
|
||||||
|
@JsonProperty("CRYPTO_NAME")
|
||||||
|
@NotNull
|
||||||
|
@Size(max = 100)
|
||||||
|
private String cryptoName;
|
||||||
|
|
||||||
|
@JsonProperty("CRYPTO_DESC")
|
||||||
|
private String cryptoDesc;
|
||||||
|
|
||||||
|
@JsonProperty("ALG_TYPE")
|
||||||
|
@NotNull
|
||||||
|
private String algType;
|
||||||
|
|
||||||
|
@JsonProperty("CIPHER_MODE")
|
||||||
|
@NotNull
|
||||||
|
private String cipherMode;
|
||||||
|
|
||||||
|
@JsonProperty("PADDING")
|
||||||
|
private String padding;
|
||||||
|
|
||||||
|
@JsonProperty("IV_HEX")
|
||||||
|
private String ivHex;
|
||||||
|
|
||||||
|
@JsonProperty("KEY_SOURCE_TYPE")
|
||||||
|
@NotNull
|
||||||
|
private String keySourceType;
|
||||||
|
|
||||||
|
@JsonProperty("ENC_KEY_HEX")
|
||||||
|
private String encKeyHex;
|
||||||
|
|
||||||
|
@JsonProperty("DEC_KEY_HEX")
|
||||||
|
private String decKeyHex;
|
||||||
|
|
||||||
|
@JsonProperty("KEY_DERIV_STRATEGY")
|
||||||
|
private String keyDerivStrategy;
|
||||||
|
|
||||||
|
@JsonProperty("KEY_DERIV_PARAMS")
|
||||||
|
private String keyDerivParams;
|
||||||
|
|
||||||
|
@JsonProperty("CACHE_YN")
|
||||||
|
@NotNull
|
||||||
|
private String cacheYn;
|
||||||
|
|
||||||
|
@JsonProperty("CACHE_TTL_SEC")
|
||||||
|
private Integer cacheTtlSec;
|
||||||
|
|
||||||
|
@JsonProperty("USE_YN")
|
||||||
|
@NotNull
|
||||||
|
private String useYn;
|
||||||
|
|
||||||
|
@JsonProperty("MODIFIED_BY")
|
||||||
|
private String modifiedBy;
|
||||||
|
|
||||||
|
@JsonProperty("MODIFIED_AT")
|
||||||
|
private String modifiedAt;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.eactive.eai.rms.onl.manage.crypto;
|
||||||
|
|
||||||
|
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.security.CryptoModuleConfig;
|
||||||
|
import com.eactive.eai.data.mapper.GenericMapper;
|
||||||
|
|
||||||
|
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||||
|
public interface CryptoModuleConfigUiMapper extends GenericMapper<CryptoModuleConfigUI, CryptoModuleConfig> {
|
||||||
|
|
||||||
|
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||||
|
void updateToEntity(CryptoModuleConfigUI ui, @MappingTarget CryptoModuleConfig entity);
|
||||||
|
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
package com.eactive.eai.rms.onl.manage.inflow.inflow;
|
||||||
|
|
||||||
|
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.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;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class InflowClientControlManController extends OnlBaseAnnotationController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private InflowControlManService service;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ComboService comboService;
|
||||||
|
|
||||||
|
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.view")
|
||||||
|
public String viewList() {
|
||||||
|
return "/onl/admin/inflow/inflowClientControlMan";
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.view", params = "cmd=DETAIL")
|
||||||
|
public String viewDetail() {
|
||||||
|
return "/onl/admin/inflow/inflowClientControlManDetail";
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.json", params = "cmd=LIST")
|
||||||
|
public ResponseEntity<GridResponse<InflowControlManUI>> selectList(HttpServletRequest request, Pageable pageVo,
|
||||||
|
String searchName) {
|
||||||
|
Page<InflowControlManUI> uiPage = service.selectClientList(pageVo, searchName);
|
||||||
|
return ResponseEntity.ok(new GridResponse<>(uiPage));
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.json", params = "cmd=DETAIL")
|
||||||
|
public ResponseEntity<InflowControlManUI> selectDetail(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
String name) {
|
||||||
|
InflowControlManUI ui = service.selectClientDetail(name);
|
||||||
|
return ResponseEntity.ok(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.json", params = "cmd=UPDATE")
|
||||||
|
public String save(HttpServletRequest request, HttpServletResponse response, InflowControlManUI ui)
|
||||||
|
throws Exception {
|
||||||
|
service.mergeClient(ui);
|
||||||
|
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.ReloadInflowClientControlCommand",
|
||||||
|
ui.getName());
|
||||||
|
agentUtilService.broadcast(command);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.json", params = "cmd=DELETE")
|
||||||
|
public String delete(HttpServletRequest request, HttpServletResponse response, String name) throws Exception {
|
||||||
|
service.deleteClient(name);
|
||||||
|
CommonCommand command = new CommonCommand("com.eactive.eai.agent.inflow.RemoveInflowClientControlCommand",
|
||||||
|
name);
|
||||||
|
agentUtilService.broadcast(command);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "/onl/admin/inflow/inflowClientControlMan.json", params = "cmd=LIST_INIT_COMBO")
|
||||||
|
public ModelAndView initCombo(HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
List<ComboVo> useYnRows = comboService.getFromCode("USE_YN");
|
||||||
|
List<ComboVo> timeUnitRows = comboService.getFromCode("INFLOW_CTRL_TIME_UNIT");
|
||||||
|
Map<String, List<ComboVo>> resultMap = new HashMap<>();
|
||||||
|
resultMap.put("useYnRows", useYnRows);
|
||||||
|
resultMap.put("timeUnitRows", timeUnitRows);
|
||||||
|
return new ModelAndView("jsonView", resultMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+33
@@ -20,6 +20,7 @@ public class InflowControlManService extends BaseService {
|
|||||||
|
|
||||||
private static final String ADAPTER_TYPE_INFLOW = "01";
|
private static final String ADAPTER_TYPE_INFLOW = "01";
|
||||||
private static final String INTERFACE_TYPE_INFLOW = "02";
|
private static final String INTERFACE_TYPE_INFLOW = "02";
|
||||||
|
private static final String CLIENT_TYPE_INFLOW = "03";
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("comboService")
|
@Qualifier("comboService")
|
||||||
@@ -92,6 +93,38 @@ public class InflowControlManService extends BaseService {
|
|||||||
service.deleteById(id);
|
service.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 클라이언트 유량제어
|
||||||
|
public Page<InflowControlManUI> selectClientList(Pageable pageable, String searchName) {
|
||||||
|
Page<InflowControlServiceDto> dtoList = service.findAllForClient(pageable, searchName);
|
||||||
|
return dtoList.map(mapper::toVo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InflowControlManUI selectClientDetail(String name) {
|
||||||
|
return mapper.toVo(service.findByIdForClient(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void mergeClient(InflowControlManUI ui) {
|
||||||
|
ui.setType(CLIENT_TYPE_INFLOW);
|
||||||
|
InflowControlId id = toId(ui);
|
||||||
|
Optional<InflowControl> inflowControlOptional = service.findById(id);
|
||||||
|
|
||||||
|
InflowControl entity = null;
|
||||||
|
if (inflowControlOptional.isPresent()) {
|
||||||
|
entity = inflowControlOptional.get();
|
||||||
|
mapper.updateToEntity(ui, entity);
|
||||||
|
} else {
|
||||||
|
entity = mapper.toEntity(ui);
|
||||||
|
}
|
||||||
|
service.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteClient(String name) {
|
||||||
|
InflowControlId id = toId(CLIENT_TYPE_INFLOW, name);
|
||||||
|
service.findById(id).ifPresent(service::delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private InflowControlId toId(InflowControlManUI ui) {
|
private InflowControlId toId(InflowControlManUI ui) {
|
||||||
return toId(ui.getType(), ui.getName());
|
return toId(ui.getType(), ui.getName());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,7 +249,12 @@ public class ApiInterfaceService extends OnlBaseService {
|
|||||||
StdMessageUI stdMessageUI = stdMessageUIMapper.toVo(standardMessageInfo);
|
StdMessageUI stdMessageUI = stdMessageUIMapper.toVo(standardMessageInfo);
|
||||||
|
|
||||||
String stdKey = stdMessageUI.getBzwkSvcKeyName();
|
String stdKey = stdMessageUI.getBzwkSvcKeyName();
|
||||||
String inboundRestPath = StringUtils.substringAfter(stdKey, STEMSG_SERVICE_URL_DELIMITER);
|
String inboundRestPath;
|
||||||
|
if (StringUtils.contains(stdKey, STEMSG_SERVICE_HEADER_DELIMITER)) {
|
||||||
|
inboundRestPath = StringUtils.substringBetween(stdKey, STEMSG_SERVICE_URL_DELIMITER, STEMSG_SERVICE_HEADER_DELIMITER);
|
||||||
|
} else {
|
||||||
|
inboundRestPath = StringUtils.substringAfter(stdKey, STEMSG_SERVICE_URL_DELIMITER);
|
||||||
|
}
|
||||||
apiInterfaceUI.setInboundRestPath(inboundRestPath);
|
apiInterfaceUI.setInboundRestPath(inboundRestPath);
|
||||||
apiInterfaceUI.setApiFullPath(standardMessageInfo.getApifullpath());
|
apiInterfaceUI.setApiFullPath(standardMessageInfo.getApifullpath());
|
||||||
|
|
||||||
@@ -257,7 +262,11 @@ public class ApiInterfaceService extends OnlBaseService {
|
|||||||
|
|
||||||
String inboundHttpMethod;
|
String inboundHttpMethod;
|
||||||
if (StringUtils.contains(stdKey, STEMSG_SERVICE_HEADER_DELIMITER)) {
|
if (StringUtils.contains(stdKey, STEMSG_SERVICE_HEADER_DELIMITER)) {
|
||||||
inboundHttpMethod = StringUtils.substringBetween(stdKey, STEMSG_METHOD_DELIMITER, STEMSG_SERVICE_HEADER_DELIMITER);
|
if (StringUtils.contains(stdKey, STEMSG_SERVICE_URL_DELIMITER)) {
|
||||||
|
inboundHttpMethod = StringUtils.substringBetween(stdKey, STEMSG_METHOD_DELIMITER, STEMSG_SERVICE_URL_DELIMITER);
|
||||||
|
} else {
|
||||||
|
inboundHttpMethod = StringUtils.substringBetween(stdKey, STEMSG_METHOD_DELIMITER, STEMSG_SERVICE_HEADER_DELIMITER);
|
||||||
|
}
|
||||||
String headerRoutingValueString = StringUtils.substringAfter(stdKey, STEMSG_SERVICE_HEADER_DELIMITER);
|
String headerRoutingValueString = StringUtils.substringAfter(stdKey, STEMSG_SERVICE_HEADER_DELIMITER);
|
||||||
|
|
||||||
List<String> headerValues = new ArrayList<>();
|
List<String> headerValues = new ArrayList<>();
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ public class StandardMessageInfoQueryServiceForApi extends AbstractDataService<S
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deleteByEaiSvcName(String eaiSvcName){
|
public void deleteByEaiSvcName(String eaiSvcName){
|
||||||
repository.deleteByEaisvcname(eaiSvcName);
|
repository.findByEaisvcname(eaiSvcName).ifPresent(repository::delete);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void existsByApiFullPathAndEaiSvcNameNot(String apiFullPath, String eaiServiceName) {
|
public void existsByApiFullPathAndEaiSvcNameNot(String apiFullPath, String eaiServiceName) {
|
||||||
|
|||||||
+5
@@ -36,6 +36,7 @@ public interface ApiInterfaceUIMapper {
|
|||||||
@Mapping(source = "lastModifiedDate", target = "eailastamndyms")
|
@Mapping(source = "lastModifiedDate", target = "eailastamndyms")
|
||||||
@Mapping(source = "apiEnabledYn", target = "apienabledyn")
|
@Mapping(source = "apiEnabledYn", target = "apienabledyn")
|
||||||
@Mapping(source = "authType", target = "authtype")
|
@Mapping(source = "authType", target = "authtype")
|
||||||
|
@Mapping(source = "verinfo", target = "verinfo")
|
||||||
@Mapping(target = "svchmseonot", constant = "0")
|
@Mapping(target = "svchmseonot", constant = "0")
|
||||||
@Mapping(target = "svcmotivusedstcd", constant = "SYNC")
|
@Mapping(target = "svcmotivusedstcd", constant = "SYNC")
|
||||||
@Mapping(target = "svcprcesdsticname", constant = "SINGLE")
|
@Mapping(target = "svcprcesdsticname", constant = "SINGLE")
|
||||||
@@ -57,6 +58,7 @@ public interface ApiInterfaceUIMapper {
|
|||||||
@Mapping(source = "eaiMessageEntity.authtype", target = "authType")
|
@Mapping(source = "eaiMessageEntity.authtype", target = "authType")
|
||||||
@Mapping(target = "lastModifiedDate", source = "eaiMessageEntity.eailastamndyms")
|
@Mapping(target = "lastModifiedDate", source = "eaiMessageEntity.eailastamndyms")
|
||||||
@Mapping(target = "svcLogLvelNo", source = "eaiMessageEntity.svcloglvelno")
|
@Mapping(target = "svcLogLvelNo", source = "eaiMessageEntity.svcloglvelno")
|
||||||
|
@Mapping(target = "verinfo", source = "eaiMessageEntity.verinfo")
|
||||||
ApiInterfaceUI toVo(EAIMessageEntity eaiMessageEntity, List<ServiceMessageEntity> serviceMessageEntities);
|
ApiInterfaceUI toVo(EAIMessageEntity eaiMessageEntity, List<ServiceMessageEntity> serviceMessageEntities);
|
||||||
|
|
||||||
@InheritConfiguration(name="toEaiMessageEntity")
|
@InheritConfiguration(name="toEaiMessageEntity")
|
||||||
@@ -165,6 +167,9 @@ public interface ApiInterfaceUIMapper {
|
|||||||
String stdMessageKey = inboundAdapterGroupName + ApiInterfaceService.STEMSG_METHOD_DELIMITER + inboundMethod;
|
String stdMessageKey = inboundAdapterGroupName + ApiInterfaceService.STEMSG_METHOD_DELIMITER + inboundMethod;
|
||||||
|
|
||||||
if(vo.getIsHeaderRouting() != null && vo.getIsHeaderRouting() && headerValues != null && headerValues.size() > 0){
|
if(vo.getIsHeaderRouting() != null && vo.getIsHeaderRouting() && headerValues != null && headerValues.size() > 0){
|
||||||
|
if(!StringUtils.isBlank(inboundRestPath)) {
|
||||||
|
stdMessageKey += ApiInterfaceService.STEMSG_SERVICE_URL_DELIMITER + inboundRestPath;
|
||||||
|
}
|
||||||
stdMessageKey += ApiInterfaceService.STEMSG_SERVICE_HEADER_DELIMITER + String.join(ApiInterfaceService.STEMSG_HEADER_SEPARATOR, headerValues);
|
stdMessageKey += ApiInterfaceService.STEMSG_SERVICE_HEADER_DELIMITER + String.join(ApiInterfaceService.STEMSG_HEADER_SEPARATOR, headerValues);
|
||||||
}else{
|
}else{
|
||||||
if(StringUtils.isBlank(inboundRestPath)) {
|
if(StringUtils.isBlank(inboundRestPath)) {
|
||||||
|
|||||||
+7
-1
@@ -2,6 +2,10 @@ package com.eactive.eai.rms.onl.transaction.apim.repository;
|
|||||||
|
|
||||||
import java.util.Optional;
|
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.stdmessage.StandardMessageInfo;
|
import com.eactive.eai.data.entity.onl.stdmessage.StandardMessageInfo;
|
||||||
import com.eactive.eai.data.jpa.BaseRepository;
|
import com.eactive.eai.data.jpa.BaseRepository;
|
||||||
|
|
||||||
@@ -9,7 +13,9 @@ import com.eactive.eai.data.jpa.BaseRepository;
|
|||||||
public interface StandardMessageInfoRepositoryForApi extends BaseRepository<StandardMessageInfo, String> {
|
public interface StandardMessageInfoRepositoryForApi extends BaseRepository<StandardMessageInfo, String> {
|
||||||
Optional<StandardMessageInfo> findByEaisvcname(String eaiSvcName);
|
Optional<StandardMessageInfo> findByEaisvcname(String eaiSvcName);
|
||||||
|
|
||||||
void deleteByEaisvcname(String eaiSvcName);
|
// @Modifying
|
||||||
|
// @Query("DELETE FROM StandardMessageInfo s WHERE s.eaisvcname = :eaiSvcName")
|
||||||
|
// void deleteByEaisvcname(@Param("eaiSvcName") String eaiSvcName);
|
||||||
|
|
||||||
boolean existsByApifullpathAndEaisvcnameNot(String fullPath, String eaiServiceName);
|
boolean existsByApifullpathAndEaisvcnameNot(String fullPath, String eaiServiceName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ public class ApiInterfaceUI {
|
|||||||
private Integer svcLogLvelNo;
|
private Integer svcLogLvelNo;
|
||||||
private String apiEnabledYn;
|
private String apiEnabledYn;
|
||||||
private String authType;
|
private String authType;
|
||||||
|
private String verinfo;
|
||||||
private Boolean isHeaderRouting;
|
private Boolean isHeaderRouting;
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime lastModifiedDate;
|
private LocalDateTime lastModifiedDate;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
import com.eactive.eai.rms.ext.djb.job.ApiStatsHourlyAggregationJob;
|
||||||
import com.eactive.ext.kjb.statistics.job.DailyToMonthlyAggregationJob;
|
import com.eactive.ext.kjb.statistics.job.DailyToMonthlyAggregationJob;
|
||||||
import com.eactive.ext.kjb.statistics.job.HourlyToDailyAggregationJob;
|
import com.eactive.ext.kjb.statistics.job.HourlyToDailyAggregationJob;
|
||||||
import com.eactive.ext.kjb.statistics.job.MonthlyToYearlyAggregationJob;
|
import com.eactive.ext.kjb.statistics.job.MonthlyToYearlyAggregationJob;
|
||||||
@@ -34,11 +35,61 @@ public class ApiStatsAggregationController {
|
|||||||
private final DailyToMonthlyAggregationJob dailyToMonthlyAggregationJob;
|
private final DailyToMonthlyAggregationJob dailyToMonthlyAggregationJob;
|
||||||
private final MonthlyToYearlyAggregationJob monthlyToYearlyAggregationJob;
|
private final MonthlyToYearlyAggregationJob monthlyToYearlyAggregationJob;
|
||||||
|
|
||||||
|
private final ApiStatsHourlyAggregationJob apiStatsHourlyAggregationJob;
|
||||||
|
|
||||||
@GetMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.view")
|
@GetMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.view")
|
||||||
public String view() {
|
public String view() {
|
||||||
return "/onl/kjb/statistics/apiStatsAggregationMan";
|
return "/onl/kjb/statistics/apiStatsAggregationMan";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 거래로그→시간별 통계 집계 수동 실행
|
||||||
|
* @param targetDate 집계 대상 날짜 (yyyyMMdd 형식, 예: 20250120)
|
||||||
|
* @return 처리 결과
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/onl/kjb/statistics/apiStatsAggregationMan.json", params = "cmd=AGGREGATION_HOUR")
|
||||||
|
public ResponseEntity<Map<String, Object>> executeAggregationHourly(
|
||||||
|
@RequestParam(required = false) String targetDate) {
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 날짜 입력 검증
|
||||||
|
if (targetDate == null || targetDate.trim().isEmpty()) {
|
||||||
|
log.error("targetDate 미입력");
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "대상 날짜를 입력하세요. (예: 20250120)");
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate date;
|
||||||
|
try {
|
||||||
|
date = LocalDate.parse(targetDate, DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
log.error("잘못된 날짜 형식: {}", targetDate);
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "잘못된 날짜 형식입니다. yyyyMMdd 형식으로 입력하세요. (예: 20250120)");
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("거래로그→시간별 통계 집계 수동 실행 시작. targetDate: {}", date);
|
||||||
|
int count = apiStatsHourlyAggregationJob.executeManual(date);
|
||||||
|
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("message", "거래로그→시간별 통계 집계가 완료되었습니다.");
|
||||||
|
result.put("targetDate", date.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
|
||||||
|
result.put("processedCount", count);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("거래로그→시간별 통계 집계 실행 중 오류 발생", e);
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "집계 실행 중 오류가 발생했습니다: " + e.getMessage());
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 시간별→일별 통계 집계 수동 실행
|
* 시간별→일별 통계 집계 수동 실행
|
||||||
* @param targetDate 집계 대상 날짜 (yyyyMMdd 형식, 예: 20250120)
|
* @param targetDate 집계 대상 날짜 (yyyyMMdd 형식, 예: 20250120)
|
||||||
|
|||||||
@@ -138,9 +138,9 @@ public class DailyToMonthlyAggregationJob implements Job {
|
|||||||
qd.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
|
qd.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
|
||||||
qd.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
qd.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
||||||
Expressions.cases()
|
Expressions.cases()
|
||||||
.when(qd.successCnt.sum().gt(0))
|
.when(qd.totalCnt.sum().gt(0))
|
||||||
.then(qd.avgRespTime.multiply(qd.successCnt).sum()
|
.then(qd.avgRespTime.multiply(qd.totalCnt).sum()
|
||||||
.divide(qd.successCnt.sum()).castToNum(BigDecimal.class))
|
.divide(qd.totalCnt.sum()).castToNum(BigDecimal.class))
|
||||||
.otherwise((BigDecimal) null)
|
.otherwise((BigDecimal) null)
|
||||||
.as("avgRespTime"),
|
.as("avgRespTime"),
|
||||||
qd.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
qd.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
||||||
|
|||||||
@@ -133,9 +133,9 @@ public class HourlyToDailyAggregationJob implements Job {
|
|||||||
qh.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
|
qh.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
|
||||||
qh.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
qh.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
||||||
Expressions.cases()
|
Expressions.cases()
|
||||||
.when(qh.successCnt.sum().gt(0))
|
.when(qh.totalCnt.sum().gt(0))
|
||||||
.then(qh.avgRespTime.multiply(qh.successCnt).sum()
|
.then(qh.avgRespTime.multiply(qh.totalCnt).sum()
|
||||||
.divide(qh.successCnt.sum()).castToNum(BigDecimal.class))
|
.divide(qh.totalCnt.sum()).castToNum(BigDecimal.class))
|
||||||
.otherwise((BigDecimal) null)
|
.otherwise((BigDecimal) null)
|
||||||
.as("avgRespTime"),
|
.as("avgRespTime"),
|
||||||
qh.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
qh.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
||||||
|
|||||||
@@ -136,9 +136,9 @@ public class MonthlyToYearlyAggregationJob implements Job {
|
|||||||
qm.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
|
qm.seq900SystemErrCnt.sum().as("seq900SystemErrCnt"),
|
||||||
qm.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
qm.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
|
||||||
Expressions.cases()
|
Expressions.cases()
|
||||||
.when(qm.successCnt.sum().gt(0))
|
.when(qm.totalCnt.sum().gt(0))
|
||||||
.then(qm.avgRespTime.multiply(qm.successCnt).sum()
|
.then(qm.avgRespTime.multiply(qm.totalCnt).sum()
|
||||||
.divide(qm.successCnt.sum()).castToNum(BigDecimal.class))
|
.divide(qm.totalCnt.sum()).castToNum(BigDecimal.class))
|
||||||
.otherwise((BigDecimal) null)
|
.otherwise((BigDecimal) null)
|
||||||
.as("avgRespTime"),
|
.as("avgRespTime"),
|
||||||
qm.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
qm.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
|
||||||
|
|||||||
+4
-9
@@ -36,9 +36,8 @@ public class ApiStatsExcelExportService {
|
|||||||
private static final String[] COLUMN_HEADERS = {
|
private static final String[] COLUMN_HEADERS = {
|
||||||
"통계시간", "API명", "인스턴스", "업무구분", "클라이언트ID",
|
"통계시간", "API명", "인스턴스", "업무구분", "클라이언트ID",
|
||||||
"Inbound Adapter", "Outbound Adapter",
|
"Inbound Adapter", "Outbound Adapter",
|
||||||
"총건수", "성공건수", "Timeout건수", "시스템오류건수", "업무오류건수",
|
"총건수", "성공건수", "Timeout건수", "시스템오류건수",
|
||||||
"Seq900 Timeout", "Seq900 시스템오류", "Seq900 업무오류",
|
"평균응답시간", "최소응답시간", "최대응답시간"
|
||||||
"평균응답시간", "최소응답시간", "최대응답시간", "P50응답시간", "P95응답시간"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -193,17 +192,13 @@ public class ApiStatsExcelExportService {
|
|||||||
createLongCell(row, colNum++, data.getSuccessCnt(), numberStyle);
|
createLongCell(row, colNum++, data.getSuccessCnt(), numberStyle);
|
||||||
createLongCell(row, colNum++, data.getTimeoutCnt(), numberStyle);
|
createLongCell(row, colNum++, data.getTimeoutCnt(), numberStyle);
|
||||||
createLongCell(row, colNum++, data.getSystemErrCnt(), numberStyle);
|
createLongCell(row, colNum++, data.getSystemErrCnt(), numberStyle);
|
||||||
createLongCell(row, colNum++, data.getBizErrCnt(), numberStyle);
|
|
||||||
createLongCell(row, colNum++, data.getSeq900TimeoutCnt(), numberStyle);
|
|
||||||
createLongCell(row, colNum++, data.getSeq900SystemErrCnt(), numberStyle);
|
|
||||||
createLongCell(row, colNum++, data.getSeq900BizErrCnt(), numberStyle);
|
|
||||||
|
|
||||||
// 소수점 컬럼 (15-19)
|
// 소수점 컬럼 (15-19)
|
||||||
createDecimalCell(row, colNum++, data.getAvgRespTime(), decimalStyle);
|
createDecimalCell(row, colNum++, data.getAvgRespTime(), decimalStyle);
|
||||||
createDecimalCell(row, colNum++, data.getMinRespTime(), decimalStyle);
|
createDecimalCell(row, colNum++, data.getMinRespTime(), decimalStyle);
|
||||||
createDecimalCell(row, colNum++, data.getMaxRespTime(), decimalStyle);
|
createDecimalCell(row, colNum++, data.getMaxRespTime(), decimalStyle);
|
||||||
createDecimalCell(row, colNum++, data.getP50RespTime(), decimalStyle);
|
|
||||||
createDecimalCell(row, colNum++, data.getP95RespTime(), decimalStyle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ public class ApiStatsSearch {
|
|||||||
private String searchClientId;
|
private String searchClientId;
|
||||||
private String searchInboundAdapter;
|
private String searchInboundAdapter;
|
||||||
private String searchOutboundAdapter;
|
private String searchOutboundAdapter;
|
||||||
|
private String searchOrgName;
|
||||||
|
private String searchType;
|
||||||
|
|
||||||
/** 파싱된 시작시간 */
|
/** 파싱된 시작시간 */
|
||||||
private LocalDateTime parsedStartDateTime;
|
private LocalDateTime parsedStartDateTime;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public class ApiStatsUI {
|
|||||||
private String gwInstanceId;
|
private String gwInstanceId;
|
||||||
private String bizDivCode;
|
private String bizDivCode;
|
||||||
private String clientId;
|
private String clientId;
|
||||||
|
private String orgName;
|
||||||
private String inboundAdapter;
|
private String inboundAdapter;
|
||||||
private String outboundAdapter;
|
private String outboundAdapter;
|
||||||
|
|
||||||
@@ -22,15 +23,13 @@ public class ApiStatsUI {
|
|||||||
private Long successCnt;
|
private Long successCnt;
|
||||||
private Long timeoutCnt;
|
private Long timeoutCnt;
|
||||||
private Long systemErrCnt;
|
private Long systemErrCnt;
|
||||||
private Long bizErrCnt;
|
|
||||||
private Long seq900TimeoutCnt;
|
|
||||||
private Long seq900SystemErrCnt;
|
|
||||||
private Long seq900BizErrCnt;
|
|
||||||
|
|
||||||
// 응답시간 메트릭
|
// 응답시간 메트릭
|
||||||
private BigDecimal avgRespTime;
|
private BigDecimal avgRespTime;
|
||||||
private BigDecimal minRespTime;
|
private BigDecimal minRespTime;
|
||||||
private BigDecimal maxRespTime;
|
private BigDecimal maxRespTime;
|
||||||
private BigDecimal p50RespTime;
|
|
||||||
private BigDecimal p95RespTime;
|
//성공율,실패율
|
||||||
|
private BigDecimal successRate;
|
||||||
|
private BigDecimal failRate;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user