1 Commits

Author SHA1 Message Date
curry772 a2824830cb config: local reposilite 환경으로 변경 2026-04-03 17:22:55 +09:00
103 changed files with 1334 additions and 4899 deletions
+2 -3
View File
@@ -1,3 +1,5 @@
gradle.properties
# Package Files # # Package Files #
*.war *.war
@@ -246,6 +248,3 @@ eapim-admin-kjb/
*.report.md *.report.md
*.claude.md *.claude.md
tmpclaude-*-cwd tmpclaude-*-cwd
logs
rinjae_
tomcat-base
+28 -36
View File
@@ -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에서 엔티티를 찾을 수 없다고 표시되면, `./gradlew compileJava` 를 먼저 실행하여 코드를 생성해야 합니다. - **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `kjb-gradle.sh compileJava`를 먼저 실행하여 코드를 생성해야 합니다.
- **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다. - **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다.
## 프로젝트 개요 ## 프로젝트 개요
@@ -39,48 +39,50 @@ eLink EMS (eLink Management System)는 eLink의 웹 기반 관리 서비스로,
### 빌드 명령어 ### 빌드 명령어
프로젝트 루트의 Gradle wrapper(`./gradlew`)를 사용합니다. JDK 8 toolchain 위치는 `.envrc` 등으로 잡습니다(아래 "기술 스택" 섹션 참조). **⚠️ 중요**: 환경변수 문제로 인해 `gradle` 대신 `kjb-gradle.sh` 스크립트를 사용해야 합니다.
- 스크립트 위치: `/c/eactive/workspaces/shell-scripts/kjb-gradle.sh`
- PATH에 등록되어 있으므로 바로 `kjb-gradle.sh` 명령어 사용 가능
```bash ```bash
# 표준 빌드 # 표준 빌드
./gradlew build kjb-gradle.sh build
# Weblogic 배포용 빌드 (테스트 제외) # Weblogic 배포용 빌드 (테스트 제외)
./gradlew build -x test -Pprofile=weblogic kjb-gradle.sh build -x test -Pprofile=weblogic
# WAR 파일 빌드 # WAR 파일 빌드
./gradlew war kjb-gradle.sh war
# 클린 빌드 # 클린 빌드
./gradlew clean build kjb-gradle.sh clean build
``` ```
### 테스트 실행 ### 테스트 실행
```bash ```bash
# 모든 테스트 실행 # 모든 테스트 실행
./gradlew test kjb-gradle.sh test
# 특정 테스트 클래스 실행 # 특정 테스트 클래스 실행
./gradlew test --tests "com.example.ClassName" kjb-gradle.sh test --tests "com.example.ClassName"
# 특정 테스트 패키지 실행 (JUnit 플랫폼 사용) # 특정 테스트 패키지 실행 (JUnit 플랫폼 사용)
./gradlew test --tests "com.eactive.eai.rms.*" kjb-gradle.sh test --tests "com.eactive.eai.rms.*"
``` ```
### 개발 태스크 ### 개발 태스크
```bash ```bash
# 패키징 없이 클래스만 컴파일 # 패키징 없이 클래스만 컴파일
./gradlew classes kjb-gradle.sh classes
# QueryDSL Q-classes 및 기타 애노테이션 생성 # QueryDSL Q-classes 및 기타 애노테이션 생성
./gradlew compileJava kjb-gradle.sh compileJava
# 의존성 트리 보기 # 의존성 트리 보기
./gradlew dependencies kjb-gradle.sh dependencies
# 사용 가능한 모든 태스크 목록 # 사용 가능한 모든 태스크 목록
./gradlew tasks --all kjb-gradle.sh tasks --all
``` ```
## 로컬 개발 빠른 시작 ## 로컬 개발 빠른 시작
@@ -90,26 +92,26 @@ eLink EMS (eLink Management System)는 eLink의 웹 기반 관리 서비스로,
1. **IDE 설정 파일 생성 (최초 1회)** 1. **IDE 설정 파일 생성 (최초 1회)**
```bash ```bash
# Eclipse 사용 시 (기본) # Eclipse 사용 시 (기본)
./gradlew eclipse kjb-gradle.sh 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. ./gradlew idea 실행 # 3. kjb-gradle.sh idea 실행
``` ```
2. **QueryDSL 등 소스 코드 생성** 2. **QueryDSL 등 소스 코드 생성**
```bash ```bash
./gradlew compileJava kjb-gradle.sh compileJava
``` ```
3. **전체 빌드 및 테스트 실행** 3. **전체 빌드 및 테스트 실행**
```bash ```bash
./gradlew build kjb-gradle.sh build
``` ```
4. **IDE에서 프로젝트 열기** 4. **IDE에서 프로젝트 열기**
- Eclipse: `./gradlew eclipse` 실행 후, IDE에서 프로젝트를 엽니다. - Eclipse: `kjb-gradle.sh eclipse` 실행 후, IDE에서 프로젝트를 엽니다.
- IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다. - IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다.
5. **실행 구성(Run Configuration) 설정** 5. **실행 구성(Run Configuration) 설정**
@@ -160,14 +162,14 @@ eapim-admin (root project)
#### Q-Class 생성 위치 #### Q-Class 생성 위치
Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다. Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다.
- `./gradlew compileJava` 또는 `./gradlew build` 실행 시 Gradle이 자동으로 생성 - `kjb-gradle.sh compileJava` 또는 `kjb-gradle.sh build` 실행 시 Gradle이 자동으로 생성
- 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다 - 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다
#### 생성된 코드 작업하기 #### 생성된 코드 작업하기
- **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다 - **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다
- **업데이트 pull 후**: `./gradlew compileJava` 를 실행하여 Q-classes를 재생성합니다 - **업데이트 pull 후**: `kjb-gradle.sh compileJava`를 실행하여 Q-classes를 재생성합니다
- **Q-classes IDE 오류 시**: `./gradlew clean compileJava` 로 재생성합니다 - **Q-classes IDE 오류 시**: `kjb-gradle.sh clean compileJava`로 재생성합니다
#### IDE별 빌드 설정 파일 #### IDE별 빌드 설정 파일
@@ -253,21 +255,12 @@ IntelliJ를 사용하려면 `build.gradle.intellij`를 `build.gradle`로 교체
WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다: WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다:
```bash ```bash
./gradlew build -x test -Pprofile=weblogic kjb-gradle.sh build -x test -Pprofile=weblogic
``` ```
## 기술 스택 ## 기술 스택
- **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` 도 후보 경로로 인식합니다. - **Java 8**: Gradle toolchain을 통해 강제
```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)
@@ -278,10 +271,9 @@ WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`
- **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` 로 다운그레이드 후 재도입 검토.
## 프로젝트 규칙 ## 프로젝트 규칙
### 패키지 구조 ### 패키지 구조
@@ -384,7 +376,7 @@ com.eactive.eai
### IntelliJ IDEA (권장) ### IntelliJ IDEA (권장)
```bash ```bash
./gradlew idea kjb-gradle.sh idea
``` ```
또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다. 또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다.
@@ -403,7 +395,7 @@ cp build.gradle build.gradle.intellij
cp build.gradle.eclipse build.gradle cp build.gradle.eclipse build.gradle
# 3. Eclipse 프로젝트 생성 # 3. Eclipse 프로젝트 생성
./gradlew eclipse kjb-gradle.sh 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 -12
View File
@@ -9,7 +9,15 @@
<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>
@@ -48,17 +56,6 @@
<filter-name>encodingFilter</filter-name> <filter-name>encodingFilter</filter-name>
<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>
@@ -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();
putSelectFromParam(); if(typeof callback === 'function') {
callback(url,key);
initGrid(); }
}, },
error:function(e){ error:function(e){
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
$(document).ready(function() {
function initGrid() { init();
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,13 +125,9 @@ function initGrid() {
} }
}); });
resizeJqGridWidth('grid','content_middle','1000');
}
$(document).ready(function() {
init();
resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){ $("#btn_search").click(function(){
var postData = getSearchForJqgrid("cmd","LIST"); var postData = getSearchForJqgrid("cmd","LIST");
$("#grid").setGridParam({ postData: postData ,page:"1" }).trigger("reloadGrid"); $("#grid").setGridParam({ postData: postData ,page:"1" }).trigger("reloadGrid");
@@ -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 style="width:150px">On Memory: <span id="deft_onMemory"></span></td> <td>On Memory: <span id="deft_onMemory"></span></td>
<td >Previous Fired: <span id="deft_previous"></span></td> <td style="width: 260px;">Previous Fired: <span id="deft_previous"></span></td>
<td >Next Fire Time: <span id="deft_next"></span></td> <td style="width: 260px;">Next Fire Time: <span id="deft_next"></span></td>
<td style="width: 250px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td> <td style="width: 220px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td>
<td style="width: 120px;">PersistJobData: <span id="deft_persistJobData"></span></td> <td style="width: 200px;">PersistJobData: <span id="deft_persistJobData"></span></td>
<td style="width: 120px;">Durable: <span id="deft_durable"></span></td> <td style="width: 200px;">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 style="width:150px">On Memory: <span id="clus_onMemory"></span></td> <td>On Memory: <span id="clus_onMemory"></span></td>
<td>Previous Fired: <span id="clus_previous"></span></td> <td style="width: 260px;">Previous Fired: <span id="clus_previous"></span></td>
<td>Next Fire Time: <span id="clus_next"></span></td> <td style="width: 260px;">Next Fire Time: <span id="clus_next"></span></td>
<td style="width: 250px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td> <td style="width: 220px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td>
<td style="width: 120px;">PersistJobData: <span id="clus_persistJobData"></span></td> <td style="width: 200px;">PersistJobData: <span id="clus_persistJobData"></span></td>
<td style="width: 120px;">Durable: <span id="clus_durable"></span></td> <td style="width: 200px;">Durable: <span id="clus_durable"></span></td>
</tr> </tr>
</table> </table>
@@ -1,138 +0,0 @@
<%@ 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>
@@ -1,185 +0,0 @@
<%@ 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>
@@ -1,153 +0,0 @@
<%@ 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>
@@ -1,365 +0,0 @@
<%@ 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,52 +17,11 @@
<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",
@@ -94,8 +53,6 @@
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'];
@@ -136,10 +93,6 @@
$("#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();
@@ -159,17 +112,9 @@
</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,75 +17,34 @@
<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'
? '<span style="color: red;">미사용</span>' ? '<span style="color: red;">미사용</span>'
: '<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:60 }, { name : 'rowNum' , align:'center', width:80 },
{ name : 'noticeType' , align:'center', width:60, formatter: formatNoticeType }, { name : 'noticeSubject' , align:'left' , width:200 , formatter: formatFile },
{ 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 },
@@ -139,8 +98,6 @@
} }
}); });
init();
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){ $("#btn_search").click(function(){
@@ -183,25 +140,9 @@
<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>게시유형</th> <th style="width:180px;">사용여부</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">
@@ -211,6 +152,8 @@
</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,36 +24,7 @@
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) {
@@ -127,10 +98,8 @@
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);
@@ -154,9 +123,19 @@
$(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', {
@@ -164,6 +143,9 @@
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];
@@ -193,7 +175,6 @@
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");
@@ -263,50 +244,31 @@
<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>게시유형</th> <th style="width:20%;">사용여부</th>
<td colspan="3">
<div class="select-style" >
<select name="noticeType" id="noticeType"></select>
</div>
</td>
</tr>
<tr>
<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' : ''}> 사용 <input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용</td>
</td>
<th>고정여부</th>
<td>
<input type="checkbox" name="fixYn" id="fixYn" value="Y" ${portalNoticeUI.fixYn eq 'Y' ? 'checked' : ''}> 고정
</td>
</tr> </tr>
<tr> <tr>
<th>본문 <font color="red">*</font></th> <th style="width:20%;">제목 <font color="red">*</font></th>
<td colspan="3"> <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> <textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
</td> </td>
</tr> </tr>
<!-- <tr> <tr>
<th>첨부파일</th> <th style="width:20%;">첨부파일</th>
<td colspan="3"> <td>
<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,54 +82,6 @@
</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");
@@ -289,7 +241,6 @@
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);
// 전월 // 전월
@@ -304,12 +255,6 @@
$("#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();
@@ -352,26 +297,9 @@
</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>2. 시간별 → 일별 통계 집계</h3> <h3>1. 시간별 → 일별 통계 집계</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">
@@ -388,7 +316,7 @@
<!-- 일별 → 월별 집계 --> <!-- 일별 → 월별 집계 -->
<div class="aggregation-section"> <div class="aggregation-section">
<h3>3. 일별 → 월별 통계 집계</h3> <h3>2. 일별 → 월별 통계 집계</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">
@@ -405,7 +333,7 @@
<!-- 월별 → 연별 집계 --> <!-- 월별 → 연별 집계 -->
<div class="aggregation-section"> <div class="aggregation-section">
<h3>4. 월별 → 연별 통계 집계</h3> <h3>3. 월별 → 연별 통계 집계</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, callChart; var totalDonutChart, seq900DonutChart, callChart, respChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -54,7 +54,9 @@
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 = {
@@ -71,7 +73,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류'] data: ['성공', 'Timeout', '시스템오류', '업무오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -96,7 +98,8 @@
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: [{
@@ -115,7 +118,64 @@
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 = {
@@ -131,13 +191,32 @@
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) {
@@ -189,7 +268,8 @@
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: [{
@@ -199,6 +279,22 @@
}] }]
}); });
// 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({
@@ -206,13 +302,24 @@
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() {
@@ -428,7 +535,8 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -437,6 +545,10 @@
{ 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 }
@@ -468,8 +580,9 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' 'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(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 },
@@ -483,9 +596,15 @@
{ 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'),
@@ -566,7 +685,9 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(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();
@@ -591,7 +712,7 @@
<tbody> <tbody>
<tr> <tr>
<th style="width:100px;">조회기간</th> <th style="width:100px;">조회기간</th>
<td colspan="5"> <td colspan="3">
<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;">
@@ -603,20 +724,22 @@
<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}">
@@ -631,7 +754,12 @@
<!-- 도넛 차트 (상단 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, callChart; var totalDonutChart, seq900DonutChart, callChart, respChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -55,7 +55,9 @@
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 = {
@@ -72,7 +74,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류'] data: ['성공', 'Timeout', '시스템오류', '업무오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -97,7 +99,8 @@
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: [{
@@ -116,7 +119,64 @@
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 = {
@@ -125,24 +185,47 @@
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) {
@@ -225,7 +308,8 @@
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: [{
@@ -235,6 +319,22 @@
}] }]
}); });
// 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({
@@ -242,13 +342,24 @@
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() {
@@ -430,7 +541,8 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -439,6 +551,10 @@
{ 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 }
@@ -470,8 +586,9 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' 'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(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 },
@@ -485,9 +602,15 @@
{ 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'),
@@ -546,7 +669,9 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(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();
@@ -611,7 +736,12 @@
<!-- 도넛 차트 (상단 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>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
@@ -622,7 +752,7 @@
<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>
@@ -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, callChart; var totalDonutChart, seq900DonutChart, callChart, respChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -47,7 +47,9 @@
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 = {
@@ -64,7 +66,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류'] data: ['성공', 'Timeout', '시스템오류', '업무오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -89,7 +91,8 @@
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: [{
@@ -108,7 +111,64 @@
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 = {
@@ -117,7 +177,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 },
@@ -128,12 +188,34 @@
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) {
@@ -187,7 +269,8 @@
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: [{
@@ -197,6 +280,22 @@
}] }]
}); });
// 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({
@@ -204,10 +303,20 @@
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시간)
@@ -443,7 +552,8 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -452,6 +562,10 @@
{ 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 }
@@ -483,8 +597,9 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' 'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(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 },
@@ -498,9 +613,15 @@
{ 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'),
@@ -590,7 +711,9 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(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();
@@ -610,7 +733,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">대시보드</div> <div class="title" id="title">API 분단위 통계</div>
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0">
<tbody> <tbody>
<tr> <tr>
@@ -624,28 +747,72 @@
<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>
</tbody> <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>
</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="callChart" class="chart"></div> <div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<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, callChart; var totalDonutChart, seq900DonutChart, callChart, respChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -54,7 +54,9 @@
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 = {
@@ -71,7 +73,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류'] data: ['성공', 'Timeout', '시스템오류', '업무오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -96,7 +98,8 @@
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: [{
@@ -115,7 +118,64 @@
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 = {
@@ -124,18 +184,39 @@
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) {
@@ -187,7 +268,8 @@
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: [{
@@ -197,7 +279,22 @@
}] }]
}); });
// 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({
@@ -210,10 +307,19 @@
] ]
}); });
// 응답시간 차트 업데이트
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() {
@@ -433,7 +539,8 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -442,6 +549,10 @@
{ 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 }
@@ -473,8 +584,9 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' 'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(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 },
@@ -488,9 +600,15 @@
{ 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'),
@@ -557,7 +675,9 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(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();
@@ -624,9 +744,13 @@
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="callChart" class="chart"></div> <div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
<div style="margin-top: 20px;"> <div style="margin-top: 20px;">
@@ -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, callChart; var totalDonutChart, seq900DonutChart, callChart, respChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -54,7 +54,9 @@
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 = {
@@ -71,7 +73,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류'] data: ['성공', 'Timeout', '시스템오류', '업무오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -96,7 +98,8 @@
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: [{
@@ -115,7 +118,64 @@
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 = {
@@ -124,20 +184,39 @@
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) {
@@ -189,7 +268,8 @@
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: [{
@@ -199,7 +279,22 @@
}] }]
}); });
// 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({
@@ -212,10 +307,19 @@
] ]
}); });
// 응답시간 차트 업데이트
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() {
@@ -409,7 +513,8 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -418,6 +523,10 @@
{ 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 }
@@ -449,8 +558,9 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' 'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(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 },
@@ -464,9 +574,15 @@
{ 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'),
@@ -528,7 +644,9 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(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();
@@ -594,7 +712,12 @@
<!-- 도넛 차트 (상단 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>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
@@ -1,313 +0,0 @@
<%@ 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,13 +962,11 @@
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;
$('#collapseInboundRoutingInfo').collapse('hide') $('#slideInboundRoutingRule').carousel(1);
//$('#slideInboundRoutingRule').carousel(1);
} }
} }
@@ -1574,7 +1572,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"
@@ -1617,19 +1615,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="$store.formState.isReqResDisabled"> value="S" :disabled="apiInterface.syncAsyncType !== 'async' || $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="$store.formState.isReqResDisabled"> value="R" :disabled="apiInterface.syncAsyncType !== 'async' || $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>
@@ -1773,34 +1771,36 @@
</select> </select>
</div> </div>
<div class="form-group col-md-8"> <div class="form-group col-md-8">
<div id="slideInboundRoutingRule"> <div id="slideInboundRoutingRule" class="carousel slide" data-ride="carousel" data-interval="false">
<div class="row"> <div class="carousel-inner" style="margin-bottom:-20px">
<div class="col-md-12"> <div class="carousel-item">
<label for="inboundRestPath"> <label>
<span class="material-icons">link</span> <span class="material-icons justify-content-md-start">http</span>
수신 REST PATH(URL) 수신 라우팅 정보
</label> </label>
<input type="text" class="form-control" id="inboundRestPath" <div class="d-grid gap-2">
name="inboundRestPath" disabled> <button type="button" id="headerRoutingButton" class="btn btn-primary"
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">
+13 -13
View File
@@ -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 nexusUrl = "https://nexus.eactive.synology.me:8090" def reposiliteUrl = "http://localhost:8080"
//def useOnJboss = false //def useOnJboss = false
def generatedJavaDir = "$buildDir/generated/java" def generatedJavaDir = "$buildDir/generated/java"
@@ -23,8 +23,12 @@ def generatedJavaDir = "$buildDir/generated/java"
allprojects { allprojects {
repositories { repositories {
maven { maven {
url "${nexusUrl}/repository/maven-public/" url "${reposiliteUrl}/private"
allowInsecureProtocol = true allowInsecureProtocol = true
credentials {
username = reposiliteUser
password = reposilitePassword
}
} }
} }
} }
@@ -125,7 +129,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.94.Final" implementation "io.netty:netty-all:4.1.0.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"
@@ -201,13 +205,14 @@ 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'
@@ -227,11 +232,6 @@ 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'
} }
} }
+4 -1
View File
@@ -1,4 +1,7 @@
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
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
+207 -17
View File
@@ -9,7 +9,7 @@
## 기술 스택 ## 기술 스택
- Spring Framework 5.3.27 - Spring Framework 5.3.27
- Java 17 - Java 8
- Gradle 8.7 - Gradle 8.7
- Oracle 19c - Oracle 19c
- Spring Data JPA 2.5.2 - Spring Data JPA 2.5.2
@@ -23,61 +23,251 @@
### 사전 요구사항 ### 사전 요구사항
- JDK 17 이상 - JDK 8 이상
- 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 wrapper(`./gradlew`) 를 사용합니다. 사용 환경에 따라 적절한 gradle 명령어를 설정하세요:
**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
# 표준 빌드 # 표준 빌드
./gradlew build $GRADLE_CMD build
# Weblogic 배포용 빌드 (테스트 제외) # Weblogic 배포용 빌드 (테스트 제외)
./gradlew build -x test -Pprofile=weblogic $GRADLE_CMD build -x test -Pprofile=weblogic
# WAR 파일 빌드[ems_stdout.log](../../logs/emsSvr-RinjaeMA/ems_stdout.log) # WAR 파일 빌드
./gradlew war $GRADLE_CMD war
# 클린 빌드 # 클린 빌드
./gradlew clean build $GRADLE_CMD clean build
# 테스트 실행 # 테스트 실행
./gradlew test $GRADLE_CMD test
# 패키징 없이 클래스만 컴파일 # 패키징 없이 클래스만 컴파일
./gradlew classes $GRADLE_CMD classes
# QueryDSL Q-classes 및 기타 애노테이션 생성 # QueryDSL Q-classes 및 기타 애노테이션 생성
./gradlew compileJava $GRADLE_CMD compileJava
# 의존성 트리 보기 # 의존성 트리 보기
./gradlew dependencies $GRADLE_CMD dependencies
# 사용 가능한 모든 태스크 목록 # 사용 가능한 모든 태스크 목록
./gradlew tasks --all $GRADLE_CMD tasks --all
``` ```
**주의**: gradlew 사용 금지 - offline gradle 단독 실행
## 프로젝트 구조 ## 프로젝트 구조
### 전체 프로젝트 구조 ### 전체 프로젝트 구조
@@ -251,11 +251,6 @@ public interface MonitoringContext {
public static final String MENU_RENDER_ADDITIONAL_SERVICES = "menu.render.additional.services"; public static final String MENU_RENDER_ADDITIONAL_SERVICES = "menu.render.additional.services";
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);
@@ -1,18 +1,15 @@
package com.eactive.eai.rms.common.interceptor; package com.eactive.eai.rms.common.interceptor;
import javax.servlet.http.HttpServletRequest; import com.eactive.eai.rms.common.login.LoginVo;
import javax.servlet.http.HttpServletResponse; import com.eactive.eai.rms.common.login.SessionManager;
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 com.eactive.eai.rms.common.context.MonitoringContext; import javax.servlet.http.HttpServletRequest;
import com.eactive.eai.rms.common.login.LoginVo; import javax.servlet.http.HttpServletResponse;
import com.eactive.eai.rms.common.login.SessionManager;
public class SessionCheckInterceptor extends HandlerInterceptorAdapter { public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
@@ -23,8 +20,6 @@ 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,
@@ -44,7 +39,6 @@ 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;
@@ -52,7 +46,7 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
SessionManager.setLoginVo(loginVo); // ThreadLocal에 설정 SessionManager.setLoginVo(loginVo); // ThreadLocal에 설정
// 이중 로그인 검사 // 이중 로그인 검사
if (!SessionManager.isValidSession(request, isDualLogin)) { if (!SessionManager.isValidSession(request)) {
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,24 +391,20 @@ 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 (!isDualLogin) { if (!isNewLogin) {
// 이중 로그인 확인
boolean isNewLogin = SessionManager.registerUserSession(request, dto); // 1.
SessionManager.forceRegisterUserSession(request, dto);
if (!isNewLogin) { session.setAttribute("dualLogin", "Y");
// 1. // 또는
SessionManager.forceRegisterUserSession(request, dto); // 2. 이중 로그인 에러 표시
session.setAttribute("dualLogin", "Y"); // model.addAttribute("error", "다른 기기에서 이미 로그인 중입니다.");
// return "/";
// 또는 }
// 2. 이중 로그인 에러 표시
// model.addAttribute("error", "다른 기기에서 이미 로그인 중입니다.");
// return "/";
}
}
// 세션에 로그인 정보 저장 // 세션에 로그인 정보 저장
request.getSession().setAttribute(CommonConstants.LOGIN, dto); request.getSession().setAttribute(CommonConstants.LOGIN, dto);
@@ -669,8 +665,7 @@ 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,7 +27,6 @@ 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) {
@@ -35,8 +34,7 @@ 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,7 +4,6 @@ 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
@@ -207,38 +206,23 @@ 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, boolean isDualLogin) { public static boolean isValidSession(HttpServletRequest request) {
LoginVo loginVo = getLoginVo(request); LoginVo loginVo = getLoginVo(request);
if (loginVo == null) { if (loginVo == null) {
return false; return false;
} }
if (isDualLogin) { String userId = loginVo.getUserId();
return true; String currentSessionId = request.getSession().getId();
} else { String registeredSessionId = loggedInUsers.get(userId);
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,11 +1,13 @@
package com.eactive.eai.rms.common.scheduler; package com.eactive.eai.rms.common.scheduler;
import java.text.SimpleDateFormat; import com.eactive.eai.rms.common.base.BaseService;
import java.util.Date; import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper;
import java.util.List; import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI;
import java.util.stream.Collectors; import com.eactive.eai.rms.common.scheduler.ui.ScheduleInfo;
import java.util.stream.StreamSupport; 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;
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;
@@ -18,14 +20,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 com.eactive.eai.rms.common.base.BaseService; import java.util.List;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper; import java.util.stream.Collectors;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI; import java.util.stream.StreamSupport;
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")
@@ -125,10 +122,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(dateFormat(triggersOfJob.get(0).getStartTime())); result.setStartTime(String.valueOf(triggersOfJob.get(0).getStartTime()));
result.setEndTime(dateFormat(triggersOfJob.get(0).getEndTime())); result.setEndTime(String.valueOf(triggersOfJob.get(0).getEndTime()));
result.setPreviousFireTime(dateFormat(triggersOfJob.get(0).getPreviousFireTime())); result.setPreviousFireTime(String.valueOf(triggersOfJob.get(0).getPreviousFireTime()));
result.setNextFireTime(dateFormat(triggersOfJob.get(0).getNextFireTime())); result.setNextFireTime(String.valueOf(triggersOfJob.get(0).getNextFireTime()));
} }
} catch (Exception e) { } catch (Exception e) {
@@ -153,14 +150,5 @@ public class SchedulerManService extends BaseService {
public void delete(String jobName) { public void delete(String jobName) {
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,6 +1,5 @@
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;
@@ -192,20 +191,6 @@ 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 로 변환한다.
@@ -10,9 +10,6 @@ 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> {
@@ -22,10 +19,6 @@ 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()));
} }
@@ -35,12 +28,7 @@ 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);
} }
} }
@@ -5,7 +5,6 @@ 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;
@@ -1,9 +1,6 @@
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;
@@ -16,7 +13,6 @@ 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;
@@ -37,15 +33,10 @@ 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;
@@ -133,7 +124,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(INTERFACE_TYPE_INFLOW).and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname))) .on(qInflowControl.id.type.eq("02").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())
@@ -152,8 +143,6 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
.fetchOne(); .fetchOne();
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) {
@@ -178,109 +167,13 @@ 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(INTERFACE_TYPE_INFLOW).and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname))) .on(qInflowControl.id.type.eq("02").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;
@@ -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 * totalCnt) / SUM(totalCnt) // 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
Expressions.cases() Expressions.cases()
.when(q.totalCnt.sum().gt(0)) .when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum() .then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -211,10 +211,11 @@ 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.totalCnt.sum().gt(0)) .when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum() .then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -292,9 +293,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.totalCnt.sum().gt(0)) .when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum() .then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -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 * totalCnt) / SUM(totalCnt) // 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
Expressions.cases() Expressions.cases()
.when(q.totalCnt.sum().gt(0)) .when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum() .then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -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 * totalCnt) / SUM(totalCnt) // 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
Expressions.cases() Expressions.cases()
.when(q.totalCnt.sum().gt(0)) .when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum() .then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .divide(q.successCnt.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.totalCnt.sum().gt(0)) .when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum() .then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -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 * totalCnt) / SUM(totalCnt) // 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
Expressions.cases() Expressions.cases()
.when(q.totalCnt.sum().gt(0)) .when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum() .then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .divide(q.successCnt.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.totalCnt.sum().gt(0)) .when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum() .then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class)) .divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -1,13 +0,0 @@
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);
}
@@ -1,38 +0,0 @@
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);
}
}
@@ -1,10 +0,0 @@
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> {
}
@@ -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.orderSeq.asc(), q.isKey.desc(), q.keySeq.asc(), q.columnName.asc()).iterator().forEachRemaining(list::add); repository.findAll(q.isKey.desc(), q.keySeq.asc(), q.columnName.asc()).iterator().forEachRemaining(list::add);
return list; return list;
} }
@@ -1,42 +0,0 @@
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; }
}
@@ -1,7 +0,0 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
public interface ApiStatusEvent {
String getEaisvcname();
String getEaisvcdesc();
String getEvent();
}
@@ -1,72 +0,0 @@
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
);
}
@@ -1,69 +0,0 @@
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;
}
}
}
@@ -1,7 +0,0 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
public interface InflowTokenInsufficient {
String getEaisvcname();
String getEaisvcdesc();
Long getCnt();
}
@@ -1,47 +0,0 @@
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; }
}
@@ -1,21 +0,0 @@
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;
}
@@ -1,18 +0,0 @@
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);
}
@@ -1,37 +0,0 @@
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));
}
}
}
@@ -1,256 +0,0 @@
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();
}
}
@@ -1,12 +0,0 @@
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> {
}
@@ -1,211 +0,0 @@
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);
}
}
@@ -1,74 +0,0 @@
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);
}
}
@@ -1,25 +0,0 @@
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;
}
}
@@ -1,40 +0,0 @@
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());
}
}
@@ -1,18 +0,0 @@
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());
}
}
@@ -1,19 +0,0 @@
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);
}
}
@@ -1,144 +0,0 @@
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;
}
}
@@ -1,175 +0,0 @@
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);
}
}
@@ -1,101 +0,0 @@
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);
}
}
@@ -1,87 +0,0 @@
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";
}
}
@@ -1,17 +0,0 @@
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: 알림 발송 로직
}
}
@@ -1,131 +0,0 @@
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;
}
}
@@ -1,31 +0,0 @@
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();
}
}
@@ -1,13 +0,0 @@
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;
}
@@ -1,31 +0,0 @@
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);
}
@@ -1,144 +0,0 @@
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();
}
}
@@ -1,17 +0,0 @@
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() 호출
}
}
@@ -1,144 +0,0 @@
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,7 +14,6 @@ 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));
} }
@@ -23,6 +22,6 @@ public class PortalApprovalAuthorizer implements ApprovalAuthorizer {
} }
private String getApproverId(Approver approver) { private String getApproverId(Approver approver) {
return approver.getUser() != null ? approver.getUser().getId() : null; return approver.getUser().getId();
} }
} }
@@ -13,16 +13,10 @@ 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 {
@@ -30,8 +24,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(Model model) { public void view() {
model.addAttribute("hardDeleteEnabled", portalApprovalManService.isHardDeleteEnabled()); // view
} }
@GetMapping(value = "/onl/apim/approval/portalApprovalMan.view", params = "cmd=DETAIL") @GetMapping(value = "/onl/apim/approval/portalApprovalMan.view", params = "cmd=DETAIL")
@@ -82,46 +76,4 @@ 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,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.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;
@@ -26,7 +25,6 @@ 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;
@@ -38,8 +36,6 @@ 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
@@ -47,9 +43,6 @@ 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;
@@ -66,10 +59,6 @@ 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);
@@ -341,28 +330,6 @@ 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);
@@ -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.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;
@@ -18,8 +19,10 @@ 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;
@@ -32,12 +35,22 @@ 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
@@ -103,15 +116,9 @@ 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(requester.getMobileNumber()); recipient.setPhone(appRequest.getApproval().getRequester().getMobileNumber());
recipient.setUserId(requester.getEmailAddr()); recipient.setUserId(appRequest.getApproval().getRequester().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);
@@ -158,8 +165,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() != null ? appRequest.getOrg().getId() : null); credentialUI.setOrgid(appRequest.getOrg().getId());
credentialUI.setOrgname(appRequest.getOrg() != null ? appRequest.getOrg().getOrgName() : null); credentialUI.setOrgname(appRequest.getOrg().getOrgName());
credentialUI.setAllowedips(appRequest.getIpWhitelist()); credentialUI.setAllowedips(appRequest.getIpWhitelist());
credentialUI.setRedirecturi(appRequest.getCallbackUrl()); credentialUI.setRedirecturi(appRequest.getCallbackUrl());
credentialUI.setAppIconFileId(appRequest.getAppIconFileId()); credentialUI.setAppIconFileId(appRequest.getAppIconFileId());
@@ -1,8 +1,6 @@
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;
@@ -18,16 +16,12 @@ 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() {
@@ -46,14 +40,6 @@ public class PortalNoticeManController extends BaseAnnotationController {
Page<PortalNoticeUI> page = portalNoticeManService.selectList(pageable, portalNoticeUISearch); Page<PortalNoticeUI> page = portalNoticeManService.selectList(pageable, portalNoticeUISearch);
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) {
@@ -93,9 +93,8 @@ 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);
@@ -124,9 +123,8 @@ 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());
@@ -32,11 +32,6 @@ public class PortalNoticeUI {
private String useYn; private String useYn;
private String inquirerName; private String inquirerName;
private String fixYn;
private String noticeType;
private String createdBy; private String createdBy;
@@ -27,8 +27,6 @@ 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;
@@ -50,9 +48,6 @@ 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));
@@ -151,37 +146,19 @@ public class PortalOrgManService extends BaseService {
throw new BizException("삭제(탈퇴) 상태인 법인만 완전삭제할 수 있습니다."); throw new BizException("삭제(탈퇴) 상태인 법인만 완전삭제할 수 있습니다.");
} }
// 사용자(PortalUser)는 별도 화면에서 정리해야 하므로 cascade 하지 않고 차단 List<String> dependencies = new ArrayList<>();
long userCount = portalUserService.countByOrgId(id); long userCount = portalUserService.countByOrgId(id);
if (userCount > 0) { if (userCount > 0) dependencies.add("사용자 " + userCount + "");
throw new BizException("의존 데이터가 존재하여 완전삭제할 수 없습니다: 사용자 " + userCount + ""); long appRequestCount = portalOrgService.countAppRequestByOrgId(id);
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);
} }
@@ -0,0 +1,79 @@
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;
}
}
@@ -1,125 +0,0 @@
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);
}
}
@@ -1,64 +0,0 @@
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);
}
}
@@ -1,71 +0,0 @@
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;
}
@@ -1,18 +0,0 @@
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);
}
@@ -1,86 +0,0 @@
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);
}
}
@@ -20,7 +20,6 @@ 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")
@@ -93,38 +92,6 @@ 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,12 +249,7 @@ 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; String inboundRestPath = StringUtils.substringAfter(stdKey, STEMSG_SERVICE_URL_DELIMITER);
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());
@@ -262,11 +257,7 @@ 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)) {
if (StringUtils.contains(stdKey, STEMSG_SERVICE_URL_DELIMITER)) { inboundHttpMethod = StringUtils.substringBetween(stdKey, STEMSG_METHOD_DELIMITER, STEMSG_SERVICE_HEADER_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<>();
@@ -19,7 +19,7 @@ public class StandardMessageInfoQueryServiceForApi extends AbstractDataService<S
} }
public void deleteByEaiSvcName(String eaiSvcName){ public void deleteByEaiSvcName(String eaiSvcName){
repository.findByEaisvcname(eaiSvcName).ifPresent(repository::delete); repository.deleteByEaisvcname(eaiSvcName);
} }
public void existsByApiFullPathAndEaiSvcNameNot(String apiFullPath, String eaiServiceName) { public void existsByApiFullPathAndEaiSvcNameNot(String apiFullPath, String eaiServiceName) {
@@ -36,7 +36,6 @@ 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")
@@ -58,7 +57,6 @@ 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")
@@ -167,9 +165,6 @@ 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)) {
@@ -2,10 +2,6 @@ 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;
@@ -13,9 +9,7 @@ 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);
// @Modifying void deleteByEaisvcname(String eaiSvcName);
// @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,7 +43,6 @@ 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,7 +15,6 @@ 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,62 +33,12 @@ public class ApiStatsAggregationController {
private final HourlyToDailyAggregationJob hourlyToDailyAggregationJob; private final HourlyToDailyAggregationJob hourlyToDailyAggregationJob;
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.totalCnt.sum().gt(0)) .when(qd.successCnt.sum().gt(0))
.then(qd.avgRespTime.multiply(qd.totalCnt).sum() .then(qd.avgRespTime.multiply(qd.successCnt).sum()
.divide(qd.totalCnt.sum()).castToNum(BigDecimal.class)) .divide(qd.successCnt.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.totalCnt.sum().gt(0)) .when(qh.successCnt.sum().gt(0))
.then(qh.avgRespTime.multiply(qh.totalCnt).sum() .then(qh.avgRespTime.multiply(qh.successCnt).sum()
.divide(qh.totalCnt.sum()).castToNum(BigDecimal.class)) .divide(qh.successCnt.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.totalCnt.sum().gt(0)) .when(qm.successCnt.sum().gt(0))
.then(qm.avgRespTime.multiply(qm.totalCnt).sum() .then(qm.avgRespTime.multiply(qm.successCnt).sum()
.divide(qm.totalCnt.sum()).castToNum(BigDecimal.class)) .divide(qm.successCnt.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"),
@@ -36,8 +36,9 @@ 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응답시간"
}; };
/** /**
@@ -192,13 +193,17 @@ 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,8 +18,6 @@ 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,7 +14,6 @@ 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;
@@ -23,13 +22,15 @@ 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