38 Commits

Author SHA1 Message Date
Rinjae fc70560f05 공지사항 관리 - 장애/점검 등록 UI 추가
- PortalNoticeUI: 장애/점검 필드 신설 (시작/종료/상태/영향API)
- PortalNoticeManService: djb_apistatus_incident 연동 분기
- 등록 폼: 동적 유형별 UI + 영향 API 팝업 (showModal)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 09:11:06 +09:00
Rinjae 43fe63964f Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/com/eactive/eai/rms/onl/apim/approval/PortalApprovalManController.java
2026-06-12 16:08:41 +09:00
Rinjae e51331986d Merge remote-tracking branch 'origin/master' 2026-06-09 19:08:38 +09:00
eastargh 1aa40f2c61 UMS관련 정리 2026-06-09 15:39:05 +09:00
eastargh c8caeeda8c 유량제어 모니터링 2026-06-09 13:04:32 +09:00
eastargh 06ad1230b2 유량제어 모니터링 2026-06-09 11:34:10 +09:00
eastargh 8109f91aa2 유량제어 현황 모니터링 기능 추가 2026-06-09 09:51:05 +09:00
eastargh 62ccebd773 Q&A 7일후 자동 종료 배치 2026-06-08 17:19:04 +09:00
eastargh ec8d6bfd7b Q&A 게시판 댓글 작성 기능 2026-06-05 17:41:37 +09:00
eastargh 480d7dac5a 앱승인시 예상완료일 항목 추가 & 승인요청 상세 조회시 "진행중"으로 상태변경 2026-06-05 15:10:06 +09:00
eastargh feb4f845a8 팝업 두번째 이후 오픈시 선택버그 수정 2026-06-05 10:11:21 +09:00
eastargh 7857f5b45c 자동 Log-off 기능 추가 2026-06-04 18:11:31 +09:00
eastargh 68c8e18e8d 스윙채 & webhook 발송 수정 2026-06-04 17:38:52 +09:00
eastargh 35e5564252 webhook 발송 2026-06-02 09:43:03 +09:00
eastargh 0d7d006f92 api 상태 변화 감지 & 웹훅 발송 2026-06-01 17:37:06 +09:00
eastargh 737b493f7c API사용현황 통계 2026-05-28 16:02:52 +09:00
eastargh af0a878a39 DB스키마변경 BAPADM->BAPAPP 2026-05-28 11:32:31 +09:00
eastargh b639e29039 DB스키마 변경 : AGWADM->AGWAPP, EMSADM->EMSAPP 2026-05-28 10:53:33 +09:00
eastargh 350297aaa2 클라이언트 유량제어 테이블 수정 2026-05-27 16:54:19 +09:00
Rinjae 23ba7993c3 Merge remote-tracking branch 'origin/master' 2026-05-21 15:16:41 +09:00
eastargh 7677e3b26e 거래로그 집계시 공통코드 테이블 변경 2026-05-21 15:10:24 +09:00
Rinjae 90c43ea7b0 - K8sUtil 파일 삭제
- 승인관리 완전삭제 기능 추가
- DJBank로 명칭 업데이트
2026-05-21 09:51:39 +09:00
eastargh 191857811f 공지사항 첨부파일 안씀 2026-05-20 11:50:40 +09:00
eastargh 88ffc1cc28 API 사용현황 신규개발 2026-05-20 11:45:00 +09:00
eastargh d3bd4acb62 요청/응답구분 선택 버그 수정 2026-05-19 10:57:48 +09:00
eastargh 1f7166502f API통계 업무오류 제외 2026-05-15 14:53:52 +09:00
eastargh 91f36605e8 스케쥴러 실행시간 format 변경 2026-05-14 11:22:47 +09:00
eastargh 0ebd31e256 API 통계 화면 리뉴얼 2026-05-14 11:21:14 +09:00
eastargh 45c6edbbeb 거래로그>시간별 수동집계 기능 추가 2026-05-14 11:20:33 +09:00
eastargh 335c7ec53a 거래로그 > 시간별 집계하는 배치잡 2026-05-14 11:19:55 +09:00
eastargh 0b341b85f1 평균응답속도 계산시 분모를 성공건수에서 총건수로 변경 2026-05-14 11:17:55 +09:00
eastargh e9ac62d3ed 인터페이스 상세 저장 버그 수정 2026-05-12 10:37:32 +09:00
eastargh 133c05f1d7 알림 발송 관련 배치 리네이밍 2026-05-08 17:24:17 +09:00
eastargh 4a1a4be54d 검색조건 유지 2026-05-08 10:37:57 +09:00
curry772 dbd36f28d4 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin 2026-05-07 15:39:27 +09:00
curry772 4ee50be000 Merge branch 'feature/crypto-module' into master 2026-05-07 15:39:13 +09:00
eastargh 9d55db3281 게시판관리 - 게시유형, 고정여부 추가 2026-05-07 09:50:49 +09:00
eastargh 9a5d5e4815 클라이언트 유량제어 신규생성 2026-05-07 09:39:59 +09:00
131 changed files with 5513 additions and 2064 deletions
+3
View File
@@ -246,3 +246,6 @@ eapim-admin-kjb/
*.report.md *.report.md
*.claude.md *.claude.md
tmpclaude-*-cwd tmpclaude-*-cwd
logs
rinjae_
tomcat-base
+36 -28
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에서 엔티티를 찾을 수 없다고 표시되면, `kjb-gradle.sh compileJava`를 먼저 실행하여 코드를 생성해야 합니다. - **QueryDSL 코드 생성**: 빌드 시 `QueryDSL`을 사용하여 Q-class가 자동으로 생성됩니다. IDE에서 엔티티를 찾을 수 없다고 표시되면, `./gradlew compileJava` 를 먼저 실행하여 코드를 생성해야 합니다.
- **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다. - **고객사별 커스터마이징**: `ext.kjb` 또는 `custom` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다.
## 프로젝트 개요 ## 프로젝트 개요
@@ -39,50 +39,48 @@ eLink EMS (eLink Management System)는 eLink의 웹 기반 관리 서비스로,
### 빌드 명령어 ### 빌드 명령어
**⚠️ 중요**: 환경변수 문제로 인해 `gradle` 대신 `kjb-gradle.sh` 스크립트를 사용해야 합니다. 프로젝트 루트의 Gradle wrapper(`./gradlew`)를 사용합니다. JDK 8 toolchain 위치는 `.envrc` 등으로 잡습니다(아래 "기술 스택" 섹션 참조).
- 스크립트 위치: `/c/eactive/workspaces/shell-scripts/kjb-gradle.sh`
- PATH에 등록되어 있으므로 바로 `kjb-gradle.sh` 명령어 사용 가능
```bash ```bash
# 표준 빌드 # 표준 빌드
kjb-gradle.sh build ./gradlew build
# Weblogic 배포용 빌드 (테스트 제외) # Weblogic 배포용 빌드 (테스트 제외)
kjb-gradle.sh build -x test -Pprofile=weblogic ./gradlew build -x test -Pprofile=weblogic
# WAR 파일 빌드 # WAR 파일 빌드
kjb-gradle.sh war ./gradlew war
# 클린 빌드 # 클린 빌드
kjb-gradle.sh clean build ./gradlew clean build
``` ```
### 테스트 실행 ### 테스트 실행
```bash ```bash
# 모든 테스트 실행 # 모든 테스트 실행
kjb-gradle.sh test ./gradlew test
# 특정 테스트 클래스 실행 # 특정 테스트 클래스 실행
kjb-gradle.sh test --tests "com.example.ClassName" ./gradlew test --tests "com.example.ClassName"
# 특정 테스트 패키지 실행 (JUnit 플랫폼 사용) # 특정 테스트 패키지 실행 (JUnit 플랫폼 사용)
kjb-gradle.sh test --tests "com.eactive.eai.rms.*" ./gradlew test --tests "com.eactive.eai.rms.*"
``` ```
### 개발 태스크 ### 개발 태스크
```bash ```bash
# 패키징 없이 클래스만 컴파일 # 패키징 없이 클래스만 컴파일
kjb-gradle.sh classes ./gradlew classes
# QueryDSL Q-classes 및 기타 애노테이션 생성 # QueryDSL Q-classes 및 기타 애노테이션 생성
kjb-gradle.sh compileJava ./gradlew compileJava
# 의존성 트리 보기 # 의존성 트리 보기
kjb-gradle.sh dependencies ./gradlew dependencies
# 사용 가능한 모든 태스크 목록 # 사용 가능한 모든 태스크 목록
kjb-gradle.sh tasks --all ./gradlew tasks --all
``` ```
## 로컬 개발 빠른 시작 ## 로컬 개발 빠른 시작
@@ -92,26 +90,26 @@ kjb-gradle.sh tasks --all
1. **IDE 설정 파일 생성 (최초 1회)** 1. **IDE 설정 파일 생성 (최초 1회)**
```bash ```bash
# Eclipse 사용 시 (기본) # Eclipse 사용 시 (기본)
kjb-gradle.sh eclipse ./gradlew eclipse
# IntelliJ IDEA 사용 시 (build.gradle.intellij 파일 사용 필요) # IntelliJ IDEA 사용 시 (build.gradle.intellij 파일 사용 필요)
# 1. build.gradle을 build.gradle.eclipse로 백업 # 1. build.gradle을 build.gradle.eclipse로 백업
# 2. build.gradle.intellij를 build.gradle로 복사 # 2. build.gradle.intellij를 build.gradle로 복사
# 3. kjb-gradle.sh idea 실행 # 3. ./gradlew idea 실행
``` ```
2. **QueryDSL 등 소스 코드 생성** 2. **QueryDSL 등 소스 코드 생성**
```bash ```bash
kjb-gradle.sh compileJava ./gradlew compileJava
``` ```
3. **전체 빌드 및 테스트 실행** 3. **전체 빌드 및 테스트 실행**
```bash ```bash
kjb-gradle.sh build ./gradlew build
``` ```
4. **IDE에서 프로젝트 열기** 4. **IDE에서 프로젝트 열기**
- Eclipse: `kjb-gradle.sh eclipse` 실행 후, IDE에서 프로젝트를 엽니다. - Eclipse: `./gradlew eclipse` 실행 후, IDE에서 프로젝트를 엽니다.
- IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다. - IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다.
5. **실행 구성(Run Configuration) 설정** 5. **실행 구성(Run Configuration) 설정**
@@ -162,14 +160,14 @@ eapim-admin (root project)
#### Q-Class 생성 위치 #### Q-Class 생성 위치
Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다. Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다.
- `kjb-gradle.sh compileJava` 또는 `kjb-gradle.sh build` 실행 시 Gradle이 자동으로 생성 - `./gradlew compileJava` 또는 `./gradlew build` 실행 시 Gradle이 자동으로 생성
- 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다 - 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다
#### 생성된 코드 작업하기 #### 생성된 코드 작업하기
- **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다 - **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다
- **업데이트 pull 후**: `kjb-gradle.sh compileJava`를 실행하여 Q-classes를 재생성합니다 - **업데이트 pull 후**: `./gradlew compileJava` 를 실행하여 Q-classes를 재생성합니다
- **Q-classes IDE 오류 시**: `kjb-gradle.sh clean compileJava`로 재생성합니다 - **Q-classes IDE 오류 시**: `./gradlew clean compileJava` 로 재생성합니다
#### IDE별 빌드 설정 파일 #### IDE별 빌드 설정 파일
@@ -255,12 +253,21 @@ IntelliJ를 사용하려면 `build.gradle.intellij`를 `build.gradle`로 교체
WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다: WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다:
```bash ```bash
kjb-gradle.sh build -x test -Pprofile=weblogic ./gradlew build -x test -Pprofile=weblogic
``` ```
## 기술 스택 ## 기술 스택
- **Java 8**: Gradle toolchain을 통해 강제 - **Java 8**: Gradle toolchain을 통해 강제 (`JavaLanguageVersion.of(8)`). 프로젝트의 `gradle.properties` 에는 JDK 경로를 기입하지 않습니다 — 개인 환경별로 `.envrc`(direnv) 또는 셸 rc 파일에서 `JAVA_HOME` 을 JDK 8 위치로 export 하세요. Gradle 8 의 toolchain auto-detect 가 `JAVA_HOME` 도 후보 경로로 인식합니다.
```bash
# 예: 프로젝트 루트의 .envrc (Zulu 8 / macOS Apple Silicon)
JAVA_HOME=${HOME}/opts/jdks/zulu8.94.0.17-ca-jdk8.0.492-macosx_aarch64
GRADLE_USER_HOME=${HOME}/eactive/djb-eapim/gradle-home
export PATH=${JAVA_HOME}/bin:${PATH}
```
Gradle 표준 위치(`/Library/Java/JavaVirtualMachines/...`, Homebrew, SDKMAN 등)에 설치한 경우는 별도 설정 없이도 auto-detect 됩니다.
- **Spring Framework 5.3.27**: 코어 프레임워크 (MVC, Data JPA) - **Spring Framework 5.3.27**: 코어 프레임워크 (MVC, Data JPA)
- **Spring Data JPA 2.5.2**: 리포지토리 추상화 - **Spring Data JPA 2.5.2**: 리포지토리 추상화
- **Hibernate 5.4.33/5.6.15**: ORM 구현 (JPA/ORM) - **Hibernate 5.4.33/5.6.15**: ORM 구현 (JPA/ORM)
@@ -271,9 +278,10 @@ kjb-gradle.sh build -x test -Pprofile=weblogic
- **Jackson 2.13.1**: JSON 처리 - **Jackson 2.13.1**: JSON 처리
- **Logback 1.2.10**: 로깅 프레임워크 (SLF4J와 함께) - **Logback 1.2.10**: 로깅 프레임워크 (SLF4J와 함께)
- **AWS SDK 2.20.142**: S3 연동 - **AWS SDK 2.20.142**: S3 연동
- **Kubernetes Client 18.0.1**: K8s 관리
- **EhCache**: 캐싱, **Lombok**: 코드 생성, **MapStruct**: 객체 매핑 - **EhCache**: 캐싱, **Lombok**: 코드 생성, **MapStruct**: 객체 매핑
> Kubernetes Client 의존성은 JDK 8 호환 문제로 제거됨(`io.kubernetes:client-java:18.0.1` 은 JDK 11+ 필요). K8s 모드 기능이 필요하면 `≤13.0.2` 로 다운그레이드 후 재도입 검토.
## 프로젝트 규칙 ## 프로젝트 규칙
### 패키지 구조 ### 패키지 구조
@@ -376,7 +384,7 @@ com.eactive.eai
### IntelliJ IDEA (권장) ### IntelliJ IDEA (권장)
```bash ```bash
kjb-gradle.sh idea ./gradlew idea
``` ```
또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다. 또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다.
@@ -395,7 +403,7 @@ cp build.gradle build.gradle.intellij
cp build.gradle.eclipse build.gradle cp build.gradle.eclipse build.gradle
# 3. Eclipse 프로젝트 생성 # 3. Eclipse 프로젝트 생성
kjb-gradle.sh eclipse ./gradlew eclipse
``` ```
Eclipse 전용 설정(`build.gradle.eclipse`)은 다음을 포함합니다: Eclipse 전용 설정(`build.gradle.eclipse`)은 다음을 포함합니다:
@@ -25,31 +25,31 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
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"
p:text="BAP" p:text="BAP"
p:schema="BAPADM" p:schema="BAPAPP"
p:dynamic="true" p:dynamic="true"
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"
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -25,31 +25,31 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
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"
<!-- p:text="BAP"--> p:text="BAP"
<!-- p:schema="BAPADM"--> p:schema="BAPAPP"
<!-- p:dynamic="true"--> p:dynamic="true"
<!-- 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"
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -25,7 +25,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
@@ -37,7 +37,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP" p:name="BAP"
p:text="BAP" p:text="BAP"
p:schema="BAPADM" p:schema="BAPAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_BAP" p:jndiName="jdbc/dsOBP_BAP"
@@ -49,7 +49,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -25,7 +25,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW" p:name="APIGW"
p:text="APIGW" p:text="APIGW"
p:schema="AGWADM" p:schema="AGWAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_AGW" p:jndiName="jdbc/dsOBP_AGW"
@@ -37,7 +37,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP" p:name="BAP"
p:text="BAP" p:text="BAP"
p:schema="BAPADM" p:schema="BAPAPP"
p:dynamic="true" p:dynamic="true"
p:online="true" p:online="true"
p:jndiName="jdbc/dsOBP_BAP" p:jndiName="jdbc/dsOBP_BAP"
@@ -49,7 +49,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType" class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING" p:name="MONITORING"
p:text="MONITORING" p:text="MONITORING"
p:schema="EMSADM" p:schema="EMSAPP"
p:dynamic="false" p:dynamic="false"
p:online="false" p:online="false"
p:jndiName="jdbc/dsOBP_EMS" p:jndiName="jdbc/dsOBP_EMS"
@@ -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>
@@ -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>
@@ -69,6 +69,7 @@
<context:component-scan base-package="com.eactive.apim.portal" /> <context:component-scan base-package="com.eactive.apim.portal" />
<context:component-scan base-package="com.eactive.ext.kjb.statistics" /> <context:component-scan base-package="com.eactive.ext.kjb.statistics" />
<context:component-scan base-package="com.eactive.ext.djb" />
<bean id="cacheManager" <bean id="cacheManager"
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr11 inst.Name=emsSvr11
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
eai.systemmode=D eai.systemmode=D
eai.drmode=N eai.drmode=N
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr96 inst.Name=emsSvr96
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
eai.systemmode=L eai.systemmode=L
eai.drmode=N eai.drmode=N
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr11 inst.Name=emsSvr11
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
eai.systemmode=P eai.systemmode=P
eai.drmode=N eai.drmode=N
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option # Priority of the was -D option
inst.Name=emsSvr11 inst.Name=emsSvr11
eai.tableowner=EMSADM eai.tableowner=EMSAPP
# P/T/D/S # P/T/D/S
eai.systemmode=T eai.systemmode=T
eai.drmode=N eai.drmode=N
+12 -9
View File
@@ -9,15 +9,7 @@
<param-name>contextConfigLocation</param-name> <param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value> <param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param> </context-param>
<!-- XSS Filter -->
<filter>
<filter-name>CrossScriptingFilter</filter-name>
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CrossScriptingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter> <filter>
<filter-name>encodingFilter</filter-name> <filter-name>encodingFilter</filter-name>
@@ -57,6 +49,17 @@
<url-pattern>*.json</url-pattern> <url-pattern>*.json</url-pattern>
</filter-mapping> </filter-mapping>
<!-- XSS Filter -->
<filter>
<filter-name>CrossScriptingFilter</filter-name>
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CrossScriptingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- <!--
<filter> <filter>
<filter-name>MonitoringFrontFilter</filter-name> <filter-name>MonitoringFrontFilter</filter-name>
+8
View File
@@ -36,6 +36,14 @@
// save the returnValue in options so that it is available in the callback function // save the returnValue in options so that it is available in the callback function
optns.returnValue = $frame[0].contentWindow.window.returnValue; optns.returnValue = $frame[0].contentWindow.window.returnValue;
optns.onClose(); optns.onClose();
// Remove iframe from DOM so window.frames doesn't accumulate stale entries;
// without this, a second showModal call leaves frames[0] pointing to the first
// (closed) iframe, causing the previous returnValue to bleed into the next callback.
var $dialogEl = $frame.closest('.ui-dialog');
setTimeout(function() {
$frame.remove();
if ($dialogEl.length) $dialogEl.remove();
}, 0);
if(optns.option && optns.option.fullScreenPopup){ if(optns.option && optns.option.fullScreenPopup){
$('iframe[name=topFrame]',window.parent.document).css('display', 'block'); $('iframe[name=topFrame]',window.parent.document).css('display', 'block');
+4
View File
@@ -30,6 +30,10 @@
<script language="javascript" src="<c:url value="/js/jquery.contextMenu.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.contextMenu.js"/>"></script>
<script language="javascript" src="<c:url value="/js/custom.js"/>"></script> <script language="javascript" src="<c:url value="/js/custom.js"/>"></script>
<script language="javascript" src="<c:url value="/js/common-${locale}.js"/>"></script> <script language="javascript" src="<c:url value="/js/common-${locale}.js"/>"></script>
<script type="text/javascript">
/* 자동 로그아웃: iframe 내 활동을 sessionStorage에 기록 (main.jsp 타이머와 공유) */
(function() { var m = function() { sessionStorage.setItem('lastActivity', String(Date.now())); }; ['mousemove','keydown','click','touchstart'].forEach(function(e){ document.addEventListener(e, m, true); }); })();
</script>
<script language="javascript" src="<c:url value="/js/jquery.inputmask.bundle.min.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.inputmask.bundle.min.js"/>"></script>
<script language="javascript" src="<c:url value="/js/jquery.searchabledropdown-1.0.8.min.js"/>"></script> <script language="javascript" src="<c:url value="/js/jquery.searchabledropdown-1.0.8.min.js"/>"></script>
<%-- <script language="javascript" src="<c:url value="/js/jquery.searchable-1.1.0.min.js"/>"></script> --%> <%-- <script language="javascript" src="<c:url value="/js/jquery.searchable-1.1.0.min.js"/>"></script> --%>
@@ -31,19 +31,19 @@ function init(){
new makeOptions("CODE","NAME").setObj($("select[name=searchJobInst]")).setNoValueInclude(true).setNoValue("", new makeOptions("CODE","NAME").setObj($("select[name=searchJobInst]")).setNoValueInclude(true).setNoValue("",
"<%= localeMessage.getString("combo.all")%>").setData(json.instanceList).rendering(); "<%= localeMessage.getString("combo.all")%>").setData(json.instanceList).rendering();
if(typeof callback === 'function') { putSelectFromParam();
callback(url,key);
} initGrid();
}, },
error:function(e){ error:function(e){
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
$(document).ready(function() {
init(); function initGrid() {
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로 var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
console.log(gridPostData);
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
@@ -125,8 +125,12 @@ $(document).ready(function() {
} }
}); });
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
}
$(document).ready(function() {
init();
$("#btn_search").click(function(){ $("#btn_search").click(function(){
var postData = getSearchForJqgrid("cmd","LIST"); var postData = getSearchForJqgrid("cmd","LIST");
@@ -465,21 +465,21 @@ $(document).ready(function() {
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<tr> <tr>
<th style="width:180px;">Default Scheduler</th> <th style="width:180px;">Default Scheduler</th>
<td>On Memory: <span id="deft_onMemory"></span></td> <td style="width:150px">On Memory: <span id="deft_onMemory"></span></td>
<td style="width: 260px;">Previous Fired: <span id="deft_previous"></span></td> <td >Previous Fired: <span id="deft_previous"></span></td>
<td style="width: 260px;">Next Fire Time: <span id="deft_next"></span></td> <td >Next Fire Time: <span id="deft_next"></span></td>
<td style="width: 220px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td> <td style="width: 250px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td>
<td style="width: 200px;">PersistJobData: <span id="deft_persistJobData"></span></td> <td style="width: 120px;">PersistJobData: <span id="deft_persistJobData"></span></td>
<td style="width: 200px;">Durable: <span id="deft_durable"></span></td> <td style="width: 120px;">Durable: <span id="deft_durable"></span></td>
</tr> </tr>
<tr> <tr>
<th style="width:180px;">Clustered Scheduler</th> <th style="width:180px;">Clustered Scheduler</th>
<td>On Memory: <span id="clus_onMemory"></span></td> <td style="width:150px">On Memory: <span id="clus_onMemory"></span></td>
<td style="width: 260px;">Previous Fired: <span id="clus_previous"></span></td> <td>Previous Fired: <span id="clus_previous"></span></td>
<td style="width: 260px;">Next Fire Time: <span id="clus_next"></span></td> <td>Next Fire Time: <span id="clus_next"></span></td>
<td style="width: 220px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td> <td style="width: 250px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td>
<td style="width: 200px;">PersistJobData: <span id="clus_persistJobData"></span></td> <td style="width: 120px;">PersistJobData: <span id="clus_persistJobData"></span></td>
<td style="width: 200px;">Durable: <span id="clus_durable"></span></td> <td style="width: 120px;">Durable: <span id="clus_durable"></span></td>
</tr> </tr>
</table> </table>
+87
View File
@@ -99,6 +99,93 @@
<iframe class="topMenu" src="" id="topFrame" name="topFrame" scrolling="no" noresize="noresize"></iframe> <iframe class="topMenu" src="" id="topFrame" name="topFrame" scrolling="no" noresize="noresize"></iframe>
<iframe class="leftMenu" src="" name="leftFrame" scrolling="no" noresize="noresize"></iframe> <iframe class="leftMenu" src="" name="leftFrame" scrolling="no" noresize="noresize"></iframe>
<iframe class="rightCon" src="" name="mainFrame" scrolling="auto"></iframe> <iframe class="rightCon" src="" name="mainFrame" scrolling="auto"></iframe>
<%-- 자동 로그아웃 경고 모달 --%>
<div id="autoLogoutModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); z-index:99999;">
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); background:#fff; padding:30px 40px; border-radius:4px; text-align:center; min-width:320px; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
<h3 style="margin:0 0 12px; color:#333; font-size:16px;">자동 로그아웃 안내</h3>
<p style="margin:0 0 22px; color:#666; font-size:14px;">
장시간 사용하지 않아 <strong><span id="autoLogoutSeconds">60</span>초</strong> 후 자동 로그아웃됩니다.
</p>
<button id="autoLogoutContinue" style="margin-right:8px; padding:8px 22px; background:#337ab7; color:#fff; border:none; border-radius:3px; cursor:pointer; font-size:13px;">계속 사용</button>
<button id="autoLogoutNow" style="padding:8px 22px; background:#d9534f; color:#fff; border:none; border-radius:3px; cursor:pointer; font-size:13px;">로그아웃</button>
</div>
</div>
<script type="text/javascript">
(function () {
var IDLE_MS = ${autoLogoutTimeout} * 60 * 1000; // 설정값(분) → ms
var WARN_MS = 60 * 1000; // 로그아웃 1분 전 경고
var CHECK_INTERVAL = 5000; // 5초 간격 체크
var countdownTimer = null;
/* sessionStorage: iframe 포함 모든 동일-origin 프레임과 공유 */
function getLastActivity() {
var t = sessionStorage.getItem('lastActivity');
return t ? parseInt(t, 10) : Date.now();
}
function markActivity() {
sessionStorage.setItem('lastActivity', String(Date.now()));
}
function resetTimer() {
markActivity();
if ($('#autoLogoutModal').is(':visible')) {
$('#autoLogoutModal').hide();
clearInterval(countdownTimer);
}
}
function showWarning(remainMs) {
var sec = Math.ceil(remainMs / 1000);
$('#autoLogoutSeconds').text(sec);
$('#autoLogoutModal').show();
clearInterval(countdownTimer);
countdownTimer = setInterval(function () {
sec--;
if (sec <= 0) {
clearInterval(countdownTimer);
doLogout();
} else {
$('#autoLogoutSeconds').text(sec);
}
}, 1000);
}
function doLogout() {
location.href = '<%=request.getContextPath()%>/rms/logout.do';
}
/* 상위 프레임(main.jsp) 활동 감지 */
['mousemove', 'keydown', 'click', 'touchstart'].forEach(function (e) {
document.addEventListener(e, markActivity, true);
});
/* 주기적 유휴 시간 체크 */
setInterval(function () {
var idle = Date.now() - getLastActivity();
var remain = IDLE_MS - idle;
if (remain <= 0) {
clearInterval(countdownTimer);
doLogout();
} else if (remain <= WARN_MS && !$('#autoLogoutModal').is(':visible')) {
showWarning(remain);
}
}, CHECK_INTERVAL);
/* 버튼 이벤트 */
$(document).on('click', '#autoLogoutContinue', function () {
resetTimer();
$.get('<%=request.getContextPath()%>/checkSession.do'); // 서버 세션도 갱신
});
$(document).on('click', '#autoLogoutNow', function () {
doLogout();
});
/* 페이지 로드 시 초기화 */
markActivity();
})();
</script>
</body> </body>
</html> </html>
+8
View File
@@ -36,6 +36,14 @@
// save the returnValue in options so that it is available in the callback function // save the returnValue in options so that it is available in the callback function
optns.returnValue = $frame[0].contentWindow.window.returnValue; optns.returnValue = $frame[0].contentWindow.window.returnValue;
optns.onClose(); optns.onClose();
// Remove iframe from DOM so window.frames doesn't accumulate stale entries;
// without this, a second showModal call leaves frames[0] pointing to the first
// (closed) iframe, causing the previous returnValue to bleed into the next callback.
var $dialogEl = $frame.closest('.ui-dialog');
setTimeout(function() {
$frame.remove();
if ($dialogEl.length) $dialogEl.remove();
}, 0);
if(optns.option && optns.option.fullScreenPopup){ if(optns.option && optns.option.fullScreenPopup){
$('iframe[name=topFrame]',window.parent.document).css('display', 'block'); $('iframe[name=topFrame]',window.parent.document).css('display', 'block');
@@ -16,173 +16,486 @@
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" > <!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.json" />'; var url ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.view" />'; var url_view ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.view" />';
var isDetail = false; var isDetail = false;
function init(url,key,callback){
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({ $.ajax({
type : "POST", type: "POST",
url:url, url: url,
dataType:"json", dataType: "json",
data:{cmd: 'LIST_INIT_COMBO'}, data: {cmd: 'LIST_INIT_COMBO'},
success:function(json){ success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering(); 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(); new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if(typeof callback === 'function') { if (typeof callback === 'function') {
callback(url,key); callback(url, key);
} }
}, },
error:function(e){ error: function(e) {
alert(e.responseText); 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(){ 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 name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase(); var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]); $(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
}); });
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', adapterId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '어댑터가 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() { $(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key ="${param.name}"; var key = "${param.name}";
if (key != "" && key !="null"){ if (key != "" && key != "null") {
isDetail = true; isDetail = true;
} }
init(url,key,detail); init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function(){ $("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 어댑터를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
if (isDetail){ if (isDetail) {
postData.push({ name: "cmd" , value:"UPDATE"}); postData.push({name: "cmd", value: "UPDATE"});
}else{ } else {
postData.push({ name: "cmd" , value:"INSERT"}); postData.push({name: "cmd", value: "INSERT"});
} }
var isInsert = !isDetail;
$.ajax({ $.ajax({
type : "POST", type: "POST",
url:url, url: url,
data:postData, data: postData,
success:function(args){ success: function(args) {
alert("<%= localeMessage.getString("common.saveMsg") %>"); showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
goNav(returnUrl);//LIST로 이동 type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
}, },
error:function(e){ error: function(e) {
alert(e.responseText); showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
$("#btn_delete").click(function() {
$(this).blur();
var adapterName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + adapterName + ']</strong> 어댑터를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"}); postData.push({name: "cmd", value: "DELETE"});
$.ajax({ $.ajax({
type : "POST", type: "POST",
url:url, url: url,
data:postData, data: postData,
success:function(args){ success: function(args) {
alert("<%= localeMessage.getString("common.deleteMsg") %>"); showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
goNav(returnUrl);//LIST로 이동 type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
}, },
error:function(e){ error: function(e) {
alert(e.responseText); showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_previous").click(function(){
goNav(returnUrl);//LIST로 이동 $("#btn_previous").click(function() {
goNav(returnUrl);
}); });
buttonControl(isDetail); buttonControl(isDetail);
titleControl(isDetail); titleControl(isDetail);
});
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
});
</script> </script>
</head> </head>
<body> <body>
<div class="right_box"> <div class="right_box">
<div class="content_top"> <div class="content_top">
<ul class="path"> <ul class="path">
<li><a href="#">${rmsMenuPath}</a></li> <li><a href="#">${rmsMenuPath}</a></li>
</ul> </ul>
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <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_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_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></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>
<%-- <img src="<c:url value="/img/btn_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div> </div>
<div class="title"><%= localeMessage.getString("infAdpConMan.title") %><span class="tooltip" ><%= localeMessage.getString("infAdpConMan.title") %></span></div>
<table id="grid" ></table> <div class="title-wrap">
<div id="pager"></div> <div class="title"><%= localeMessage.getString("infAdpConMan.title") %><span class="tooltip"><%= localeMessage.getString("infAdpConMan.title") %></span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<!-- detail --> <!-- 탭 네비게이션 -->
<div class="detail-tabs">
<button type="button" class="detail-tab active" data-tab="config">
<i class="material-icons">settings</i> 설정
</button>
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
<i class="material-icons">analytics</i> 현황
</button>
</div>
<!-- 설정 탭 -->
<div class="tab-content active" id="tabConfigContent">
<form id="ajaxForm"> <form id="ajaxForm">
<table class="table_row" cellspacing="0"> <span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaNm") %></th> <th><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
<td><input type="text" name="name" /></td> <td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaDes") %></th> <th><%= localeMessage.getString("infAdpConMan.adaDes") %></th>
<td><input type="text" name="desc" /></td> <td><input type="text" name="desc" style="width:300px; background-color: #ebebec;" /></td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th> <th><%= localeMessage.getString("infAdpConMan.useYn") %></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> <td>
<div class="select-style"> <select name="useYn" class="styled-select"></select>
<select name="thresholdTimeUnit" /> </td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th> <th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td> <td>
<div class="select-style"> <div class="threshold-group">
<select name="useYn" /> <select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div> </div>
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</form> </form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle --> </div><!-- end content_middle -->
</div><!-- end right_box --> </div><!-- end right_box -->
</body> </body>
</html> </html>
@@ -0,0 +1,138 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" >
var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
$(document).ready(function() {
$('#grid').jqGrid({
datatype:"json",
mtype: 'POST',
url: url,
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val()},
colNames:['클라이언트 ID',
'클라이언트명',
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
'<%= localeMessage.getString("infAdpConMan.thr") %>',
'<%= localeMessage.getString("infAdpConMan.thrTimeUnit") %>',
'<%= localeMessage.getString("infAdpConMan.useYn") %>'
],
colModel:[
{ name : 'NAME' , align:'left' , width:'100' , sortable:false },
{ name : 'DESC' , align:'left' , width:'200'},
{ name : 'THRESHOLDPERSECOND' , align:'right' , width:'60'},
{ name : 'THRESHOLD' , align:'right' , width:'60'},
{ name : 'THRESHOLDTIMEUNIT' , align:'center' , width:'80'},
{ name : 'USEYN' , align:'center' , width:'60' , editoptions:{value:"0:사용안함;1:사용함"}, formatter:"select" }
],
jsonReader: {
repeatitems:false
},
pager : $('#pager'),
page : '${param.page}',
rowNum : '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList : eval('[${rmsDefaultRowList}]'),
ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId);
var name = rowData['NAME'];
var url2 = url_view;
url2 += '?cmd=DETAIL';
url2 += '&page='+$(this).getGridParam("page");
url2 += '&returnUrl='+getReturnUrl();
url2 += '&menuId='+'${param.menuId}';
//검색값
url2 += '&searchName='+$("input[name=searchName]").val();
//key값
url2 += '&name='+name;
goNav(url2);
},
gridComplete:function (d){
var colModel = $(this).getGridParam("colModel");
for(var i = 0 ; i< colModel.length; i++){
$(this).setColProp(colModel[i].name, {sortable : false});
}
}
});
resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val()}, page:1 }).trigger("reloadGrid");
});
$("#btn_new").click(function(){
var url2 = url_view;
url2 += '?cmd=DETAIL';
url2 += '&page='+$("#grid").getGridParam("page");
url2 += '&returnUrl='+getReturnUrl();
url2 += '&menuId='+'${param.menuId}';
//검색값
url2 += '&searchName=';
goNav(url2);
});
$("input[name^=search]").keydown(function(key){
if (key.keyCode == 13){
$("#btn_search").click();
}
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div><!-- end content_top -->
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
</div>
<div class="title">클라이언트 유량제어<span class="tooltip">클라이언트(API Key) 유량제어를 관리한다</span></div>
<table class="search_condition" cellspacing=0;>
<tbody>
<tr>
<th style="width:180px;">클라이언트명</th>
<td><input type="text" name="searchName" value="${param.searchName}"></td>
</tr>
</tbody>
</table>
<table id="grid" ></table>
<div id="pager"></div>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -0,0 +1,516 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
var isDetail = false;
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.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();
updateThresholdLabel();
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()]);
});
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
},
error: function(e) {
alert(e.responseText);
}
});
}
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
//버킷 현황 조회
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', clientId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
// 전체 합산 계산
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
// 요약 영역 표시
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '그룹이 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.name}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 클라이언트를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
if (isDetail) {
postData.push({name: "cmd", value: "UPDATE"});
} else {
postData.push({name: "cmd", value: "INSERT"});
}
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_delete").click(function() {
$(this).blur();
var clientName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + clientName + ']</strong> 클라이언트를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"});
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_previous").click(function() {
goNav(returnUrl);
});
buttonControl(isDetail);
titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
$(document).keydown(function(e) {
if (e.keyCode === 27) {
// ESC 키 처리 (필요시 모달 닫기 등)
}
});
});
</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_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title-wrap">
<div class="title">클라이언트 유량제어<span class="tooltip">클라이언트 유량제어</span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<!-- 탭 네비게이션 -->
<div class="detail-tabs">
<button type="button" class="detail-tab active" data-tab="config">
<i class="material-icons">settings</i> 설정
</button>
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
<i class="material-icons">analytics</i> 현황
</button>
</div>
<!-- 설정 탭 -->
<div class="tab-content active" id="tabConfigContent">
<form id="ajaxForm">
<span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>클라이언트 ID</th>
<td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th>클라이언트명</th>
<td><input type="text" name="desc" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<select name="useYn" class="styled-select"></select>
</td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div>
</td>
</tr>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
<div class="bucket-summary-item">
<span class="bucket-summary-label">적용 API</span>
<span class="bucket-summary-value" id="summaryActiveApi">0<span class="unit">개</span></span>
<span class="bucket-summary-sub" id="summaryActiveApiList"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -16,173 +16,486 @@
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" > <!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.json" />'; var url ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.view" />'; var url_view ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.view" />';
var isDetail = false; var isDetail = false;
function init(url,key,callback){
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({ $.ajax({
type : "POST", type: "POST",
url:url, url: url,
dataType:"json", dataType: "json",
data:{cmd: 'LIST_INIT_COMBO'}, data: {cmd: 'LIST_INIT_COMBO'},
success:function(json){ success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering(); 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(); new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if(typeof callback === 'function') { if (typeof callback === 'function') {
callback(url,key); callback(url, key);
} }
}, },
error:function(e){ error: function(e) {
alert(e.responseText); 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(){ 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 name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase(); var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]); $(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
}); });
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', interfaceId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '인터페이스가 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() { $(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key ="${param.name}"; var key = "${param.name}";
if (key != "" && key !="null"){ if (key != "" && key != "null") {
isDetail = true; isDetail = true;
} }
init(url,key,detail); init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function(){ $("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 인터페이스를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
if (isDetail){ if (isDetail) {
postData.push({ name: "cmd" , value:"UPDATE"}); postData.push({name: "cmd", value: "UPDATE"});
}else{ } else {
postData.push({ name: "cmd" , value:"INSERT"}); postData.push({name: "cmd", value: "INSERT"});
} }
var isInsert = !isDetail;
$.ajax({ $.ajax({
type : "POST", type: "POST",
url:url, url: url,
data:postData, data: postData,
success:function(args){ success: function(args) {
alert("<%= localeMessage.getString("common.saveMsg") %>"); showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
goNav(returnUrl);//LIST로 이동 type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
}, },
error:function(e){ error: function(e) {
alert(e.responseText); showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
$("#btn_delete").click(function() {
$(this).blur();
var ifName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + ifName + ']</strong> 인터페이스를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"}); postData.push({name: "cmd", value: "DELETE"});
$.ajax({ $.ajax({
type : "POST", type: "POST",
url:url, url: url,
data:postData, data: postData,
success:function(args){ success: function(args) {
alert("<%= localeMessage.getString("common.deleteMsg") %>"); showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
goNav(returnUrl);//LIST로 이동 type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
}, },
error:function(e){ error: function(e) {
alert(e.responseText); showAlert(e.responseText, {type: 'error'});
}
});
} }
}); });
}); });
$("#btn_previous").click(function(){
goNav(returnUrl);//LIST로 이동 $("#btn_previous").click(function() {
goNav(returnUrl);
}); });
buttonControl(isDetail); buttonControl(isDetail);
titleControl(isDetail); titleControl(isDetail);
});
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
});
</script> </script>
</head> </head>
<body> <body>
<div class="right_box"> <div class="right_box">
<div class="content_top"> <div class="content_top">
<ul class="path"> <ul class="path">
<li><a href="#">${rmsMenuPath}</a></li> <li><a href="#">${rmsMenuPath}</a></li>
</ul> </ul>
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <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_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_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></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>
<%-- <img src="<c:url value="/img/btn_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div> </div>
<div class="title"><%= localeMessage.getString("infIfConMan.title") %><span class="tooltip" ><%= localeMessage.getString("infIfConMan.title") %></span></div>
<table id="grid" ></table> <div class="title-wrap">
<div id="pager"></div> <div class="title"><%= localeMessage.getString("infIfConMan.title") %><span class="tooltip"><%= localeMessage.getString("infIfConMan.title") %></span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<!-- detail --> <!-- 탭 네비게이션 -->
<div class="detail-tabs">
<button type="button" class="detail-tab active" data-tab="config">
<i class="material-icons">settings</i> 설정
</button>
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
<i class="material-icons">analytics</i> 현황
</button>
</div>
<!-- 설정 탭 -->
<div class="tab-content active" id="tabConfigContent">
<form id="ajaxForm"> <form id="ajaxForm">
<table class="table_row" cellspacing="0"> <span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifNm") %></th> <th><%= localeMessage.getString("infIfConMan.ifNm") %></th>
<td><input type="text" name="name" /></td> <td><input type="text" name="name" style="width:300px;background-color: #ebebec;" /></td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifDesc") %></th> <th><%= localeMessage.getString("infIfConMan.ifDesc") %></th>
<td><input type="text" name="desc" /></td> <td><input type="text" name="desc" style="width:300px;background-color: #ebebec;" /></td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th> <th><%= localeMessage.getString("infAdpConMan.useYn") %></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> <td>
<div class="select-style"> <select name="useYn" class="styled-select"></select>
<select name="thresholdTimeUnit" /> </td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th> <th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td> <td>
<div class="select-style"> <div class="threshold-group">
<select name="useYn" /> <select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div> </div>
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</form> </form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle --> </div><!-- end content_middle -->
</div><!-- end right_box --> </div><!-- end right_box -->
</body> </body>
</html> </html>
@@ -39,12 +39,22 @@
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" />';
function dateFormat(cellvalue) {
if ( cellvalue == null || cellvalue.length < 8 )
return '';
return cellvalue.substr(0 ,4) + '-' +
cellvalue.substr(4, 2) + '-' +
cellvalue.substr(6, 2) ;
}
$(document).ready(function () { $(document).ready(function () {
var key = "${param.id}"; var key = "${param.id}";
if (key) { if (key) {
detail(key); detail(key);
} }
$("#expectEndDate").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
$("#btn_app_approve").click(function () { $("#btn_app_approve").click(function () {
if (confirm("승인하시겠습니까?")) { if (confirm("승인하시겠습니까?")) {
$.ajax({ $.ajax({
@@ -113,6 +123,28 @@
} }
}); });
$("#btn_modify").click(function () {
if (confirm("수정하시겠습니까?")) {
$.ajax({
type: "POST",
url: url,
data: {cmd: "UPDATE", id: key, expectEndDate: $("#expectEndDate").val().replaceAll('-','')},
success: function (args) {
alert("수정되었습니다.");
if (window.dialogArguments && window.dialogArguments.self) {
window.dialogArguments.self.location.reload();
} else if (window.opener) {
window.opener.location.reload();
}
window.close();
},
error: function (e) {
alert(e.responseText);
}
});
}
});
$("#btn_close").click(function () { $("#btn_close").click(function () {
window.close(); window.close();
}); });
@@ -126,12 +158,25 @@
data: {cmd: 'DETAIL', id: key}, data: {cmd: 'DETAIL', id: key},
success: function (json) { success: function (json) {
var data = json; var data = json;
if (data.approvalStatus.code == 'END') {
$('#btn_modify').hide();
$('#expectEndDate').hide();
$('#spExpectEndDate').show();
$('#spExpectEndDate').text(dateFormat(data.expectEndDate));
} else {
$('#btn_modify').show();
$('#expectEndDate').show();
$('#spExpectEndDate').hide();
}
$("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용"); $("#approvalType").text(data.approvalType === 'USER' ? "법인사용자" : "API 사용");
$("#approvalStatus").text(data.approvalStatus.description); $("#approvalStatus").text(data.approvalStatus.description);
$("#requesterName").text(data.requester.userName); $("#requesterName").text(data.requester.userName);
$("#approvalSubject").text(data.approvalSubject); $("#approvalSubject").text(data.approvalSubject);
$("#approvalDetail").html(data.approvalDetail); $("#approvalDetail").html(data.approvalDetail);
$("#createdDate").text(data.createdDate).inputmask("9999-99-99 99:99:99.999", {'autoUnmask': true}); $("#createdDate").text(data.createdDate).inputmask("9999-99-99 99:99:99.999", {'autoUnmask': true});
$("#expectEndDate").val(data.expectEndDate);
if (data.approvers.some(approver => if (data.approvers.some(approver =>
approver.approverStatus === 'CURRENT' && approver.approverStatus === 'CURRENT' &&
@@ -144,7 +189,7 @@
document.getElementById("btn_app_reject").style.display = "none"; document.getElementById("btn_app_reject").style.display = "none";
} }
if (data.approvalStatus.description === '배포실패') { if (data.approvalStatus.code === 'FAILED') {
document.getElementById("btn_app_redeploy").style.display = ""; document.getElementById("btn_app_redeploy").style.display = "";
} }
@@ -242,9 +287,14 @@
</tr> </tr>
<tr> <tr>
<th>상세 내용</th> <th>상세 내용</th>
<td colspan="3"> <td>
<div id="approvalDetail" style="white-space: pre-wrap;"></div> <div id="approvalDetail" style="white-space: pre-wrap;"></div>
</td> </td>
<th>예상완료일</th>
<td>
<input type="text" id="expectEndDate" style="width: 120px; text-align:center"/>
<span id="spExpectEndDate"></span>
</td>
</tr> </tr>
</table> </table>
@@ -282,6 +332,9 @@
</table> </table>
</div> </div>
<div class="popup_button"> <div class="popup_button">
<button type="button" class="cssbtn" id="btn_modify" level="W"><i
class="material-icons">save</i>수정
</button>
<button type="button" class="cssbtn" style="display: none;" id="btn_app_approve" level="W"><i <button type="button" class="cssbtn" style="display: none;" id="btn_app_approve" level="W"><i
class="material-icons">check</i>승인 class="material-icons">check</i>승인
</button> </button>
@@ -17,11 +17,60 @@
<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 dateFormat(cellvalue, options, rowObject) {
if ( cellvalue == null || cellvalue.length < 8 )
return '';
return cellvalue.substr(0 ,4) + '-' +
cellvalue.substr(4, 2) + '-' +
cellvalue.substr(6, 2) ;
}
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",
@@ -33,7 +82,7 @@
searchApprovalStatus: $('select[name=searchApprovalStatus]').val(), searchApprovalStatus: $('select[name=searchApprovalStatus]').val(),
searchApprovalSubject: $('input[name=searchApprovalSubject]').val() searchApprovalSubject: $('input[name=searchApprovalSubject]').val()
}, },
colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일'], colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일', '예상완료일'],
colModel: [ colModel: [
{ name: 'rowNum', align: 'center', width: '30', sortable: false}, { name: 'rowNum', align: 'center', width: '30', sortable: false},
{ name: 'id', align: 'center', key: true, hidden: true }, { name: 'id', align: 'center', key: true, hidden: true },
@@ -42,7 +91,8 @@
{ name: 'approvalType', align: 'center', width: 80, hidden: true}, { name: 'approvalType', align: 'center', width: 80, hidden: true},
{ name: 'approvalStatus.description', align: 'center', width: 80 }, { name: 'approvalStatus.description', align: 'center', width: 80 },
{ name: 'requester.userName', align: 'center', width: 100 }, { name: 'requester.userName', align: 'center', width: 100 },
{ name: 'createdDate', align: 'center', width: 120, formatter: 'date', formatoptions: { srcformat: 'ISO8601Long', newformat: 'Y-m-d H:i:s' } } { name: 'createdDate', align: 'center', width: 120, formatter: 'date', formatoptions: { srcformat: 'ISO8601Long', newformat: 'Y-m-d H:i:s' } },
{ name: 'expectEndDate', align: 'center', width: 100, formatter: dateFormat},
], ],
jsonReader: { jsonReader: {
repeatitems: false repeatitems: false
@@ -53,6 +103,8 @@
viewrecords: true, viewrecords: true,
autowidth: true, autowidth: true,
height: 'auto', height: 'auto',
multiselect: true,
multiboxonly: true,
ondblClickRow: function(rowId) { ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId); var rowData = $(this).getRowData(rowId);
var id = rowData['id']; var id = rowData['id'];
@@ -93,6 +145,10 @@
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid"); $("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
}); });
$("#btn_delete_selected").click(function() {
deleteSelectedApprovals();
});
$("select[name^=search], input[name^=search]").keydown(function(key) { $("select[name^=search], input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) { if (key.keyCode == 13) {
$("#btn_search").click(); $("#btn_search").click();
@@ -112,9 +168,17 @@
</div><!-- end content_top --> </div><!-- end content_top -->
<div class="content_middle" id="content_middle"> <div class="content_middle" id="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<c:if test="${hardDeleteEnabled}">
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
</c:if>
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button> <button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
</div> </div>
<div class="title">승인 요청 목록<span class="tooltip">승인 요청을 관리합니다.</span></div> <div class="title">승인 요청 목록<span class="tooltip">승인 요청을 관리합니다.</span></div>
<c:if test="${hardDeleteEnabled}">
<div style="background:#fff3cd; border:1px solid #ffc107; padding:8px 15px; margin:5px 0; border-radius:4px; color:#856404;">
<strong>[주의]</strong> 선택 삭제 시 승인 요청과 승인자 이력이 DB에서 영구 삭제됩니다.
</div>
</c:if>
<form id="ajaxForm" onsubmit="return false;"> <form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0">
<tbody> <tbody>
@@ -134,8 +198,8 @@
<div class="select-style"> <div class="select-style">
<select name="searchApprovalStatus"> <select name="searchApprovalStatus">
<option value="">전체</option> <option value="">전체</option>
<option value="CREATED">생성됨</option>
<option value="REQUESTED">요청됨</option> <option value="REQUESTED">요청됨</option>
<option value="PROCESSING">진행중</option>
<option value="DENIED">반려됨</option> <option value="DENIED">반려됨</option>
<option value="END">승인됨</option> <option value="END">승인됨</option>
</select> </select>
@@ -20,15 +20,22 @@
var select_orgId = ""; var select_orgId = "";
function formatInquiryStatus(cellvalue, options, rowObject) { function formatInquiryStatus(cellvalue, options, rowObject) {
return cellvalue === 'PENDING' if (cellvalue == 'PENDING') {
? '<span style="color: red;">문의중</span>' return '<span style="color: red;">문의중</span>';
: '<span>답변완료</span>'; } else if (cellvalue == 'RESPONDED') {
return '<span >답변완료</span>'
} else if (cellvalue == 'CLOSED') {
return '<span >답변종료</span>'
} else {
return cellvalue;
}
} }
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; var commentCount = rowObject.commentCount > 0 ? ' <span style="color: #337ab7;">[' + rowObject.commentCount + ']</span>' : '';
return cellvalue + icon + commentCount;
} }
function deleteSelectedRows() { function deleteSelectedRows() {
@@ -37,6 +37,13 @@
border-bottom: 2px solid #e0e0e0; /* 제목 아래 줄 긋기 */ border-bottom: 2px solid #e0e0e0; /* 제목 아래 줄 긋기 */
} }
.comment-section {
margin-bottom: 30px;
border: 1px solid #e0e0e0; /* 동일한 박스 스타일 */
padding: 20px;
border-radius: 5px;
}
.info-row { .info-row {
margin-bottom: 10px; margin-bottom: 10px;
display: flex; display: flex;
@@ -58,6 +65,15 @@
min-height: 200px; min-height: 200px;
} }
#commentDetail {
min-height: 100px;
border: 1px solid #00000032;
border-radius: 4px;
padding: 10px;
flex: 1;
font-family: '맑은고딕'
}
.required-mark { .required-mark {
color: red; color: red;
margin-left: 2px; margin-left: 2px;
@@ -68,6 +84,7 @@
var url = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.json" />'; var url = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.json" />';
var url_view = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.view" />'; var url_view = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.view" />';
var file_download = '<c:url value="/file/download.do" />'; var file_download = '<c:url value="/file/download.do" />';
var inquiryStatus = 'PENDING';
function decodeHTMLEntities(text) { function decodeHTMLEntities(text) {
var textArea = document.createElement('textarea'); var textArea = document.createElement('textarea');
@@ -82,10 +99,21 @@
dataType: "json", dataType: "json",
data: {cmd: 'DETAIL', id: key}, data: {cmd: 'DETAIL', id: key},
success: function (data) { success: function (data) {
$("#id").val(key); $("#id").val(key);
$("#inquirySubject").text(data.inquirySubject); $("#inquirySubject").text(data.inquirySubject);
$("#inquiryDetail").html(data.inquiryDetail); $("#inquiryDetail").html(data.inquiryDetail);
$("#inquiryStatus").text(data.inquiryStatus === 'PENDING' ? '문의중' : '답변완료');
inquiryStatus = data.inquiryStatus;
if (data.inquiryStatus == 'PENDING') {
$("#inquiryStatus").text('문의중');
} else if (data.inquiryStatus == 'RESPONDED') {
$("#inquiryStatus").text('답변완료');
} else if (data.inquiryStatus == 'CLOSED') {
$("#inquiryStatus").text('답변종료');
}
$("#inquirer").text(data.inquirer.userName); $("#inquirer").text(data.inquirer.userName);
$("#createdDate").text(timeStampFormat(data.createdDate)); $("#createdDate").text(timeStampFormat(data.createdDate));
$("#responder").text(data.responderName || ''); $("#responder").text(data.responderName || '');
@@ -94,6 +122,8 @@
var decodedContent = decodeHTMLEntities(data.responseDetail); var decodedContent = decodeHTMLEntities(data.responseDetail);
$("#responseDetail").summernote('code', decodedContent || ''); $("#responseDetail").summernote('code', decodedContent || '');
// 댓글 표시
listComment(key);
// 첨부 파일 표시 // 첨부 파일 표시
displayAttachFiles(data.fileInfo); displayAttachFiles(data.fileInfo);
@@ -105,6 +135,59 @@
}); });
} }
function listComment(inquiryId) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_COMMENT', id: inquiryId},
success: function (data) {
var $commentList = $('#commentList');
$commentList.empty();
if (!data || data.length === 0) return;
$.each(data, function (i, comment) {
var date = comment.createdDate ? timeStampFormat(comment.createdDate) : '';
var row = $('<div>').addClass('info-row');
$('<div>').addClass('info-label').css('color', '#555').text(comment.writerName || '').appendTo(row);
var content = $('<div>').addClass('info-content');
$('<div>').css('white-space', 'pre-wrap').text(comment.commentDetail).appendTo(content);
var meta = $('<div>').css({'font-size': '0.9em', 'color': '#999', 'margin-top': '2px'});
$('<span>').text(date).appendTo(meta);
$('<button>').attr('type', 'button').addClass('cssbtn smallBtn2').css({'margin-left': '8px', 'font-size': '0.85em', 'padding': '1px 6px'})
.text('delete').on('click', (function(id, inquiryId) {
return function() { deleteComment(id, inquiryId); };
})(comment.id, comment.inquiryId))
.appendTo(meta);
meta.appendTo(content);
content.appendTo(row);
row.appendTo($commentList);
});
},
error: function (e) {
alert(e.responseText);
}
});
}
function deleteComment(commentId, inquiryId) {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 삭제할 수 없습니다');
return;
}
if (!confirm("댓글을 삭제하시겠습니까?")) return;
$.ajax({
type: "POST",
url: url,
data: {cmd: 'DELETE_COMMENT', id: commentId},
success: function () {
listComment(inquiryId);
},
error: function (e) {
alert(e.responseText);
}
});
}
$(document).ready(function () { $(document).ready(function () {
var returnUrl = getReturnUrlForReturn(); var returnUrl = getReturnUrlForReturn();
var key = "${param.id}"; var key = "${param.id}";
@@ -115,11 +198,16 @@
height: 200 height: 200
}); });
if (key) { if (key) {
detail(key); detail(key);
} }
$("#btn_modify").click(function () { $("#btn_modify").click(function () {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 수정할 수 없습니다');
return;
}
if (!checkRequired("ajaxForm")) return; if (!checkRequired("ajaxForm")) return;
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return; if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
@@ -140,6 +228,32 @@
}); });
}); });
$("#btn_comment").click(function () {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 댓글을 등록할 수 없습니다');
return;
}
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
$.ajax({
type: "POST",
url: url,
data: {
cmd: "INSERT_COMMENT",
inquiryId: $('#id').val(),
commentDetail: $('#commentDetail').val(),
},
success: function (json) {
alert("<%= localeMessage.getString("common.saveMsg") %>");
$('#commentDetail').val('');
listComment(key);
},
error: function (e) {
alert(e.responseText);
}
});
});
$("#btn_previous").click(function () { $("#btn_previous").click(function () {
goNav(returnUrl); goNav(returnUrl);
}); });
@@ -206,6 +320,10 @@
<!-- 답변 작성 부분 --> <!-- 답변 작성 부분 -->
<div class="response-section"> <div class="response-section">
<div class="info-row">
<div class="info-label">상태</div>
<div id="inquiryStatus" class="info-content"></div>
</div>
<div class="info-row"> <div class="info-row">
<div class="info-label">답변자</div> <div class="info-label">답변자</div>
<div id="responder" class="info-content"></div> <div id="responder" class="info-content"></div>
@@ -221,6 +339,19 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 댓글 작성 부분 -->
<div class="comment-section">
<div id="commentList"></div>
<div class="info-row">
<div class="info-label">${sessionScope.login.userName}</div>
<div style="flex:1; display: flex; gap: 8px; align-items: stretch;">
<textarea id="commentDetail" name="commentDetail" data-required data-warning="댓글 내용을 입력하여 주십시오."></textarea>
<button type="button" class="cssbtn" id="btn_comment" level="W" style="width:100px; height: 100px; text-align:center; ">댓글등록</button>
</div>
</div>
</div>
<input type="hidden" name="id" id="id"> <input type="hidden" name="id" id="id">
</form> </form>
</div> </div>
@@ -17,6 +17,17 @@
<script language="javascript" > <script language="javascript" >
var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />'; var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />';
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />'; var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
var combo;
function formatNoticeType(cellvalue, options, rowObject) {
var name = "";
for (var i = 0; i < combo.noticeTypeList.length; i++) {
if (combo.noticeTypeList[i].CODE == cellvalue) {
return combo.noticeTypeList[i].NAME;
}
}
return cellvalue;
}
function formatuseYn(cellvalue, options, rowObject) { function formatuseYn(cellvalue, options, rowObject) {
return cellvalue === 'N' return cellvalue === 'N'
@@ -24,27 +35,57 @@
: '<span>사용</span>'; : '<span>사용</span>';
} }
function formatFixYn(cellvalue, options, rowObject) {
return cellvalue === 'Y'
? '고정'
: '';
}
function formatFile(cellvalue, options, rowObject) { function formatFile(cellvalue, options, rowObject) {
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png'; var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : ''; var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : '';
return cellvalue + icon; return cellvalue + icon;
} }
function init(){
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
console.log('init', json);
combo = json;
new makeOptions("CODE","NAME").setObj($("select[name=searchNoticeType]")).setNoValueInclude(true).setNoValue('','<%=localeMessage.getString("combo.all")%>').setData(json.noticeTypeList).rendering();
putSelectFromParam();
},
error:function(e){
alert(e.responseText);
}
});
}
$(document).ready(function() { $(document).ready(function() {
$('#grid').jqGrid({ $('#grid').jqGrid({
datatype:"json", datatype:"json",
mtype: 'POST', mtype: 'POST',
url: url, url: url,
postData : { cmd : 'LIST', postData : { cmd : 'LIST',
searchNoticeType: $('select[name=searchNoticeType]').val(),
searchUseYn: $('select[name=searchUseYn]').val(), searchUseYn: $('select[name=searchUseYn]').val(),
searchNoticeSubject: $('input[name=searchNoticeSubject]').val(), searchNoticeSubject: $('input[name=searchNoticeSubject]').val(),
searchNoticeDetail: $('input[name=searchNoticeDetail]').val() searchNoticeDetail: $('input[name=searchNoticeDetail]').val()
}, },
colNames:['id', 'No.', '제목', '사용', '마지막 수정일', '등록일', '등록자', '조회수'], colNames:['id', 'No.', '게시유형', '제목', '고정여부', '사용', '마지막 수정일', '등록일', '등록자', '조회수'],
colModel:[ colModel:[
{ name : 'id' , align:'center', key:true, hidden:true}, { name : 'id' , align:'center', key:true, hidden:true},
{ name : 'rowNum' , align:'center', width:80 }, { name : 'rowNum' , align:'center', width:60 },
{ name : 'noticeSubject' , align:'left' , width:200 , formatter: formatFile }, { name : 'noticeType' , align:'center', width:60, formatter: formatNoticeType },
{ name : 'noticeSubject' , align:'left' , width:300, formatter: formatFile },
{ name : 'fixYn' , align:'center', width:60, formatter: formatFixYn },
{ name : 'useYn' , align:'center', width:60, formatter: formatuseYn }, { name : 'useYn' , align:'center', width:60, formatter: formatuseYn },
{ name : 'lastModifiedDate', align:'center', width:120 , formatter: timeStampFormat}, { name : 'lastModifiedDate', align:'center', width:120 , formatter: timeStampFormat},
{ name : 'createdDate' , align:'center', width:120, formatter: timeStampFormat }, { name : 'createdDate' , align:'center', width:120, formatter: timeStampFormat },
@@ -98,6 +139,8 @@
} }
}); });
init();
resizeJqGridWidth('grid','content_middle','1000'); resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){ $("#btn_search").click(function(){
@@ -140,9 +183,25 @@
<div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div> <div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div>
<form id="ajaxForm" onsubmit="return false;"> <form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing=0;> <table class="search_condition" cellspacing=0;>
<colgroup>
<col style="width:180px;">
<col style="width:240px;">
<col style="width:180px;">
<col style="width:240px;">
<col style="width:180px;">
<col style="width:240px;">
</colgroup>
<tbody> <tbody>
<tr> <tr>
<th style="width:180px;">사용여부</th> <th>게시유형</th>
<td>
<div class="select-style">
<select name="searchNoticeType"></select>
</div>
</td>
<th>제목/내용</th>
<td><input type="text" name="searchSubjectDetail"></td>
<th>사용여부</th>
<td> <td>
<div class="select-style"> <div class="select-style">
<select name="searchUseYn"> <select name="searchUseYn">
@@ -152,8 +211,6 @@
</select> </select>
</div> </div>
</td> </td>
<th style="width:180px;">제목/내용</th>
<td><input type="text" name="searchSubjectDetail"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -19,12 +19,70 @@
<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 apiPopupUrl = '<c:url value="/onl/apim/apigroup/apiGroupMan.view" />';
var file_download = '<c:url value="/file/download.do" />'; var file_download = '<c:url value="/file/download.do" />';
var isDetail = false; var isDetail = false;
var fileInfo = null; var fileInfo = null;
// 코드 상수 — PortalNoticeUI 와 동기화
var NOTICE_TYPE_NORMAL = '1';
var NOTICE_TYPE_MAINTENANCE = '2';
var NOTICE_TYPE_INCIDENT = '3';
// 영향 API 목록 (UI 상태)
var affectedApis = [];
function isIncidentType(t) {
return t === NOTICE_TYPE_INCIDENT || t === NOTICE_TYPE_MAINTENANCE;
}
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();
toggleIncidentFields();
if (key) {
detail(key);
}
},
error:function(e){
alert(e.responseText);
}
});
}
function toggleIncidentFields() {
var t = $('#noticeType').val();
if (isIncidentType(t)) {
$('.incident-row').show();
// INCIDENT 만 상태 영역 표시
if (t === NOTICE_TYPE_INCIDENT) {
$('.incident-state-row').show();
} else {
$('.incident-state-row').hide();
}
} else {
$('.incident-row').hide();
$('.incident-state-row').hide();
}
} }
function decodeHTMLEntities(text) { function decodeHTMLEntities(text) {
@@ -44,67 +102,123 @@
function displayAttachFile(file) { function displayAttachFile(file) {
var $attachFiles = $('#attachFiles'); var $attachFiles = $('#attachFiles');
$attachFiles.empty(); $attachFiles.empty();
if (file) { if (file) {
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png'; var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
var fileItem = $('<div>').addClass('file-item'); var fileItem = $('<div>').addClass('file-item');
$('<img>').attr('src', iconPath).css({'marginLeft': '15px','marginRight': '5px' ,'height': '1.2em'}).appendTo(fileItem); $('<img>').attr('src', iconPath).css({'marginLeft': '15px','marginRight': '5px' ,'height': '1.2em'}).appendTo(fileItem);
var fileNameSpan = $('<span>').addClass('fileName')
var fileNameSpan = $('<span>')
.addClass('fileName')
.text(file.originalFileName + '.' + file.fileExtension + ' (' + formatFileSize(file.size) + ')') .text(file.originalFileName + '.' + file.fileExtension + ' (' + formatFileSize(file.size) + ')')
.css('cursor', 'pointer') .css('cursor', 'pointer')
.on('click', function() { .on('click', function() { if (file.fileId) downloadFile(file.fileId); });
if (file.fileId) {
downloadFile(file.fileId);
}
});
fileNameSpan.appendTo(fileItem); fileNameSpan.appendTo(fileItem);
$('<button>').text('X').addClass('delete-file').css('marginLeft', '15px') $('<button>').text('X').addClass('delete-file').css('marginLeft', '15px')
.on('click', function() { .on('click', function() {
fileInfo = null; fileInfo = null;
displayAttachFile(null); displayAttachFile(null);
// 폼에 삭제 플래그 추가 $('<input>').attr({type: 'hidden', name: 'fileDeleted', value: 'true'}).appendTo('#ajaxForm');
$('<input>').attr({ }).appendTo(fileItem);
type: 'hidden',
name: 'fileDeleted',
value: 'true'
}).appendTo('#ajaxForm');
})
.appendTo(fileItem);
fileItem.appendTo($attachFiles); fileItem.appendTo($attachFiles);
} }
} }
function downloadFile(fileId) { function downloadFile(fileId) {
if (fileId) { if (fileId) {
var fileSn = 1; // 단일 파일이므로 시리얼 넘버를 1로 고정 window.location.href = file_download + '?fileId=' + fileId + '&fileSn=1';
window.location.href = file_download + '?fileId=' + fileId + '&fileSn=' + fileSn;
} else {
console.log("File not yet uploaded or doesn't have an ID");
} }
} }
function renderAffectedApis() {
var $tb = $('#affectedApiTbody');
$tb.empty();
if (!affectedApis.length) {
$tb.append('<tr><td colspan="3" style="text-align:center;color:#999;">선택된 영향 API가 없습니다.</td></tr>');
return;
}
$.each(affectedApis, function(idx, api) {
var $tr = $('<tr>');
$tr.append('<td style="text-align:center;width:40px;"><input type="checkbox" class="affected-api-check" data-idx="' + idx + '"></td>');
$tr.append('<td>' + (api.apiId || '') + '</td>');
$tr.append('<td>' + (api.apiName || '') + '</td>');
$tb.append($tr);
});
}
function openApiPopup() {
// showModal 헬퍼는 urlAddServiceType + &menuId=getMenuId() + &pop=true 를 자동 부여 →
// AuthorizeInterceptor 가 정상적으로 권한 인식. window.open 직접 호출 시 권한 부족 발생.
// 본 페이지는 summernote 본문 에디터 등 다른 iframe 이 함께 존재할 수 있어
// frames[0] 가정은 위험 → DOM 에서 .ui-dialog 내 iframe 중 src=apiGroupMan 인 것을 식별.
var selectedIds = affectedApis.map(function(a) { return a.apiId; }).join(',');
var url2 = apiPopupUrl + '?cmd=POPUP&selectedApiIds=' + encodeURIComponent(selectedIds);
var args = {};
showModal(url2, args, 1280, 860, function () {
var doc = args.self && args.self.document;
if (!doc) return;
var popupFrame = null;
var iframes = doc.querySelectorAll('iframe');
for (var i = 0; i < iframes.length; i++) {
var src = iframes[i].getAttribute('src') || '';
if (src.indexOf('apiGroupMan') >= 0) {
popupFrame = iframes[i].contentWindow;
break;
}
}
if (!popupFrame) return;
var rv = popupFrame.returnValue;
if (!rv) return;
var rows = Array.isArray(rv) ? rv : [rv];
$.each(rows, function(_, item) {
if (!item || !item.apiId) return;
var exists = affectedApis.some(function(a){ return a.apiId === item.apiId; });
if (!exists) {
affectedApis.push({apiId: item.apiId, apiName: item.apiDesc || item.apiName || ''});
}
});
renderAffectedApis();
});
}
function removeSelectedAffectedApis() {
var idxes = [];
$('.affected-api-check:checked').each(function() {
idxes.push(parseInt($(this).data('idx'), 10));
});
if (!idxes.length) {
alert('삭제할 항목을 선택해주세요.');
return;
}
// 역순 삭제
idxes.sort(function(a,b){return b-a;}).forEach(function(i){ affectedApis.splice(i, 1); });
renderAffectedApis();
}
function detail(key) { function detail(key) {
if (!isDetail) return; if (!isDetail) return;
$.ajax({ $.ajax({
type: "POST", type: "POST", url: url, dataType: "json",
url: url,
dataType: "json",
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);
// 파일 정보 표시 // 장애/점검 필드
$('#summary').val(data.summary || '');
$('#startedAt').val(data.startedAt || '');
$('#endAt').val(data.endAt || '');
$('#state').val(data.state || 'INVESTIGATING');
$('#previousState').text(data.previousState || '없음');
affectedApis = (data.affectedApis || []).map(function(a){
return {apiId: a.apiId, apiName: a.apiName || ''};
});
renderAffectedApis();
toggleIncidentFields();
if (data.fileInfo) { if (data.fileInfo) {
fileInfo = { fileInfo = {
originalFileName: data.fileInfo.fileDetails[0].originalFileName, originalFileName: data.fileInfo.fileDetails[0].originalFileName,
@@ -115,37 +229,21 @@
displayAttachFile(fileInfo); displayAttachFile(fileInfo);
} }
}, },
error: function (e) { error: function (e) { alert(e.responseText); }
alert(e.responseText);
}
}); });
} }
$(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 방식, 리사이징 지원) initSummernote('#contents', { placeholder: '여기에 내용을 입력하세요.', height: 300 });
initSummernote('#contents', {
placeholder: '여기에 내용을 입력하세요.',
height: 300
});
if (key) { $('#noticeType').on('change', toggleIncidentFields);
detail(key);
} $('#btn_addAffectedApi').click(openApiPopup);
$('#btn_removeAffectedApi').click(removeSelectedAffectedApis);
$('#fileInput').change(function(e) { $('#fileInput').change(function(e) {
var file = e.target.files[0]; var file = e.target.files[0];
@@ -153,51 +251,66 @@
fileInfo = { fileInfo = {
originalFileName: file.name.split('.').slice(0, -1).join('.'), originalFileName: file.name.split('.').slice(0, -1).join('.'),
fileExtension: file.name.split('.').pop(), fileExtension: file.name.split('.').pop(),
file: file, file: file, size: file.size
size: file.size
}; };
displayAttachFile(fileInfo); displayAttachFile(fileInfo);
} }
this.value = ''; // 입력 필드 초기화 this.value = '';
});
$('#addFile').click(function() {
$('#fileInput').click();
}); });
$('#addFile').click(function() { $('#fileInput').click(); });
$("#btn_modify").click(function () { $("#btn_modify").click(function () {
if (!checkRequired("ajaxForm")) return; if (!checkRequired("ajaxForm")) return;
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true) var noticeType = $('#noticeType').val();
if (isIncidentType(noticeType)) {
if (!$('#startedAt').val()) {
alert('시작 시각을 입력해 주십시오.');
return; return;
}
if (noticeType === NOTICE_TYPE_MAINTENANCE && !$('#endAt').val()) {
alert('점검 종료 시각을 입력해 주십시오.');
return;
}
if (affectedApis.length === 0) {
alert('영향 API를 1건 이상 선택해 주십시오.');
return;
}
}
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true) return;
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'));
// 시작/종료 시각이 비어 있으면 전송하지 않음 (서버 파싱 오류 방지)
if (!$('#startedAt').val()) formData.delete('startedAt');
if (!$('#endAt').val()) formData.delete('endAt');
// 영향 API → affectedApis[i].apiId/apiName 으로 전송
$.each(affectedApis, function(i, api) {
formData.set('affectedApis[' + i + '].apiId', api.apiId);
formData.set('affectedApis[' + i + '].apiName', api.apiName || '');
});
formData.append("cmd", isDetail ? "UPDATE" : "INSERT"); formData.append("cmd", isDetail ? "UPDATE" : "INSERT");
// 파일 처리
if (fileInfo && fileInfo.file) { if (fileInfo && fileInfo.file) {
formData.set("files", fileInfo.file); formData.set("files", fileInfo.file);
formData.set("fileName", fileInfo.originalFileName + '.' + fileInfo.fileExtension); formData.set("fileName", fileInfo.originalFileName + '.' + fileInfo.fileExtension);
} }
$.ajax({ $.ajax({
type: "POST", type: "POST", url: url, data: formData,
url: url, processData: false, contentType: false,
data: formData, success: function () {
processData: false,
contentType: false,
success: function (json) {
alert("<%= localeMessage.getString("common.saveMsg") %>"); alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl); goNav(returnUrl);
}, },
error: function (e) { error: function (e) { alert(e.responseText); }
alert(e.responseText);
}
}); });
}); });
@@ -205,25 +318,18 @@
if (confirm("<%= localeMessage.getString("common.confirmMsg")%>")) { if (confirm("<%= localeMessage.getString("common.confirmMsg")%>")) {
var postData = $('#ajaxForm').serializeArray(); var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"}); postData.push({name: "cmd", value: "DELETE"});
$.ajax({ $.ajax({
type: "POST", type: "POST", url: url, data: postData,
url: url, success: function () {
data: postData,
success: function (args) {
alert("<%= localeMessage.getString("common.deleteMsg") %>"); alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl); goNav(returnUrl);
}, },
error: function (e) { error: function (e) { alert(e.responseText); }
alert(e.responseText);
}
}); });
} }
}); });
$("#btn_previous").click(function () { $("#btn_previous").click(function () { goNav(returnUrl); });
goNav(returnUrl);
});
}); });
</script> </script>
</head> </head>
@@ -233,7 +339,7 @@
<ul class="path"> <ul class="path">
<li><a href="#">${rmsMenuPath}</a></li> <li><a href="#">${rmsMenuPath}</a></li>
</ul> </ul>
</div><!-- end content_top --> </div>
<div class="content_middle"> <div class="content_middle">
<div class="search_wrap"> <div class="search_wrap">
<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_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
@@ -244,29 +350,87 @@
<form id="ajaxForm" enctype="multipart/form-data" accept-charset="UTF-8"> <form id="ajaxForm" enctype="multipart/form-data" accept-charset="UTF-8">
<input type="hidden" name="id" id="id"> <input type="hidden" name="id" id="id">
<table class="table_row" cellspacing="0"> <table class="table_row" cellspacing="0">
<colgroup>
<col style="width: 10%"/>
<col style="width: 40%"/>
<col style="width: 10%"/>
<col style="width: 40%"/>
</colgroup>
<tr> <tr>
<th style="width:20%;">사용여부</th> <th>게시유형 <font color="red">*</font></th>
<td> <td colspan="3">
<input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용</td> <div class="select-style">
</tr> <select name="noticeType" id="noticeType"></select>
<tr> </div>
<th style="width:20%;">제목 <font color="red">*</font></th>
<td><input type="text" id="noticeSubject" name="noticeSubject" style="width:100%" data-required data-warning="제목을 입력하여 주십시오."/></td>
</tr>
<tr>
<th style="width:20%;">본문 <font color="red">*</font></th>
<td>
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
</td> </td>
</tr> </tr>
<tr> <tr>
<th style="width:20%;">첨부파일</th> <th>제목 <font color="red">*</font></th>
<td colspan="3"><input type="text" id="noticeSubject" name="noticeSubject" style="width:100%" data-required data-warning="제목을 입력하여 주십시오."/></td>
</tr>
<tr>
<th>사용여부</th>
<td> <td>
<div style="margin: 5px 0px; display: inline-block;"> <input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용
<input type="file" id="fileInput" name="files" style="display:none;" /> </td>
<button type="button" id="addFile" class="cssbtn smallBtn">파일 선택</button> <th>상단고정</th>
<td>
<input type="checkbox" name="fixYn" id="fixYn" value="Y" ${portalNoticeUI.fixYn eq 'Y' ? 'checked' : ''}> 고정
</td>
</tr>
<tr>
<th>본문 <font color="red">*</font></th>
<td colspan="3">
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
</td>
</tr>
<!-- ───── 장애/점검 전용 영역 ───── -->
<tr class="incident-row" style="display:none;">
<th>시작 <font color="red">*</font></th>
<td>
<input type="datetime-local" id="startedAt" name="startedAt" style="width:90%">
</td>
<th>종료</th>
<td>
<input type="datetime-local" id="endAt" name="endAt" style="width:90%" placeholder="해소시 자동 채움">
</td>
</tr>
<tr class="incident-row incident-state-row" style="display:none;">
<th>상태</th>
<td>
<div class="select-style">
<select id="state" name="state">
<option value="INVESTIGATING">INVESTIGATING - 조사중</option>
<option value="IDENTIFIED">IDENTIFIED - 원인 파악</option>
<option value="MONITORING">MONITORING - 모니터링</option>
<option value="RESOLVED">RESOLVED - 해소</option>
<option value="CANCELED">CANCELED - 취소</option>
</select>
</div> </div>
<div id="attachFiles" style="margin: 5px 0px; display: inline-block;"></div> </td>
<th>이전 상태</th>
<td><span id="previousState">없음</span></td>
</tr>
<tr class="incident-row" style="display:none;">
<th>영향 API 선택</th>
<td colspan="3">
<div style="margin-bottom:5px;">
<button type="button" class="cssbtn smallBtn" id="btn_removeAffectedApi"><i class="material-icons">delete</i> 삭제</button>
<button type="button" class="cssbtn smallBtn" id="btn_addAffectedApi"><i class="material-icons">add</i> 추가</button>
</div>
<table class="table_row" cellspacing="0" style="width:100%;">
<thead>
<tr>
<th style="width:40px;text-align:center;"><input type="checkbox" id="affectedApiAll" onclick="$('.affected-api-check').prop('checked', this.checked);"></th>
<th>API ID</th>
<th>API 명</th>
</tr>
</thead>
<tbody id="affectedApiTbody">
<tr><td colspan="3" style="text-align:center;color:#999;">선택된 영향 API가 없습니다.</td></tr>
</tbody>
</table>
</td> </td>
</tr> </tr>
</table> </table>
@@ -82,6 +82,54 @@
</style> </style>
<jsp:include page="/jsp/common/include/script.jsp"/> <jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript"> <script language="javascript">
function executeAggregationHourly() {
var targetDate = $("#targetHour").val().replace(/-/g, "");
var resultDiv = $("#hourlyResult");
// 날짜 입력 검증
if (!targetDate || targetDate.trim() === '') {
alert('대상 날짜를 입력하세요. (예: 20250120)');
$("#targetHour").focus();
return;
}
resultDiv.hide();
$("#btn_execute_hourly").prop("disabled", true).text("실행중...");
$.ajax({
url: '<c:url value="/onl/kjb/statistics/apiStatsAggregationMan.json"/>',
type: 'POST',
data: {
cmd: 'AGGREGATION_HOUR',
targetDate: targetDate,
serviceType: '${param.serviceType}'
},
success: function(response) {
resultDiv.removeClass("error").addClass("success");
var message = response.message + "\n";
message += "대상 날짜: " + response.targetDate + "\n";
message += "처리 건수: " + response.processedCount;
resultDiv.find("pre").text(message);
resultDiv.show();
},
error: function(xhr) {
resultDiv.removeClass("success").addClass("error");
var message = "집계 실행 실패\n";
try {
var error = JSON.parse(xhr.responseText);
message += error.message;
} catch(e) {
message += xhr.responseText || "알 수 없는 오류가 발생했습니다.";
}
resultDiv.find("pre").text(message);
resultDiv.show();
},
complete: function() {
$("#btn_execute_hourly").prop("disabled", false).text("집계 실행");
}
});
}
function executeHourlyToDaily() { function executeHourlyToDaily() {
var targetDate = $("#targetDate").val().replace(/-/g, ""); var targetDate = $("#targetDate").val().replace(/-/g, "");
var resultDiv = $("#dailyResult"); var resultDiv = $("#dailyResult");
@@ -241,6 +289,7 @@
var year = yesterday.getFullYear(); var year = yesterday.getFullYear();
var month = String(yesterday.getMonth() + 1).padStart(2, '0'); var month = String(yesterday.getMonth() + 1).padStart(2, '0');
var day = String(yesterday.getDate()).padStart(2, '0'); var day = String(yesterday.getDate()).padStart(2, '0');
$("#targetHour").val(year + month + day);
$("#targetDate").val(year + month + day); $("#targetDate").val(year + month + day);
// 전월 // 전월
@@ -255,6 +304,12 @@
$("#targetYear").val(lastYear); $("#targetYear").val(lastYear);
// 버튼 이벤트 // 버튼 이벤트
$("#btn_execute_hourly").click(function() {
if (confirm("시간별 통계 집계를 실행하시겠습니까?")) {
executeAggregationHourly();
}
});
$("#btn_execute_daily").click(function() { $("#btn_execute_daily").click(function() {
if (confirm("시간별→일별 통계 집계를 실행하시겠습니까?")) { if (confirm("시간별→일별 통계 집계를 실행하시겠습니까?")) {
executeHourlyToDaily(); executeHourlyToDaily();
@@ -297,9 +352,26 @@
</ul> </ul>
</div> </div>
<!-- 거래로그 → 시간별 집계 -->
<div class="aggregation-section">
<h3>1. 거래로그 → 시간별 통계 집계</h3>
<div class="aggregation-form">
<label>대상 날짜:</label>
<input type="text" id="targetHour" placeholder="yyyyMMdd" maxlength="8">
<span style="color: #666; font-size: 12px;">(예: 20250120)</span>
<button type="button" class="cssbtn" id="btn_execute_hourly" level="W">
<i class="material-icons">play_arrow</i> 집계 실행
</button>
</div>
<div id="hourlyResult" class="result-area">
<strong>실행 결과:</strong>
<pre></pre>
</div>
</div>
<!-- 시간별 → 일별 집계 --> <!-- 시간별 → 일별 집계 -->
<div class="aggregation-section"> <div class="aggregation-section">
<h3>1. 시간별 → 일별 통계 집계</h3> <h3>2. 시간별 → 일별 통계 집계</h3>
<div class="aggregation-form"> <div class="aggregation-form">
<label>대상 날짜:</label> <label>대상 날짜:</label>
<input type="text" id="targetDate" placeholder="yyyyMMdd" maxlength="8"> <input type="text" id="targetDate" placeholder="yyyyMMdd" maxlength="8">
@@ -316,7 +388,7 @@
<!-- 일별 → 월별 집계 --> <!-- 일별 → 월별 집계 -->
<div class="aggregation-section"> <div class="aggregation-section">
<h3>2. 일별 → 월별 통계 집계</h3> <h3>3. 일별 → 월별 통계 집계</h3>
<div class="aggregation-form"> <div class="aggregation-form">
<label>대상 월:</label> <label>대상 월:</label>
<input type="text" id="targetMonth" placeholder="yyyyMM" maxlength="6"> <input type="text" id="targetMonth" placeholder="yyyyMM" maxlength="6">
@@ -333,7 +405,7 @@
<!-- 월별 → 연별 집계 --> <!-- 월별 → 연별 집계 -->
<div class="aggregation-section"> <div class="aggregation-section">
<h3>3. 월별 → 연별 통계 집계</h3> <h3>4. 월별 → 연별 통계 집계</h3>
<div class="aggregation-form"> <div class="aggregation-form">
<label>대상 연도:</label> <label>대상 연도:</label>
<input type="text" id="targetYear" placeholder="yyyy" maxlength="4"> <input type="text" id="targetYear" placeholder="yyyy" maxlength="4">
@@ -40,7 +40,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -54,9 +54,7 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
@@ -73,7 +71,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -98,8 +96,7 @@
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }, { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}], }],
graphic: [{ graphic: [{
@@ -118,64 +115,7 @@
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
@@ -191,32 +131,13 @@
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
] ]
}; };
callChart.setOption(callOption); callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
} }
function updateCharts(data) { function updateCharts(data) {
@@ -268,8 +189,7 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}], }],
graphic: [{ graphic: [{
@@ -279,22 +199,6 @@
}] }]
}); });
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -302,24 +206,13 @@
series: [ series: [
{ data: successData }, { data: successData },
{ data: timeoutData }, { data: timeoutData },
{ data: systemErrData }, { data: systemErrData }
{ data: bizErrData }
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(6, 8) + '일'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장 // 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data; callChart.rawData = data;
respChart.rawData = data;
} }
function fetchChartData() { function fetchChartData() {
@@ -535,8 +428,7 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -545,10 +437,6 @@
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -580,9 +468,8 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -596,15 +483,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -685,9 +566,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -712,7 +591,7 @@
<tbody> <tbody>
<tr> <tr>
<th style="width:100px;">조회기간</th> <th style="width:100px;">조회기간</th>
<td colspan="3"> <td colspan="5">
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;"> <input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;">
~ ~
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;"> <input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;">
@@ -724,22 +603,20 @@
<td> <td>
<input type="text" name="searchApiName" value="${param.searchApiName}"> <input type="text" name="searchApiName" value="${param.searchApiName}">
</td> </td>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">인스턴스</th> <th style="width:100px;">인스턴스</th>
<td> <td>
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}"> <input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
</td> </td>
</tr> </tr>
<tr> <tr>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">클라이언트ID</th> <th style="width:100px;">클라이언트ID</th>
<td> <td>
<input type="text" name="searchClientId" value="${param.searchClientId}"> <input type="text" name="searchClientId" value="${param.searchClientId}">
</td> </td>
</tr>
<tr>
<th style="width:100px;">Inbound Adapter</th> <th style="width:100px;">Inbound Adapter</th>
<td> <td>
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}"> <input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
@@ -754,12 +631,7 @@
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
@@ -41,7 +41,7 @@
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.view"/>';
var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>'; var url_minute_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -55,9 +55,7 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
@@ -74,7 +72,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -99,8 +97,7 @@
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }, { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}], }],
graphic: [{ graphic: [{
@@ -119,64 +116,7 @@
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
@@ -185,47 +125,24 @@
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
}, },
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 }, legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] }, xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 }, yAxis: { type: 'value', minInterval: 1 },
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
] ]
}; };
callChart.setOption(callOption); callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
// 드릴다운 이벤트 (시간 클릭 시 분단위 화면으로 이동) // 드릴다운 이벤트 (시간 클릭 시 분단위 화면으로 이동)
callChart.on('click', function(params) { callChart.on('click', function(params) {
drillDownToMinute(callChart, params.dataIndex); drillDownToMinute(callChart, params.dataIndex);
}); });
respChart.on('click', function(params) {
drillDownToMinute(respChart, params.dataIndex);
});
} }
function drillDownToMinute(chart, dataIndex) { function drillDownToMinute(chart, dataIndex) {
@@ -308,8 +225,7 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}], }],
graphic: [{ graphic: [{
@@ -319,22 +235,6 @@
}] }]
}); });
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -342,24 +242,13 @@
series: [ series: [
{ data: successData }, { data: successData },
{ data: timeoutData }, { data: timeoutData },
{ data: systemErrData }, { data: systemErrData }
{ data: bizErrData }
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(8, 10) + '시'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장 // 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data; callChart.rawData = data;
respChart.rawData = data;
} }
function fetchChartData() { function fetchChartData() {
@@ -541,8 +430,7 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -551,10 +439,6 @@
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -586,9 +470,8 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -602,15 +485,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -669,9 +546,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -736,12 +611,7 @@
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
@@ -10,7 +10,7 @@
%> %>
<html> <html>
<head> <head>
<title>분별 통계 조회</title> <title>대시보드</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<style> <style>
@@ -33,7 +33,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -47,9 +47,7 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
@@ -66,7 +64,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -91,8 +89,7 @@
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }, { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}], }],
graphic: [{ graphic: [{
@@ -111,64 +108,7 @@
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
@@ -177,7 +117,7 @@
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
}, },
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 }, legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] }, xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 }, yAxis: { type: 'value', minInterval: 1 },
@@ -188,34 +128,12 @@
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [], showSymbol: false }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [], showSymbol: false },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [], showSymbol: false }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [], showSymbol: false }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [], showSymbol: false }
] ]
}; };
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 }
],
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [], showSymbol: false },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [], showSymbol: false },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [], showSymbol: false }
]
};
callChart.setOption(callOption); callChart.setOption(callOption);
respChart.setOption(respOption);
} }
function updateCharts(data) { function updateCharts(data) {
@@ -269,8 +187,7 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}], }],
graphic: [{ graphic: [{
@@ -280,22 +197,6 @@
}] }]
}); });
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -303,20 +204,10 @@
series: [ series: [
{ data: successData }, { data: successData },
{ data: timeoutData }, { data: timeoutData },
{ data: systemErrData }, { data: systemErrData }
{ data: bizErrData }
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
} }
// 조회 기간 검증 및 조정 (최대 1시간) // 조회 기간 검증 및 조정 (최대 1시간)
@@ -552,8 +443,7 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -562,10 +452,6 @@
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -597,9 +483,8 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -613,15 +498,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -711,9 +590,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -733,7 +610,7 @@
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %> <i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button> </button>
</div> </div>
<div class="title" id="title">API 분단위 통계</div> <div class="title" id="title">대시보드</div>
<table class="search_condition" cellspacing="0"> <table class="search_condition" cellspacing="0">
<tbody> <tbody>
<tr> <tr>
@@ -747,72 +624,28 @@
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 1시간, 초과시 자동 조정)</span> <span style="color:#888; font-size:12px; margin-left:10px;">(최대 1시간, 초과시 자동 조정)</span>
</td> </td>
</tr> </tr>
<tr>
<th style="width:100px;">API명</th>
<td>
<input type="text" name="searchApiName" value="${param.searchApiName}">
</td>
<th style="width:100px;">인스턴스</th>
<td>
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
</td>
</tr>
<tr>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">클라이언트ID</th>
<td>
<input type="text" name="searchClientId" value="${param.searchClientId}">
</td>
</tr>
<tr>
<th style="width:100px;">Inbound Adapter</th>
<td>
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
</td>
<th style="width:100px;">Outbound Adapter</th>
<td>
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
</td>
</tr>
</tbody> </tbody>
</table> </table>
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
<div style="margin-top: 20px;"> <div style="margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div class="title" style="margin: 0;">API요약 통계</div> <div class="title" style="margin: 0;">API별 사용현황</div>
<button type="button" class="cssbtn" id="btn_excel_export_summary" level="R"> <button type="button" class="cssbtn" id="btn_excel_export_summary" level="R">
<i class="material-icons">file_download</i> Excel 다운로드 (요약) <i class="material-icons">file_download</i> Excel 다운로드
</button> </button>
</div> </div>
<table id="gridSummary"></table> <table id="gridSummary"></table>
<div id="pagerSummary"></div> <div id="pagerSummary"></div>
</div> </div>
<!-- 상세 그리드 -->
<div style="margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div class="title" style="margin: 0;">상세 통계</div>
<button type="button" class="cssbtn" id="btn_excel_export" level="R">
<i class="material-icons">file_download</i> Excel 다운로드 (상세)
</button>
</div>
<table id="grid"></table>
<div id="pager"></div>
</div>
</div> </div>
</div> </div>
</body> </body>
@@ -40,7 +40,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -54,9 +54,7 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
@@ -73,7 +71,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -98,8 +96,7 @@
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }, { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}], }],
graphic: [{ graphic: [{
@@ -118,64 +115,7 @@
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
@@ -184,39 +124,18 @@
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
}, },
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 }, legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] }, xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 }, yAxis: { type: 'value', minInterval: 1 },
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
] ]
}; };
callChart.setOption(callOption); callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
} }
function updateCharts(data) { function updateCharts(data) {
@@ -268,8 +187,7 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}], }],
graphic: [{ graphic: [{
@@ -279,22 +197,7 @@
}] }]
}); });
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -307,19 +210,10 @@
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(4, 6) + '월'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장 // 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data; callChart.rawData = data;
respChart.rawData = data;
} }
function fetchChartData() { function fetchChartData() {
@@ -539,8 +433,7 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -549,10 +442,6 @@
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -584,9 +473,8 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -600,15 +488,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -675,9 +557,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -744,14 +624,10 @@
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
<div style="margin-top: 20px;"> <div style="margin-top: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
@@ -40,7 +40,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart; var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) { function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0'; if (cellvalue == null || cellvalue == '') return '0';
@@ -54,9 +54,7 @@
function initCharts() { function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart')); totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart')); callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트 // 총건수 도넛 차트
var totalDonutOption = { var totalDonutOption = {
@@ -73,7 +71,7 @@
legend: { legend: {
orient: 'horizontal', orient: 'horizontal',
bottom: 10, bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류'] data: ['성공', 'Timeout', '시스템오류']
}, },
series: [{ series: [{
name: '총건수', name: '총건수',
@@ -98,8 +96,7 @@
data: [ data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } }, { value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } }, { value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }, { value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
] ]
}], }],
graphic: [{ graphic: [{
@@ -118,64 +115,7 @@
totalDonutChart.setOption(totalDonutOption); totalDonutChart.setOption(totalDonutOption);
// Seq900 도넛 차트
var seq900DonutOption = {
title: {
text: 'Seq900 에러 분포',
left: 'center',
top: 10,
textStyle: { fontSize: 14 }
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
data: ['Timeout', '시스템오류', '업무오류']
},
series: [{
name: 'Seq900 에러',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
formatter: '{b}: {c}'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: [
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
]
}],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '0',
textAlign: 'center',
fill: '#333',
fontSize: 24,
fontWeight: 'bold'
}
}]
};
seq900DonutChart.setOption(seq900DonutOption);
// 호출량 차트 // 호출량 차트
var callOption = { var callOption = {
@@ -184,39 +124,20 @@
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } } axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
}, },
legend: { data: ['성공', 'Timeout', '시스템오류', '업무오류'], bottom: 0 }, legend: { data: ['성공', 'Timeout', '시스템오류'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] }, xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 }, yAxis: { type: 'value', minInterval: 1 },
series: [ series: [
{ name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] }, { name: '성공', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#5470C6' }, data: [] },
{ name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] }, { name: 'Timeout', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FAC858' }, data: [] },
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }, { name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
{ name: '업무오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#FC8452' }, data: [] }
] ]
}; };
callChart.setOption(callOption); callChart.setOption(callOption);
// 응답시간 차트
var respOption = {
title: { text: '응답시간 추이 (ms)', left: 'center', textStyle: { fontSize: 14 } },
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
},
legend: { data: ['P95', 'P50', '평균'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value' },
series: [
{ name: 'P95', type: 'line', smooth: true, itemStyle: { color: '#EE6666' }, lineStyle: { type: 'dashed' }, data: [] },
{ name: 'P50', type: 'line', smooth: true, itemStyle: { color: '#5470C6' }, data: [] },
{ name: '평균', type: 'line', smooth: true, itemStyle: { color: '#91CC75' }, data: [] }
]
};
respChart.setOption(respOption);
} }
function updateCharts(data) { function updateCharts(data) {
@@ -268,8 +189,7 @@
data: [ data: [
{ value: totalSuccess, name: '성공' }, { value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' }, { value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' }, { value: totalSystemErr, name: '시스템오류' }
{ value: totalBizErr, name: '업무오류' }
] ]
}], }],
graphic: [{ graphic: [{
@@ -279,22 +199,7 @@
}] }]
}); });
// Seq900 도넛 차트 업데이트
var seq900Total = seq900Timeout + seq900SystemErr + seq900BizErr;
seq900DonutChart.setOption({
series: [{
data: [
{ value: seq900Timeout, name: 'Timeout' },
{ value: seq900SystemErr, name: '시스템오류' },
{ value: seq900BizErr, name: '업무오류' }
]
}],
graphic: [{
style: {
text: seq900Total.toLocaleString()
}
}]
});
// 호출량 차트 업데이트 // 호출량 차트 업데이트
callChart.setOption({ callChart.setOption({
@@ -307,19 +212,10 @@
] ]
}); });
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(0, 4) + '년'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장 // 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data; callChart.rawData = data;
respChart.rawData = data;
} }
function fetchChartData() { function fetchChartData() {
@@ -513,8 +409,7 @@
postData: gridPostData, postData: gridPostData,
colNames: [ colNames: [
'API명', 'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)' '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
], ],
colModel: [ colModel: [
@@ -523,10 +418,6 @@
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '120', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '100', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false } { name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -558,9 +449,8 @@
colNames: [ colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID', '통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter', 'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류', '총건수', '성공', 'Timeout', '시스템오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류', '평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
], ],
colModel: [ colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false }, { name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -574,15 +464,9 @@
{ name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'successCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'timeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false }, { name: 'systemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'bizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900TimeoutCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900SystemErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'seq900BizErrCnt', align: 'right', width: '70', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'avgRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'minRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }, { name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
{ name: 'p50RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false },
{ name: 'p95RespTime', align: 'right', width: '70', formatter: decimalFormatter, sortable: false }
], ],
jsonReader: { repeatitems: false }, jsonReader: { repeatitems: false },
pager: $('#pager'), pager: $('#pager'),
@@ -644,9 +528,7 @@
// 윈도우 리사이즈 시 차트 리사이즈 // 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() { $(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize(); if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize(); if (callChart) callChart.resize();
if (respChart) respChart.resize();
}); });
buttonControl(); buttonControl();
@@ -712,12 +594,7 @@
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="seq900DonutChart" class="chart"></div>
</div>
<!-- 호출량 및 응답시간 차트 (하단 50%씩) -->
<div class="chart-container">
<div id="callChart" class="chart"></div> <div id="callChart" class="chart"></div>
<div id="respChart" class="chart"></div>
</div> </div>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
@@ -0,0 +1,313 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
request.setCharacterEncoding("UTF-8");
%>
<html>
<head>
<title>대시보드</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
<script language="javascript">
var url = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.view"/>';
function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
return Number(cellvalue).toLocaleString();
}
function decimalFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
return (cellvalue).toFixed(2);
}
function getPostData() {
var postData = {}
if (arguments.length == 2) {
postData[arguments[0]] = arguments[1];
}
var searchStartDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDate]").val().replace(/-/g, "");
if (searchStartDate && searchEndDate) {
var start = new Date(searchStartDate.substring(0, 4), parseInt(searchStartDate.substring(4, 6)) - 1, searchStartDate.substring(6, 8));
var end = new Date(searchEndDate.substring(0, 4), parseInt(searchEndDate.substring(4, 6)) - 1, searchEndDate.substring(6, 8));
var diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
if (diffDays > 31) {
alert('조회 기간은 최대 31일까지 가능합니다.');
return null;
}
}
postData.searchType = $('input[name="searchType"]:checked').val();
postData.searchOrgName = $('#searchOrgName').val();
postData.searchApiName = $('#searchApiName').val();
postData.searchStartDateTime = searchStartDate;
postData.searchEndDateTime = searchEndDate;
return postData;
}
function search() {
var postData = getPostData("cmd", "LIST");
if (postData) {
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
}
}
function exportToExcel() {
var postData = getPostData("cmd", "EXCEL_EXPORT");
if (!postData) {
return;
}
postData.serviceType = '${param.serviceType}';
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function() {
console.log('[Excel Export] Response received - Status:', xhr.status);
if (xhr.status === 200) {
var blob = xhr.response;
var contentType = xhr.getResponseHeader('Content-Type');
console.log('[Excel Export] Blob size:', blob.size, 'Content-Type:', contentType);
// JSON 에러 응답인지 확인
if (contentType && contentType.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function() {
try {
var errorObj = JSON.parse(reader.result);
alert(errorObj.message || 'Excel 파일 생성에 실패했습니다.');
} catch (e) {
alert('Excel 파일 생성에 실패했습니다.');
}
};
reader.readAsText(blob);
return;
}
// Blob 크기가 0이면 에러
if (blob.size === 0) {
alert('Excel 파일 생성에 실패했습니다. (빈 응답)');
return;
}
// 파일명 추출
var filename = 'api_stats.xlsx';
var disposition = xhr.getResponseHeader('Content-Disposition');
console.log('[Excel Export] Content-Disposition:', disposition);
if (disposition && disposition.indexOf('filename=') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
filename = decodeURIComponent(filename);
}
}
console.log('[Excel Export] Downloading as:', filename);
// 파일 다운로드
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
console.log('[Excel Export] Download triggered');
} else if (xhr.status === 204) {
alert('조회된 데이터가 없습니다.');
} else {
// 에러 응답 처리
var reader = new FileReader();
reader.onload = function() {
var message = 'Excel 다운로드 중 오류가 발생했습니다.';
console.log('reader.result', reader.result)
try {
var errorObj = JSON.parse(reader.result);
if (errorObj.message) message = errorObj.message;
} catch (e) {
console.error('[Excel Export] Error parsing error response:', e);
}
alert(message);
};
reader.readAsText(xhr.response);
}
};
xhr.onerror = function() {
console.error('[Excel Export] Network error');
alert('네트워크 오류가 발생했습니다.');
};
// Form data 생성
var formData = [];
for (var key in postData) {
if (postData.hasOwnProperty(key) && postData[key] != null && postData[key] !== '') {
formData.push(encodeURIComponent(key) + '=' + encodeURIComponent(postData[key]));
}
}
xhr.send(formData.join('&'));
}
function list() {
var gridPostData = getPostData("cmd", "LIST");
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
postData: gridPostData,
colNames: [
'구분',
'총건수', '성공', '성공율(%)', '실패율(%)', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
{ name: 'orgName', align: 'left', width: '250', sortable: false },
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'successRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'failRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: 'auto',
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
loadComplete: function(d) {
var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, { sortable: false });
}
}
});
}
$(document).ready(function() {
// 날짜/시간 입력 마스크
$("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true });
$("input[name=searchStartDate], input[name=searchEndDate]").datepicker();
var today = getToday();
var startDate = today;
var endDate = today;
if (!$("input[name=searchStartDate]").val()) {
$("input[name=searchStartDate]").val(startDate);
}
if (!$("input[name=searchEndDate]").val()) {
$("input[name=searchEndDate]").val(endDate);
}
list();
resizeJqGridWidth('grid', 'content_middle', '1200');
$("#btn_search").click(function() {
search();
});
$("#btn_excel").click(function() {
exportToExcel();
});
$("input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) {
$("#btn_search").click();
}
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_excel" level="R" style="display: inline-block;"><i class="material-icons">table_view</i> 엑셀</button>
<button type="button" class="cssbtn" id="btn_search" level="R" style="display: inline-block;">
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button>
</div>
<div class="title" id="title">API 사용현황</div>
<table class="search_condition" cellspacing="0">
<tbody>
<tr>
<th style="width:120px;">조회기간</th>
<td colspan="5">
<input type="text" name="searchStartDate" id="searchStartDate" value="${param.searchStartDate}" style="width:100px;">
~
<input type="text" name="searchEndDate" id="searchEndDate" value="${param.searchEndDate}" style="width:100px;">
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span>
</td>
</tr>
<tr>
<th style="width:120px;">조회구분</th>
<td>
<input type="radio" name="searchType" id="searchTypeORG" value="ORG" checked="checked"><label for="searchTypeORG">제휴사</label></input>
<input type="radio" name="searchType" id="searchTypeAPI" value="API"><label for="searchTypeAPI">API</label></input>
<input type="radio" name="searchType" id="searchTypeDATE" value="DATE"><label for="searchTypeDATE">사용일</label></input>
</td>
<th style="width:120px;">제휴사명</th>
<td>
<input type="text" name="searchOrgName" id="searchOrgName" value="${param.searchOrgName}">
</td>
<th style="width:120px;">API명</th>
<td>
<input type="text" name="searchApiName" id="searchApiName" value="${param.searchApiName}">
</td>
</tr>
</tbody>
</table>
<div>
<table id="grid"></table>
<div id="pager"></div>
</div>
</div>
</div>
</body>
</html>
@@ -1624,12 +1624,12 @@
x-effect="if (apiInterface.syncAsyncType !== 'async') apiInterface.requestType = 'S'"> x-effect="if (apiInterface.syncAsyncType !== 'async') apiInterface.requestType = 'S'">
<input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeRequest" <input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeRequest"
x-model="apiInterface.requestType" x-model="apiInterface.requestType"
value="S" :disabled="apiInterface.syncAsyncType !== 'async' || $store.formState.isReqResDisabled"> value="S" :disabled="$store.formState.isReqResDisabled">
<label class="btn btn-outline-primary" for="btnTypeRequest"><i class="bi bi-arrow-bar-right"></i> 요청</label> <label class="btn btn-outline-primary" for="btnTypeRequest"><i class="bi bi-arrow-bar-right"></i> 요청</label>
<input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeResponse" <input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeResponse"
x-model="apiInterface.requestType" x-model="apiInterface.requestType"
value="R" :disabled="apiInterface.syncAsyncType !== 'async' || $store.formState.isReqResDisabled"> value="R" :disabled="$store.formState.isReqResDisabled">
<label class="btn btn-outline-primary" for="btnTypeResponse"><i class="bi bi-arrow-bar-left"></i> 응답</label> <label class="btn btn-outline-primary" for="btnTypeResponse"><i class="bi bi-arrow-bar-left"></i> 응답</label>
</div> </div>
</div> </div>
+9 -5
View File
@@ -202,13 +202,12 @@ dependencies {
implementation 'software.amazon.awssdk:sso:2.20.142' implementation 'software.amazon.awssdk:sso:2.20.142'
implementation 'software.amazon.awssdk:sts:2.20.142' implementation 'software.amazon.awssdk:sts:2.20.142'
implementation ('io.kubernetes:client-java:18.0.1') {
exclude group: 'org.slf4j', module: 'slf4j-api'
exclude group: 'org.slf4j', module: 'logback-classic'
}
implementation group: 'commons-net', name: 'commons-net', version: '3.5' implementation group: 'commons-net', name: 'commons-net', version: '3.5'
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없어 xercesImpl 가 NCDFE 를 일으킴.
// xml-apis 1.4.01 에 그 클래스가 포함됨. 직접 의존성으로 묶어 WAR 에 패키징되도록 함.
implementation 'xml-apis:xml-apis:1.4.01'
testRuntimeOnly 'com.h2database:h2:2.1.214' testRuntimeOnly 'com.h2database:h2:2.1.214'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
@@ -228,6 +227,11 @@ configurations.all {
cacheDynamicVersionsFor 10, 'minutes' cacheDynamicVersionsFor 10, 'minutes'
// Do not cache changing modules // Do not cache changing modules
cacheChangingModulesFor 0, 'seconds' cacheChangingModulesFor 0, 'seconds'
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없음.
// 일부 transitive 가 끌어오는 xml-apis:1.0.b2 (2002, DOM L2) 는 이 클래스를 포함하지 않아
// xercesImpl 가 NoClassDefFoundError 를 일으킴 → 1.4.01 로 강제 통일.
force 'xml-apis:xml-apis:1.4.01'
} }
} }
+11
View File
@@ -0,0 +1,11 @@
import oracledb
conn = oracledb.connect(user='AGWAPP', password='elink1234', dsn='192.168.240.177:1598/DAPM')
cursor = conn.cursor()
cursor.execute("SELECT TABLE_NAME, STATUS FROM DBA_TABLES WHERE OWNER = 'AGWAPP' AND TABLE_NAME LIKE 'API_STATS%' ORDER BY TABLE_NAME")
for row in cursor.fetchall():
print(f" {row[0]} ({row[1]})")
cursor.close()
conn.close()
+12 -12
View File
@@ -1,4 +1,4 @@
CREATE TABLE EMSADM.API_HISTORY CREATE TABLE EMSAPP.API_HISTORY
( (
API_ID VARCHAR2(30), API_ID VARCHAR2(30),
SEQ NUMBER(22), SEQ NUMBER(22),
@@ -9,23 +9,23 @@ CREATE TABLE EMSADM.API_HISTORY
LOB (SAVEDDATA) STORE AS SECUREFILE (TABLESPACE TS_AGW_D01) LOB (SAVEDDATA) STORE AS SECUREFILE (TABLESPACE TS_AGW_D01)
TABLESPACE TS_EMS_D01; TABLESPACE TS_EMS_D01;
COMMENT ON TABLE EMSADM.API_HISTORY IS 'API 변경이력 테이블'; COMMENT ON TABLE EMSAPP.API_HISTORY IS 'API 변경이력 테이블';
COMMENT ON COLUMN EMSADM.API_HISTORY.API_ID IS 'API ID(TSEAIHE01.EAISVCNAME)'; COMMENT ON COLUMN EMSAPP.API_HISTORY.API_ID IS 'API ID(TSEAIHE01.EAISVCNAME)';
COMMENT ON COLUMN EMSADM.API_HISTORY.SEQ IS 'Sequence 값'; COMMENT ON COLUMN EMSAPP.API_HISTORY.SEQ IS 'Sequence 값';
COMMENT ON COLUMN EMSADM.API_HISTORY.SAVEDDATA IS '변경저장 정보'; COMMENT ON COLUMN EMSAPP.API_HISTORY.SAVEDDATA IS '변경저장 정보';
COMMENT ON COLUMN EMSADM.API_HISTORY.USERID IS '사용자ID'; COMMENT ON COLUMN EMSAPP.API_HISTORY.USERID IS '사용자ID';
COMMENT ON COLUMN EMSADM.API_HISTORY.SAVEDDT IS '저장일시'; COMMENT ON COLUMN EMSAPP.API_HISTORY.SAVEDDT IS '저장일시';
CREATE UNIQUE INDEX EMSADM.PK_API_HISTORY ON EMSADM.API_HISTORY CREATE UNIQUE INDEX EMSAPP.PK_API_HISTORY ON EMSAPP.API_HISTORY
(API_ID, SEQ) (API_ID, SEQ)
TABLESPACE TS_EMS_I01; TABLESPACE TS_EMS_I01;
ALTER TABLE EMSADM.API_HISTORY ADD ( ALTER TABLE EMSAPP.API_HISTORY ADD (
CONSTRAINT PK_API_HISTORY CONSTRAINT PK_API_HISTORY
PRIMARY KEY PRIMARY KEY
(API_ID, SEQ) (API_ID, SEQ)
USING INDEX EMSADM.PK_API_HISTORY USING INDEX EMSAPP.PK_API_HISTORY
ENABLE VALIDATE); ENABLE VALIDATE);
GRANT DELETE, INSERT, SELECT, UPDATE ON EMSADM.API_HISTORY TO RL_EMS_ALL; GRANT DELETE, INSERT, SELECT, UPDATE ON EMSAPP.API_HISTORY TO RL_EMS_ALL;
GRANT SELECT ON EMSADM.API_HISTORY TO RL_EMS_SEL; GRANT SELECT ON EMSAPP.API_HISTORY TO RL_EMS_SEL;
+30 -30
View File
@@ -39,37 +39,37 @@
```sql ```sql
-- 개발 서버용 -- 개발 서버용
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', 'http://172.31.32.111:10230/BATAppWeb/EaiBatCall'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', 'http://172.31.32.111:10230/BATAppWeb/EaiBatCall');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', 'http://172.31.35.144:9021'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', 'http://172.31.35.144:9021');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
-- autogen template -- autogen template
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.url', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.url', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', ''); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
``` ```
+4 -4
View File
@@ -163,13 +163,13 @@ SMS 인증번호 발송 - authCode: 123456
```sql ```sql
-- SMS 인증 설정 초기화 -- SMS 인증 설정 초기화
INSERT INTO EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) INSERT INTO EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL)
VALUES ('Monitoring', 'kjb.sms_auth.enabled', 'true'); VALUES ('Monitoring', 'kjb.sms_auth.enabled', 'true');
INSERT INTO EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) INSERT INTO EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL)
VALUES ('Monitoring', 'kjb.sms_auth.mode', 'real'); VALUES ('Monitoring', 'kjb.sms_auth.mode', 'real');
INSERT INTO EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) INSERT INTO EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL)
VALUES ('Monitoring', 'kjb.sms_auth.fixed_value', '654321'); VALUES ('Monitoring', 'kjb.sms_auth.fixed_value', '654321');
``` ```
@@ -187,7 +187,7 @@ SMS 발송을 위해 UMS 관련 프로퍼티가 설정되어 있어야 합니다
### Q: SMS 인증을 일시적으로 비활성화하려면? ### Q: SMS 인증을 일시적으로 비활성화하려면?
```sql ```sql
UPDATE EMSADM.TSEAIRM24 UPDATE EMSAPP.TSEAIRM24
SET PRPTY2VAL = 'false' SET PRPTY2VAL = 'false'
WHERE PRPTYGROUPNAME = 'Monitoring' AND PRPTYNAME = 'kjb.sms_auth.enabled'; WHERE PRPTYGROUPNAME = 'Monitoring' AND PRPTYNAME = 'kjb.sms_auth.enabled';
``` ```
+81 -271
View File
@@ -9,7 +9,7 @@
## 기술 스택 ## 기술 스택
- Spring Framework 5.3.27 - Spring Framework 5.3.27
- Java 8 - Java 17
- Gradle 8.7 - Gradle 8.7
- Oracle 19c - Oracle 19c
- Spring Data JPA 2.5.2 - Spring Data JPA 2.5.2
@@ -23,251 +23,61 @@
### 사전 요구사항 ### 사전 요구사항
- JDK 8 이상 - JDK 17 이상
- Gradle 8.7 - Gradle 8.7
- Git - Git
- Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB) - Oracle 19c (또는 개발 환경에 따라 접근 가능한 DB)
### Git Bash에서 초기 세팅
```bash
# ====================================
# 1. 변수 설정 (프로젝트 경로 및 브랜치)
# ====================================
export WORKSPACE_DIR="/c/eactive/workspaces/kjb-eapim"
export PROJECT_BRANCH="jenkins_with_weblogic"
# Git Repository URL 설정
# 기본: ssh://git@192.168.240.178:18081/eapim
# 대체: https://git.eactive.synology.me:8090/kjb-eapim
export GIT_REPO_BASE="ssh://git@192.168.240.178:18081/eapim"
# export GIT_REPO_BASE="https://git.eactive.synology.me:8090/kjb-eapim"
# Gradle 명령어 설정
# 일반 환경: gradle
# msi-gf63 환경: /c/eactive/workspaces/shell-scripts/kjb-gradle.sh
export GRADLE_CMD="gradle"
# export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
# ====================================
# 2. 작업 디렉토리 생성
# ====================================
mkdir -p $WORKSPACE_DIR
cd $WORKSPACE_DIR
# ====================================
# 3. eapim-admin 프로젝트 클론
# ====================================
git clone ${GIT_REPO_BASE}/eapim-admin.git
cd eapim-admin
# 프로젝트 브랜치로 체크아웃
git checkout $PROJECT_BRANCH
# ====================================
# 4. eapim-online 프로젝트 클론 (코어 모듈 포함)
# ====================================
cd $WORKSPACE_DIR
git clone ${GIT_REPO_BASE}/eapim-online.git
cd eapim-online
git checkout $PROJECT_BRANCH
# ====================================
# 5. 의존 모듈 클론
# ====================================
cd $WORKSPACE_DIR
# elink-portal-common 클론
git clone ${GIT_REPO_BASE}/elink-portal-common.git
cd elink-portal-common
git checkout master # 또는 필요한 브랜치
# kjb-safedb 클론
cd $WORKSPACE_DIR
git clone ${GIT_REPO_BASE}/kjb-safedb.git
cd kjb-safedb
git checkout master # 또는 필요한 브랜치
# ====================================
# 6. 최종 폴더 구조 확인
# ====================================
# kjb-eapim/
# ├── eapim-admin/ # Admin 관리 콘솔 (현재 프로젝트)
# ├── eapim-online/ # Online 게이트웨이 코어 모듈
# │ ├── elink-online-core/
# │ ├── elink-online-core-jpa/
# │ ├── elink-online-transformer/
# │ ├── elink-online-common/
# │ ├── elink-online-emsclient/
# │ └── elink-online-adapter/
# ├── elink-portal-common/ # 포털 공통 컴포넌트 (JPA 엔티티)
# └── kjb-safedb/ # SafeDB 암호화 라이브러리
# ====================================
# 7. eapim-admin 프로젝트로 이동 및 디펜던시 다운로드
# ====================================
cd $WORKSPACE_DIR/eapim-admin
# Gradle 디펜던시 다운로드
$GRADLE_CMD dependencies
```
### Windows CMD에서 초기 세팅
```cmd
REM ====================================
REM 1. 변수 설정 (프로젝트 경로 및 브랜치)
REM ====================================
set WORKSPACE_DIR=C:\eactive\workspaces\kjb-eapim
set PROJECT_BRANCH=jenkins_with_weblogic
REM Git Repository URL 설정
REM 기본: ssh://git@192.168.240.178:18081/eapim
REM 대체: https://git.eactive.synology.me:8090/kjb-eapim
set GIT_REPO_BASE=ssh://git@192.168.240.178:18081/eapim
REM set GIT_REPO_BASE=https://git.eactive.synology.me:8090/kjb-eapim
REM Gradle 명령어 설정
REM 일반 환경: gradle
REM msi-gf63 환경: C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
set GRADLE_CMD=gradle
REM set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
REM ====================================
REM 2. 작업 디렉토리 생성
REM ====================================
if not exist "%WORKSPACE_DIR%" mkdir "%WORKSPACE_DIR%"
cd /d "%WORKSPACE_DIR%"
REM ====================================
REM 3. eapim-admin 프로젝트 클론
REM ====================================
git clone %GIT_REPO_BASE%/eapim-admin.git
cd eapim-admin
REM 프로젝트 브랜치로 체크아웃
git checkout %PROJECT_BRANCH%
REM ====================================
REM 4. eapim-online 프로젝트 클론 (코어 모듈 포함)
REM ====================================
cd /d "%WORKSPACE_DIR%"
git clone %GIT_REPO_BASE%/eapim-online.git
cd eapim-online
git checkout %PROJECT_BRANCH%
REM ====================================
REM 5. 의존 모듈 클론
REM ====================================
cd /d "%WORKSPACE_DIR%"
REM elink-portal-common 클론
git clone %GIT_REPO_BASE%/elink-portal-common.git
cd elink-portal-common
git checkout master
REM kjb-safedb 클론
cd /d "%WORKSPACE_DIR%"
git clone %GIT_REPO_BASE%/kjb-safedb.git
cd kjb-safedb
git checkout master
REM ====================================
REM 6. 최종 폴더 구조 확인
REM ====================================
REM kjb-eapim\
REM ├── eapim-admin\ # Admin 관리 콘솔 (현재 프로젝트)
REM ├── eapim-online\ # Online 게이트웨이 코어 모듈
REM │ ├── elink-online-core\
REM │ ├── elink-online-core-jpa\
REM │ ├── elink-online-transformer\
REM │ ├── elink-online-common\
REM │ ├── elink-online-emsclient\
REM │ └── elink-online-adapter\
REM ├── elink-portal-common\ # 포털 공통 컴포넌트 (JPA 엔티티)
REM └── kjb-safedb\ # SafeDB 암호화 라이브러리
REM ====================================
REM 7. eapim-admin 프로젝트로 이동 및 디펜던시 다운로드
REM ====================================
cd /d "%WORKSPACE_DIR%\eapim-admin"
REM Gradle 디펜던시 다운로드
%GRADLE_CMD% dependencies
```
### 환경별 설정 파일 ### 환경별 설정 파일
프로젝트 클론 후 필요한 환경 설정을 수정하세요: 프로젝트 클론 후 필요한 환경 설정을 수정하세요:
- `WebContent/WEB-INF/properties/env.D.properties` - 개발 환경 - `WebContent/WEB-INF/properties/env.D.properties` - 개발 환경
- `WebContent/WEB-INF/properties/env.T.properties` - 테스트/스테이징 환경 - `WebContent/WEB-INF/properties/env.T.properties` - 테스트/스테이징 환경
- `WebContent/WEB-INF/properties/env.P.properties` - 운영 환경 - `WebContent/WEB-INF/properties/env.P.properties` - 운영 환경
### 주의사항
- **gradlew 사용 금지**: 반드시 시스템에 설치된 gradle을 직접 사용하거나 환경에 맞는 스크립트를 사용하세요 ### 주의사항
- **Gradle 명령어 변수화**: 위의 `GRADLE_CMD` 변수를 설정하면 환경별로 다른 gradle 명령어 사용 가능
- **SafeDB**: 암호화 기능이 필요한 경우 `-Dkjb_safedb.mode=fake` 설정 (개발 환경) 또는 실제 SafeDB 라이브러리 설치 - **SafeDB**: 암호화 기능이 필요한 경우 `-Dkjb_safedb.mode=fake` 설정 (개발 환경) 또는 실제 SafeDB 라이브러리 설치
- **멀티 모듈 구조**: eapim-online의 여러 모듈들을 참조하므로 반드시 eapim-online도 클론해야 합니다 - **멀티 모듈 구조**: eapim-online의 여러 모듈들을 참조하므로 반드시 eapim-online도 클론해야 합니다
## 빌드 및 실행 ## 빌드 및 실행
### Gradle 명령어 설정 ### Gradle 명령어
사용 환경에 따라 적절한 gradle 명령어를 설정하세요: 프로젝트 루트의 Gradle wrapper(`./gradlew`) 를 사용합니다.
**Git Bash:**
```bash
# 일반 환경
export GRADLE_CMD="gradle"
# msi-gf63 환경
export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
```
**Windows CMD:**
```cmd
REM 일반 환경
set GRADLE_CMD=gradle
REM msi-gf63 환경
set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
```
### 빌드 및 실행 명령어 ### 빌드 및 실행 명령어
```bash ```bash
# 표준 빌드 # 표준 빌드
$GRADLE_CMD build ./gradlew build
# Weblogic 배포용 빌드 (테스트 제외) # Weblogic 배포용 빌드 (테스트 제외)
$GRADLE_CMD build -x test -Pprofile=weblogic ./gradlew build -x test -Pprofile=weblogic
# WAR 파일 빌드 # WAR 파일 빌드[ems_stdout.log](../../logs/emsSvr-RinjaeMA/ems_stdout.log)
$GRADLE_CMD war ./gradlew war
# 클린 빌드 # 클린 빌드
$GRADLE_CMD clean build ./gradlew clean build
# 테스트 실행 # 테스트 실행
$GRADLE_CMD test ./gradlew test
# 패키징 없이 클래스만 컴파일 # 패키징 없이 클래스만 컴파일
$GRADLE_CMD classes ./gradlew classes
# QueryDSL Q-classes 및 기타 애노테이션 생성 # QueryDSL Q-classes 및 기타 애노테이션 생성
$GRADLE_CMD compileJava ./gradlew compileJava
# 의존성 트리 보기 # 의존성 트리 보기
$GRADLE_CMD dependencies ./gradlew dependencies
# 사용 가능한 모든 태스크 목록 # 사용 가능한 모든 태스크 목록
$GRADLE_CMD tasks --all ./gradlew tasks --all
``` ```
**주의**: gradlew 사용 금지 - offline gradle 단독 실행
## 프로젝트 구조 ## 프로젝트 구조
### 전체 프로젝트 구조 ### 전체 프로젝트 구조
@@ -428,7 +238,7 @@ WebContent/
``` ```
-Deai.datasource.type=DEV -Deai.datasource.type=DEV
-Dinst.Name=emsSvr11 -Dinst.Name=emsSvr11
-Deai.tableowner=EMSADM -Deai.tableowner=EMSAPP
-Deai.systemmode=D -Deai.systemmode=D
-Dfile.encoding=utf-8 -Dfile.encoding=utf-8
-DLOGBACK_LOG_LEVEL=info -DLOGBACK_LOG_LEVEL=info
@@ -438,7 +248,7 @@ WebContent/
``` ```
-Deai.datasource.type=DEV -Deai.datasource.type=DEV
-Dinst.Name=emsSvr99 -Dinst.Name=emsSvr99
-Deai.tableowner=EMSADM -Deai.tableowner=EMSAPP
-Deai.systemmode=D -Deai.systemmode=D
-Dfile.encoding=utf-8 -Dfile.encoding=utf-8
-Dlogin.mode=db -Dlogin.mode=db
@@ -577,82 +387,82 @@ gradle test --tests "com.eactive.eai.rms.*"
```sql ```sql
-- 로컬 개발용 (외부) -- 로컬 개발용 (외부)
-- EAI, UMS 주소 미기입시 내부적으론 호출 시도 하지 않고 warning 로그만 남기며 정상 처리 -- EAI, UMS 주소 미기입시 내부적으론 호출 시도 하지 않고 warning 로그만 남기며 정상 처리
--insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', ''); --insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
-- insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', ''); -- insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
--insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', ''); --insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', '');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI='); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
commit; commit;
-- 은행 개발 서버용 -- 은행 개발 서버용
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', 'http://172.31.32.111:10230/BATAppWeb/EaiBatCall'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', 'http://172.31.32.111:10230/BATAppWeb/EaiBatCall');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', 'http://172.31.35.144:9021'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', 'http://172.31.35.144:9021');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', 'http://192.168.246.15:8181'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', 'http://192.168.246.15:8181');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI='); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
commit; commit;
-- 은행 운영 서버용 -- 은행 운영 서버용
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', 'http://172.21.1.40:10860/BATAppWeb/EaiBatCall'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai_batch.url', 'http://172.21.1.40:10860/BATAppWeb/EaiBatCall');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.backup_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.interface_id', 'UIEFF00001UAG');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', 'http://172.24.128.66:9021'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', 'http://172.24.128.66:9021');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', 'http://192.168.246.15:8181'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.host_url', 'http://192.168.246.15:8181');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI='); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_uri', '/api/billing/findBillingForCondition');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.commission_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.query_partnercode_uri', '/api/customer/findCustomerByBusinessManRegistrationNo/%s');
insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s'); insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.create_partnercode_uri', '/not-yet-implement/%s');
commit; commit;
``` ```
## 라이선스 ## 라이선스
@@ -255,6 +255,13 @@ public interface MonitoringContext {
// 이중 로그인 허용여부 // 이중 로그인 허용여부
public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED"; public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED";
// 자동 로그아웃 타임아웃 (단위: 분, 기본값: 10)
public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout";
// 웹훅 재전송 설정
public static final String API_WEBHOOK_RETRY_COUNT = "api.webhook.retry_count";
public static final String API_WEBHOOK_RETRY_TIME = "api.webhook.retry_time";
public abstract String getStringProperty(String propertyName); public abstract String getStringProperty(String propertyName);
@@ -617,6 +617,8 @@ public class MainController implements InterceptorSkipController {
menuId = menuId == null ? "" : menuId; menuId = menuId == null ? "" : menuId;
model.addAttribute("mainPage", mainPage); model.addAttribute("mainPage", mainPage);
model.addAttribute("menuId", menuId); model.addAttribute("menuId", menuId);
model.addAttribute("autoLogoutTimeout",
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 10));
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("CURRENT SERVICE TYPE : " + service); logger.debug("CURRENT SERVICE TYPE : " + service);
@@ -1,13 +1,11 @@
package com.eactive.eai.rms.common.scheduler; package com.eactive.eai.rms.common.scheduler;
import com.eactive.eai.rms.common.base.BaseService; import java.text.SimpleDateFormat;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper; import java.util.Date;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI; import java.util.List;
import com.eactive.eai.rms.common.scheduler.ui.ScheduleInfo; import java.util.stream.Collectors;
import com.eactive.eai.rms.common.scheduler.ui.SchedulerUISearch; import java.util.stream.StreamSupport;
import com.eactive.eai.rms.data.entity.man.monitoringCode.service.MonitoringCodeService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobHistoryService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobInfoService;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.quartz.JobDetail; import org.quartz.JobDetail;
import org.quartz.JobKey; import org.quartz.JobKey;
@@ -20,9 +18,14 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.List; import com.eactive.eai.rms.common.base.BaseService;
import java.util.stream.Collectors; import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper;
import java.util.stream.StreamSupport; import com.eactive.eai.rms.common.scheduler.ui.JobInfoUI;
import com.eactive.eai.rms.common.scheduler.ui.ScheduleInfo;
import com.eactive.eai.rms.common.scheduler.ui.SchedulerUISearch;
import com.eactive.eai.rms.data.entity.man.monitoringCode.service.MonitoringCodeService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobHistoryService;
import com.eactive.eai.rms.data.entity.man.scheduler.JobInfoService;
@Service("schedulerService") @Service("schedulerService")
@Transactional(transactionManager = "transactionManagerForEMS") @Transactional(transactionManager = "transactionManagerForEMS")
@@ -122,10 +125,10 @@ public class SchedulerManService extends BaseService {
List<? extends Trigger> triggersOfJob = scheduler.getTriggersOfJob(new JobKey(jobName, Scheduler.DEFAULT_GROUP)); List<? extends Trigger> triggersOfJob = scheduler.getTriggersOfJob(new JobKey(jobName, Scheduler.DEFAULT_GROUP));
if(triggersOfJob.size() > 0) { if(triggersOfJob.size() > 0) {
result.setStartTime(String.valueOf(triggersOfJob.get(0).getStartTime())); result.setStartTime(dateFormat(triggersOfJob.get(0).getStartTime()));
result.setEndTime(String.valueOf(triggersOfJob.get(0).getEndTime())); result.setEndTime(dateFormat(triggersOfJob.get(0).getEndTime()));
result.setPreviousFireTime(String.valueOf(triggersOfJob.get(0).getPreviousFireTime())); result.setPreviousFireTime(dateFormat(triggersOfJob.get(0).getPreviousFireTime()));
result.setNextFireTime(String.valueOf(triggersOfJob.get(0).getNextFireTime())); result.setNextFireTime(dateFormat(triggersOfJob.get(0).getNextFireTime()));
} }
} catch (Exception e) { } catch (Exception e) {
@@ -151,4 +154,13 @@ public class SchedulerManService extends BaseService {
jobInfoService.deleteById(jobName); jobInfoService.deleteById(jobName);
} }
private String dateFormat(Date date) {
if (date == null) {
return "";
} else {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
}
} }
@@ -1,5 +1,6 @@
package com.eactive.eai.rms.common.util; package com.eactive.eai.rms.common.util;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
@@ -191,6 +192,20 @@ public class StringUtils
} }
return sb.toString(); return sb.toString();
} }
public static String toString(Object val) {
return val != null ? val.toString() : null;
}
public static Long toLong(Object val) {
return val != null ? ((Number) val).longValue() : null;
}
public static BigDecimal toDecimal(Object val) {
if (val == null) return null;
if (val instanceof BigDecimal) return (BigDecimal) val;
return new BigDecimal(val.toString());
}
// //
// /** // /**
// * - "" 나 Null 을 입력받아 SPACE 로 변환한다. // * - "" 나 Null 을 입력받아 SPACE 로 변환한다.
@@ -107,6 +107,17 @@ public class UserInfoService extends
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
public List<UserInfo> findAllByRoleId(String roleId) {
QUserInfo qUserInfo = QUserInfo.userInfo;
QUserRole qUserRole = QUserRole.userRole;
return getJPAQueryFactory()
.selectFrom(qUserInfo)
.join(qUserRole).on(qUserInfo.userid.eq(qUserRole.id.userId))
.where(qUserRole.id.roleId.eq(roleId))
.fetch();
}
public Page<UserInfo> findByUsername(Pageable pageable, String username) { public Page<UserInfo> findByUsername(Pageable pageable, String username) {
QUserInfo qUserInfo = QUserInfo.userInfo; QUserInfo qUserInfo = QUserInfo.userInfo;
BooleanExpression predicate = qUserInfo.isNotNull(); BooleanExpression predicate = qUserInfo.isNotNull();
@@ -0,0 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@EMSDataSource
public interface PortalInquiryCommentRepository extends BaseRepository<InquiryComment, String> {
long countByInquiry_Id(String inquiryId);
long countByInquiry_IdAndDelYn(String inquiryId, String delYn);
List<InquiryComment> findByInquiry_IdAndDelYnOrderByCreatedDateAsc(String inquiryId, String delYn);
Optional<InquiryComment> findTopByInquiry_IdAndDelYnOrderByCreatedDateDesc(
String inquiryId, String delYn);
Optional<InquiryComment> findTopByInquiry_IdAndAdminYnAndDelYnOrderByCreatedDateDesc(
String inquiryId, String adminYn, String delYn);
boolean existsByInquiry_IdAndAdminYnAndDelYnAndCreatedDateAfter(
String inquiryId, String adminYn, String delYn, LocalDateTime after);
}
@@ -0,0 +1,31 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import com.eactive.apim.portal.qna.entity.InquiryComment;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalInquiryCommentService extends AbstractEMSDataSerivce<InquiryComment, String, PortalInquiryCommentRepository> {
// AbstractEMSDataSerivce의 repository 필드가 BaseRepository<T,I>로 고정되어
// 커스텀 메서드를 호출할 수 없으므로 별도 필드로 직접 주입
@Autowired
private PortalInquiryCommentRepository inquiryCommentRepository;
public long countByInquiryId(String inquiryId) {
return inquiryCommentRepository.countByInquiry_Id(inquiryId);
}
public long countActiveByInquiryId(String inquiryId) {
return inquiryCommentRepository.countByInquiry_IdAndDelYn(inquiryId, "N");
}
public List<InquiryComment> findByInquiryId(String inquiryId) {
return inquiryCommentRepository.findByInquiry_IdAndDelYnOrderByCreatedDateAsc(inquiryId, "N");
}
}
@@ -4,7 +4,10 @@ import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.eai.data.jpa.BaseRepository; import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource; import com.eactive.eai.rms.data.EMSDataSource;
import java.util.List;
@EMSDataSource @EMSDataSource
public interface PortalInquiryRepository extends BaseRepository<Inquiry, String> { public interface PortalInquiryRepository extends BaseRepository<Inquiry, String> {
List<Inquiry> findByInquiryStatus(String inquiryStatus);
} }
@@ -1,20 +1,28 @@
package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry; package com.eactive.eai.rms.data.entity.onl.apim.portalinquiry;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
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.apim.portal.qna.entity.Inquiry; import com.eactive.apim.portal.qna.entity.Inquiry;
import com.eactive.apim.portal.qna.entity.QInquiry; import com.eactive.apim.portal.qna.entity.QInquiry;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce; import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import com.querydsl.core.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service @Service
@Transactional(transactionManager = "transactionManagerForEMS") @Transactional(transactionManager = "transactionManagerForEMS")
public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String, PortalInquiryRepository> { public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String, PortalInquiryRepository> {
// AbstractEMSDataSerivce의 repository 필드가 BaseRepository<T,I>로 고정되어
// 커스텀 메서드를 호출할 수 없으므로 별도 필드로 직접 주입
@Autowired
private PortalInquiryRepository portalInquiryRepository;
public Page<Inquiry> findAll(Pageable pageable, PortalInquirySearch portalInquirySearch) { public Page<Inquiry> findAll(Pageable pageable, PortalInquirySearch portalInquirySearch) {
QInquiry qInquiry = QInquiry.inquiry; QInquiry qInquiry = QInquiry.inquiry;
@@ -58,4 +66,8 @@ public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String
repository.deleteAll(inquiries); repository.deleteAll(inquiries);
} }
} }
public List<Inquiry> findByInquiryStatus(String inquiryStatus) {
return portalInquiryRepository.findByInquiryStatus(inquiryStatus);
}
} }
@@ -10,6 +10,9 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
@Service @Service
@Transactional(transactionManager = "transactionManagerForEMS") @Transactional(transactionManager = "transactionManagerForEMS")
public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, String, PortalNoticeRepository> { public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, String, PortalNoticeRepository> {
@@ -19,6 +22,10 @@ public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, St
BooleanBuilder predicate = new BooleanBuilder(); BooleanBuilder predicate = new BooleanBuilder();
if (StringUtils.isNotBlank(portalNoticeUISearch.getSearchNoticeType())) {
predicate.and(qPortalNotice.noticeType.eq(portalNoticeUISearch.getSearchNoticeType()));
}
if (StringUtils.isNotBlank(portalNoticeUISearch.getSearchUseYn())) { if (StringUtils.isNotBlank(portalNoticeUISearch.getSearchUseYn())) {
predicate.and(qPortalNotice.useYn.eq(portalNoticeUISearch.getSearchUseYn())); predicate.and(qPortalNotice.useYn.eq(portalNoticeUISearch.getSearchUseYn()));
} }
@@ -28,7 +35,12 @@ public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, St
.or(qPortalNotice.noticeDetail.containsIgnoreCase(portalNoticeUISearch.getSearchSubjectDetail()))); .or(qPortalNotice.noticeDetail.containsIgnoreCase(portalNoticeUISearch.getSearchSubjectDetail())));
} }
return repository.findAll(predicate, pageable); //return repository.findAll(predicate, pageable);
Sort fixYnFirst = Sort.by(Sort.Direction.DESC, "fixYn");
Sort combinedSort = fixYnFirst.and(pageable.getSort());
Pageable fixedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), combinedSort);
return repository.findAll(predicate, fixedPageable);
} }
} }
@@ -5,6 +5,7 @@ import lombok.Data;
@Data @Data
public class PortalNoticeUISearch { public class PortalNoticeUISearch {
private String searchNoticeType;
private String searchUseYn; private String searchUseYn;
private String searchSubjectDetail; private String searchSubjectDetail;
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.apistatus; package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -30,12 +30,12 @@ public class ApiStatus extends AbstractEntity<String> {
private String statusCode; private String statusCode;
@LastModifiedBy // EMSAuditorAware가 자동으로 채움 @LastModifiedBy // EMSAuditorAware가 자동으로 채움
@Column(name = "MOD_USERID", length = 50) @Column(name = "MODIFIED_BY", length = 50)
private String modUserid = "SCHEDULER"; private String modifiedBy = "SCHEDULER";
@LastModifiedDate // save() 자동으로 sysdate 채움 @LastModifiedDate // save() 자동으로 sysdate 채움
@Column(name = "MOD_DATE") @Column(name = "MODIFIED_DATE")
private LocalDateTime modDate; private LocalDateTime modifiedDate;
@Override @Override
public @NonNull String getId() { return eaisvcname; } public @NonNull String getId() { return eaisvcname; }
@@ -0,0 +1,8 @@
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
public interface ApiStatusEvent {
String getEaisvcname();
String getEaisvcdesc();
String getPrevStatusCode();
String getEvent();
}
@@ -0,0 +1,7 @@
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
public interface InflowTokenInsufficient {
String getEaisvcname();
String getEaisvcdesc();
Long getCnt();
}
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow; package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable; import java.io.Serializable;
@@ -17,12 +17,12 @@ import lombok.NonNull;
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true) @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
@Entity @Entity
@Table(name = "TSEAIFR11") @Table(name = "TSEAIFR11")
public class InflowTokenFailLog extends AbstractEntity<InflowTokenFailLogId> implements Serializable { public class InflowTokenInsufficientLog extends AbstractEntity<InflowTokenInsufficientLogId> implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@EmbeddedId @EmbeddedId
@EqualsAndHashCode.Include @EqualsAndHashCode.Include
private InflowTokenFailLogId id; private InflowTokenInsufficientLogId id;
@Column(name = "EAISEVRINSTNCNAME", length = 20) @Column(name = "EAISEVRINSTNCNAME", length = 20)
private String eaisevrinstncname; private String eaisevrinstncname;
@@ -43,5 +43,5 @@ public class InflowTokenFailLog extends AbstractEntity<InflowTokenFailLogId> imp
private String thresholdtimeunit; private String thresholdtimeunit;
@Override @Override
public @NonNull InflowTokenFailLogId getId() { return id; } public @NonNull InflowTokenInsufficientLogId getId() { return id; }
} }
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow; package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable; import java.io.Serializable;
import javax.persistence.Column; import javax.persistence.Column;
@@ -7,7 +7,7 @@ package com.eactive.eai.rms.data.ext.djb.inflow;
@Data @Data
@Embeddable @Embeddable
public class InflowTokenFailLogId implements Serializable { public class InflowTokenInsufficientLogId implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Column(name = "EAIBZWKDSTCD", length = 4) @Column(name = "EAIBZWKDSTCD", length = 4)
@@ -0,0 +1,33 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "PTL_WEBHOOK_REQ")
@Getter
@NoArgsConstructor
public class WebhookReq {
@Id
@Column(name = "ID", length = 36, nullable = false)
private String id;
@Column(name = "ORG_ID", length = 100)
@Comment("법인 ID")
private String orgId;
@Column(name = "TARGET_URL", length = 500, nullable = false)
@Comment("웹훅 수신 URL")
private String targetUrl;
@Column(name = "SECRET", length = 200)
@Comment("HMAC 서명용 Secret Key")
private String secret;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "PTL_WEBHOOK_REQ_API")
@Getter
@NoArgsConstructor
public class WebhookReqApi implements Serializable {
@EmbeddedId
private WebhookReqApiId id;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class WebhookReqApiId implements Serializable {
@Column(name = "WEBHOOK_REQ_ID", nullable = false, length = 36)
@Comment("웹훅 요청 ID")
private String webhookReqId;
@Column(name = "API_ID", nullable = false, length = 100)
@Comment("API ID")
private String apiId;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "PTL_WEBHOOK_REQ_EVENT")
@Getter
@NoArgsConstructor
public class WebhookReqEvent implements Serializable {
@EmbeddedId
private WebhookReqEventId id;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class WebhookReqEventId implements Serializable {
@Column(name = "WEBHOOK_REQ_ID", nullable = false, length = 36)
@Comment("웹훅 요청 ID")
private String webhookReqId;
@Column(name = "EVENT_TYPE", nullable = false, length = 50)
@Comment("이벤트 유형 (CONTROL_START, ERROR_START 등)")
private String eventType;
}
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.webhook; package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -42,6 +42,9 @@ public class WebhookSendLog {
@Column(name = "STATUS_CODE") @Column(name = "STATUS_CODE")
private Integer statusCode; private Integer statusCode;
@Column(name = "ORG_ID")
private String orgId;
@Lob @Lob
@Column(name = "RESPONSE_BODY") @Column(name = "RESPONSE_BODY")
private String responseBody; private String responseBody;
@@ -13,7 +13,9 @@ 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.authserver.QClientEntity;
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;
import com.eactive.eai.data.entity.onl.inflow.QInflowControl; import com.eactive.eai.data.entity.onl.inflow.QInflowControl;
@@ -25,7 +27,6 @@ import com.eactive.eai.rms.onl.manage.inflow.history.InflowControlHistoryManUISe
import com.eactive.eai.rms.onl.manage.inflow.inflow.InflowControlManMapper; import com.eactive.eai.rms.onl.manage.inflow.inflow.InflowControlManMapper;
import com.querydsl.core.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple; import com.querydsl.core.Tuple;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
@@ -33,10 +34,15 @@ import com.querydsl.jpa.impl.JPAQueryFactory;
public class InflowControlService extends AbstractDataService<InflowControl, InflowControlId, InflowControlRepository> { public class InflowControlService extends AbstractDataService<InflowControl, InflowControlId, InflowControlRepository> {
private static final String ADAPTER_TYPE_INFLOW = "01"; private static final String ADAPTER_TYPE_INFLOW = "01";
private static final String INTERFACE_TYPE_INFLOW = "02";
private static final String CLIENT_TYPE_INFLOW = "03";
@PersistenceContext @PersistenceContext
EntityManager entityManager; EntityManager entityManager;
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
EntityManager entityManagerForEMS;
@Autowired @Autowired
InflowControlManMapper mapper; InflowControlManMapper mapper;
@@ -124,7 +130,7 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
.select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc) .select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc)
.from(qEAIMessageEntity) .from(qEAIMessageEntity)
.leftJoin(qInflowControl) .leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq("02").and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname))) .on(qInflowControl.id.type.eq(INTERFACE_TYPE_INFLOW).and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
.where(qEAIMessageEntity.eaisvcname.contains(searchName)) .where(qEAIMessageEntity.eaisvcname.contains(searchName))
.orderBy(qEAIMessageEntity.eaisvcname.asc()) .orderBy(qEAIMessageEntity.eaisvcname.asc())
.offset(pageable.getOffset()) .offset(pageable.getOffset())
@@ -144,6 +150,8 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
return new PageImpl<>(dtoList, pageable, totalCount); return new PageImpl<>(dtoList, pageable, totalCount);
} }
private InflowControlServiceDto toDto(QEAIMessageEntity qEAIMessageEntity, QInflowControl qInflowControl, private InflowControlServiceDto toDto(QEAIMessageEntity qEAIMessageEntity, QInflowControl qInflowControl,
Tuple tuple) { Tuple tuple) {
InflowControlServiceDto dto = null; InflowControlServiceDto dto = null;
@@ -167,13 +175,75 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
.select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc) .select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc)
.from(qEAIMessageEntity) .from(qEAIMessageEntity)
.leftJoin(qInflowControl) .leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq("02").and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname))) .on(qInflowControl.id.type.eq(INTERFACE_TYPE_INFLOW).and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
.where(qEAIMessageEntity.eaisvcname.eq(eaisvcname)) .where(qEAIMessageEntity.eaisvcname.eq(eaisvcname))
.fetchOne(); .fetchOne();
return toDto(qEAIMessageEntity, qInflowControl, tuple); return toDto(qEAIMessageEntity, qInflowControl, tuple);
} }
public InflowControlServiceDto findByIdForClient(String clientId) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControl qInflowControl = QInflowControl.inflowControl;
QClientEntity qClientEntity = QClientEntity.clientEntity;
Tuple tuple = jpaQueryFactory
.select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
.from(qClientEntity)
.leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
.where(qClientEntity.clientid.eq(clientId))
.fetchOne();
return toDto(qClientEntity, qInflowControl, tuple);
}
public Page<InflowControlServiceDto> findAllForClient(Pageable pageable, String searchName) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControl qInflowControl = QInflowControl.inflowControl;
QClientEntity qClientEntity = QClientEntity.clientEntity;
List<Tuple> tupleList = jpaQueryFactory
.select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
.from(qClientEntity)
.leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
.where(qClientEntity.clientname.contains(searchName))
.orderBy(qClientEntity.clientname.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
List<InflowControlServiceDto> dtoList = tupleList
.stream()
.map(tuple -> toDto(qClientEntity, qInflowControl, tuple))
.collect(Collectors.toList());
long totalCount = jpaQueryFactory
.select(qClientEntity.clientname.count())
.from(qClientEntity)
.where(qClientEntity.clientname.contains(searchName))
.fetchOne();
return new PageImpl<>(dtoList, pageable, totalCount);
}
private InflowControlServiceDto toDto(QClientEntity qClientEntity, QInflowControl qInflowControl,
Tuple tuple) {
InflowControlServiceDto dto = null;
if (tuple.get(qInflowControl) != null)
dto = mapper.toDto(tuple.get(qInflowControl));
else {
dto = new InflowControlServiceDto();
dto.setName(tuple.get(qClientEntity.clientid));
dto.setType(CLIENT_TYPE_INFLOW);
}
dto.setDesc(tuple.get(qClientEntity.clientname));
return dto;
}
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 * successCnt) / SUM(successCnt) // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -211,11 +211,10 @@ public class ApiStatsHourService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -293,9 +292,9 @@ public class ApiStatsHourService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -214,11 +214,11 @@ public class ApiStatsMinuteService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt) // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -120,11 +120,11 @@ public class ApiStatsMonthService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt) // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -172,9 +172,9 @@ public class ApiStatsMonthService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -117,11 +117,11 @@ public class ApiStatsYearService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"), q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt) // 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -169,9 +169,9 @@ public class ApiStatsYearService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"), q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"), q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases() Expressions.cases()
.when(q.successCnt.sum().gt(0)) .when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum() .then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class)) .divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null) .otherwise((BigDecimal) null)
.as("avgRespTime"))) .as("avgRespTime")))
.from(q) .from(q)
@@ -1,6 +0,0 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
public interface ApiStatusEvent {
String getEaisvcname();
String getEvent();
}
@@ -1,61 +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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class ApiStatusService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
@Autowired
private ApiStatusRepository apiStatusRepository;
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.getEvent(), newStatusCode);
//TODO: 내부직원 알림발송 & 제휴사 webhook 발송
}
}
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,17 +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 InflowTokenFailRepository extends BaseRepository<InflowTokenFailLog, InflowTokenFailLogId> {
@Query(nativeQuery = true, value =
" SELECT EAISVCNAME" +
" , COUNT(*) AS CNT" +
" FROM TSEAIFR11" +
" WHERE MSGDPSTYMS >= TO_CHAR(SYSTIMESTAMP - NUMTODSINTERVAL(:rangeMinute, 'MINUTE'), 'YYYYMMDDHH24MISSFF3')" +
" GROUP BY EAISVCNAME")
List<InflowTokenFailSummary> countByEaisvcname(@Param("rangeMinute") long rangeMinute);
}
@@ -1,30 +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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class InflowTokenFailService {
private static final Logger log = LoggerFactory.getLogger(InflowTokenFailService.class);
@Autowired
private InflowTokenFailRepository inflowTokenFailRepository;
public void checkRecentFails(long rangeMinute) {
List<InflowTokenFailSummary> rows = inflowTokenFailRepository.countByEaisvcname(rangeMinute);
for (InflowTokenFailSummary summary : rows) {
log.debug("유량제어 토큰 획득 실패: {} 최근 {}분 동안 {}건", summary.getEaisvcname(), rangeMinute, summary.getCnt());
//TOOD: 내부직원 알림 발송
}
}
}
@@ -1,6 +0,0 @@
package com.eactive.eai.rms.data.ext.djb.inflow;
public interface InflowTokenFailSummary {
String getEaisvcname();
Long getCnt();
}
@@ -0,0 +1,41 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class ApiStatusDetectionService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusDetectionService.class);
/**
* 이벤트 감지
* @param event "CONTROL_START" : 점검시작
"CONTROL_END" : 점검종료
"ERROR_START" : 장애시작
"ERROR_END" : 장애종료
"DELAY_START" : 지연시작
"DELAY_END" : 지연종료
* @param apiIds API IDs
*/
public void detect(String event, List<String> apiIds) {
log.debug("이벤트 감지: {} {}", event, apiIds);
}
// API FSM 내 아직 장애로 남아있는 API 목록
public void remainDownApiIds() {
}
// 정상 동작 - detect()에서 일괄 처리 (복구 = 점검종료, 장애종료, 지연종료)
// public void recovered(String event, String[] apiIds) {
//
// }
}
@@ -1,10 +1,12 @@
package com.eactive.eai.rms.data.ext.djb.apistatus; package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.List; import java.util.List;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.jpa.BaseRepository; import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> { public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
@@ -20,9 +22,13 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
* 7. 기타 : STAY * 7. 기타 : STAY
*/ */
@Query(nativeQuery = true, value = @Query(nativeQuery = true, value =
" SELECT EAISVCNAME, EVENT" " SELECT EAISVCNAME"
+ " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC"
+ " , PREV_STATUS_CODE "
+ " , EVENT"
+ " FROM (" + " FROM ("
+ " SELECT EAISVCNAME" + " SELECT EAISVCNAME"
+ " , STATUS_CODE AS PREV_STATUS_CODE "
+ " , CASE WHEN STATUS_CODE != 'C' AND CTRL_YN = 'Y' THEN 'CONTROL_START'" + " , 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 = 'C' AND CTRL_YN = 'N' THEN 'CONTROL_END'"
+ " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'" + " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'"
@@ -32,11 +38,12 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
+ " ELSE 'STAY' END AS EVENT" + " ELSE 'STAY' END AS EVENT"
+ " FROM (" + " FROM ("
+ " SELECT A.EAISVCNAME" + " SELECT A.EAISVCNAME"
+ " , NVL(B.STATUS_CODE, '1') AS STATUS_CODE" + " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
+ " , NVL((SELECT 'Y' FROM TSEAITI01" //TODO: 게시판 임시로 생성하여 사용. 유차장이 완료하면 변경할것
+ " WHERE (TO_CHAR(SYSDATE,'HH24MI') BETWEEN SUBSTR(EAICTRLDSTICCTNT,16,4) AND SUBSTR(EAICTRLDSTICCTNT,21,4)" + " , NVL(( SELECT 'Y' FROM PTL_NOTICE_API X "
+ " OR SUBSTR(EAICTRLDSTICCTNT,16,9) = '0000|0000')" + " WHERE EXISTS (SELECT 1 FROM EMSAPP.PTL_NOTICE Y WHERE X.NOTICE_ID = Y.ID)"
+ " AND A.EAISVCNAME = EAICTRLNAME),'N') AS CTRL_YN" //+ " AND TO_CHAR(SYSDATE, 'YYYYMMDDHH24MI') BETWEEN START_TIME AND END_TIME) "
+ " AND A.EAISVCNAME = X.API_NAME),'N') AS CTRL_YN "
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'" + " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'" + " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
+ " ELSE 'N' END" + " ELSE 'N' END"
@@ -60,7 +67,7 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS DELAY_YN" + " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS DELAY_YN"
+ " FROM TSEAIHE01 A LEFT OUTER JOIN API_STATUS B ON A.EAISVCNAME = B.EAISVCNAME" + " FROM TSEAIHE01 A LEFT OUTER JOIN API_STATUS B ON A.EAISVCNAME = B.EAISVCNAME"
+ " )" + " )"
+ " ) WHERE EVENT != 'STAY'") + " ) A WHERE EVENT != 'STAY'")
List<ApiStatusEvent> findApiStatusEvents( List<ApiStatusEvent> findApiStatusEvents(
@Param("errorRate") int errorRate, @Param("errorRate") int errorRate,
@Param("errorRangeMinute") int errorRangeMinute, @Param("errorRangeMinute") int errorRangeMinute,
@@ -0,0 +1,133 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
import com.eactive.eai.rms.ext.djb.util.UmsManager;
@Service
@Transactional
public class ApiStatusService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
@Autowired
private ApiStatusRepository apiStatusRepository;
@Autowired
private UmsManager ums;
@Autowired
private ApiStatusDetectionService apiStatusDectionService;
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"))
);
HashMap<String, List<String>> eventMap = new HashMap<>();
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);
//이벤트별로 api분리 저장
List<String> apis = (List<String>)eventMap.get(event.getEvent());
if (apis == null) {
apis = new ArrayList<String>();
apis.add(event.getEaisvcname());
eventMap.put(event.getEvent(), apis);
} else {
apis.add(event.getEaisvcname());
}
}
//이벤트별로 ums, webhook을 발송한다
for (Map.Entry<String, List<String>> entry : eventMap.entrySet()) {
String event = entry.getKey();
List<String> apiIds = entry.getValue();
String message = getSwingChatMessage(event, apiIds);
ums.sendMessenger("api-monitor", MessageCode.API_STATUS_CHANGED, message);
//제휴사 웹훅 발송
ums.sendWebhook(event, apiIds);
//개발자포탈 API상태모니터링 화면을 위한 처리
apiStatusDectionService.detect(event, apiIds);
}
}
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;
}
}
private String getSwingChatMessage(String event, List<String> apiIds) {
String message = "";
switch (event) {
case "CONTROL_START":
message = "API 서비스 점검이 시작되었습니다"; //점검
break;
case "CONTROL_END":
message = "API 서비스 점검이 완료되었습니다"; //정상
break;
case "ERROR_START":
message = "API 서비스 장애가 발생하였습니다"; //장애
break;
case "ERROR_END":
message = "API 서비스 장애가 복구되었습니다"; //정상
break;
case "DELAY_START":
message = "API 서비스 지연이 발생하였습니다"; //지연
break;
case "DELAY_END":
message = "API 서비스 지연이 복구되었습니다"; //정상
break;
default:
message = "";
}
return message + "\n- " + String.join(",", apiIds);
}
}
@@ -0,0 +1,21 @@
package com.eactive.eai.rms.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;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLog;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLogId;
public interface InflowTokenRepository extends BaseRepository<InflowTokenInsufficientLog, InflowTokenInsufficientLogId> {
@Query(nativeQuery = true, value =
" SELECT EAISVCNAME" +
" , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC" +
" , COUNT(*) AS CNT" +
" FROM TSEAIFR11 A" +
" WHERE MSGDPSTYMS >= TO_CHAR(SYSTIMESTAMP - NUMTODSINTERVAL(:rangeMinute, 'MINUTE'), 'YYYYMMDDHH24MISSFF3')" +
" GROUP BY EAISVCNAME")
List<InflowTokenInsufficient> countTokenInsufficient(@Param("rangeMinute") long rangeMinute);
}
@@ -0,0 +1,39 @@
package com.eactive.eai.rms.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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.ext.djb.util.UmsManager;
@Service
@Transactional
public class InflowTokenService {
private static final Logger log = LoggerFactory.getLogger(InflowTokenService.class);
@Autowired
private InflowTokenRepository inflowTokenRepository;
@Autowired
private UmsManager ums;
public void checkRecentFails(long rangeMinute) {
List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute);
for (InflowTokenInsufficient info : rows) {
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
String message = "유량제어 토큰 획득 실패하였습니다 \n" + info.getEaisvcname();
ums.sendMessenger("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, message);
}
}
}
@@ -0,0 +1,143 @@
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 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 TSEAICM20 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT' AND B.USEYN = 'Y' " +
" 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;
}
}
@@ -17,7 +17,7 @@ import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil; import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.ext.djb.apistatus.ApiStatusService; import com.eactive.eai.rms.ext.djb.apistatus.ApiStatusService;
import com.eactive.eai.rms.onl.common.util.DateUtil; import com.eactive.eai.rms.onl.common.util.DateUtil;
/** /**
@@ -43,9 +43,9 @@ import com.eactive.eai.rms.onl.common.util.DateUtil;
* </pre> * </pre>
*/ */
@DisallowConcurrentExecution @DisallowConcurrentExecution
public class ApiStatusUpdateJob implements Job { public class ApiStatusMonitorJob implements Job {
private static final Logger log = LoggerFactory.getLogger(ApiStatusUpdateJob.class); private static final Logger log = LoggerFactory.getLogger(ApiStatusMonitorJob.class);
/** Job 파라미터 키: API 장애기준 시간간격(분) */ /** Job 파라미터 키: API 장애기준 시간간격(분) */
public static final String KEY_API_STATUS_ERROR_RANGE_MINUTE = "api.status.error.range_minute"; public static final String KEY_API_STATUS_ERROR_RANGE_MINUTE = "api.status.error.range_minute";
@@ -88,11 +88,11 @@ public class ApiStatusUpdateJob implements Job {
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext"); monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
ApiStatusService apiStatusUpdateService = appContext.getBean(ApiStatusService.class); ApiStatusService apiStatusUpdateService = appContext.getBean(ApiStatusService.class);
DataSourceContextHolder.setDataSourceType( DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW)); DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try { try {
apiStatusUpdateService.updateApiStatus(param); apiStatusUpdateService.updateApiStatus(param);
} catch (Exception e) { } catch (Exception e) {
log.error("ApiStatusUpdateJob execution failed", e); log.error("ApiStatusUpdateJob execution failed", e);
throw new JobExecutionException(e); throw new JobExecutionException(e);
@@ -18,7 +18,7 @@ import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder; import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil; import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.ext.djb.inflow.InflowTokenFailService; import com.eactive.eai.rms.ext.djb.inflow.InflowTokenService;
import com.eactive.eai.rms.onl.common.util.DateUtil; import com.eactive.eai.rms.onl.common.util.DateUtil;
/** /**
@@ -40,9 +40,9 @@ import com.eactive.eai.rms.onl.common.util.DateUtil;
*/ */
@DisallowConcurrentExecution @DisallowConcurrentExecution
public class InflowTokenFailMonitorJob implements Job { public class InflowTokenMonitorJob implements Job {
private static final Logger log = LoggerFactory.getLogger(InflowTokenFailMonitorJob.class); private static final Logger log = LoggerFactory.getLogger(InflowTokenMonitorJob.class);
private transient MonitoringContext monitoringContext; private transient MonitoringContext monitoringContext;
@@ -72,7 +72,7 @@ public class InflowTokenFailMonitorJob implements Job {
} }
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext"); monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
InflowTokenFailService service = appContext.getBean(InflowTokenFailService.class); InflowTokenService service = appContext.getBean(InflowTokenService.class);
DataSourceContextHolder.setDataSourceType( DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW)); DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
@@ -0,0 +1,73 @@
package com.eactive.eai.rms.ext.djb.job;
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.onl.apim.portalInquiry.PortalInquiryClosingService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 매일 00:05 실행 - RESPONDED 상태 문의글 자동 종료 배치
*
* <p>종료 조건 (기본 7일):</p>
* <ul>
* <li>답변완료 후 댓글이 7일간 없는 경우</li>
* <li>관리자 댓글 등록 후 7일간 포탈사용자 댓글이 없는 경우</li>
* </ul>
*
* <p>Cron: 0 5 0 * * ?</p>
*
* <p>Job 파라미터 (JobDataMap):</p>
* <ul>
* <li>{@value #KEY_INQUIRY_COMMENT_CLOSING_DAY}: 종료 기준 일수 (기본값: 7)</li>
* </ul>
*/
@DisallowConcurrentExecution
public class PortalInquiryClosingJob implements Job {
private static final Logger log = LoggerFactory.getLogger(PortalInquiryClosingJob.class);
/** Job 파라미터 키: 댓글종료 기간(일) */
public static final String KEY_INQUIRY_COMMENT_CLOSING_DAY = "inquiry.comment.closing_day";
private static final int DEFAULT_CLOSING_DAYS = 7;
@Autowired
private PortalInquiryClosingService inquiryCommentClosingService;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("*** START PortalInquiryCommentClosingService run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
int closingDays = DEFAULT_CLOSING_DAYS;
String closingDayParam = context.getMergedJobDataMap().getString(KEY_INQUIRY_COMMENT_CLOSING_DAY);
if (closingDayParam != null) {
try {
closingDays = Integer.parseInt(closingDayParam);
} catch (NumberFormatException e) {
log.warn("잘못된 {} 파라미터 값: '{}', 기본값 {}일 사용", KEY_INQUIRY_COMMENT_CLOSING_DAY, closingDayParam, DEFAULT_CLOSING_DAYS);
}
}
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try {
int closedCount = inquiryCommentClosingService.closeExpiredInquiries(closingDays);
log.info("PortalInquiryCommentClosingService 완료: {}건 CLOSED 처리", closedCount);
} catch (Exception e) {
log.error("PortalInquiryCommentClosingService execution failed", e);
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
log.info("*** END PortalInquiryCommentClosingService run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
}
}
@@ -1,29 +1,20 @@
package com.eactive.eai.rms.ext.djb.statistics; package com.eactive.eai.rms.ext.djb.statistics;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDate; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
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 com.eactive.eai.rms.common.vo.GridResponse; import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayService;
import com.eactive.ext.kjb.statistics.mapping.ApiStatsUIMapper;
import com.eactive.ext.kjb.statistics.service.ApiStatsExcelExportService;
import com.eactive.ext.kjb.statistics.ui.ApiStatsChartUI;
import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch; import com.eactive.ext.kjb.statistics.ui.ApiStatsSearch;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI; import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
@@ -35,37 +26,29 @@ import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor @RequiredArgsConstructor
public class ApiUseStatsController { public class ApiUseStatsController {
private final ApiStatsDayService service; private final ApiUseStatsService service;
private final ApiStatsUIMapper mapper; private final ApiUseStatsExcelExportService excelExportService;
private final ApiStatsExcelExportService excelExportService;
@GetMapping(value = "/onl/djb/statistics/apiUseStatsMan.view") @GetMapping(value = "/onl/kjb/statistics/apiUseStatsMan.view")
public String view() { public String view() {
return "/onl/djb/statistics/apiUseStatsMan"; return "/onl/kjb/statistics/apiUseStatsMan";
} }
@PostMapping(value = "/onl/djb/statistics/apiUseStatsMan.json", params = "cmd=LIST") @PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<ApiStatsUI>> selectList(ApiStatsSearch search, Pageable pageable) { public ResponseEntity<GridResponse<ApiStatsUI>> selectList(ApiStatsSearch search, Pageable pageable) {
Pageable sortedPageable = PageRequest.of( Page<ApiStatsUI> page = service.selectList(search, pageable);
pageable.getPageNumber(), return ResponseEntity.ok(new GridResponse<>(page));
pageable.getPageSize(),
Sort.by(Sort.Order.desc("statTime")));
Page<ApiStatsDay> page = service.selectList(search, sortedPageable);
Page<ApiStatsUI> uiPage = page.map(mapper::toVo);
return ResponseEntity.ok(new GridResponse<>(uiPage));
} }
@PostMapping(value = "/onl/djb/statistics/apiStatsDayMan.json", params = "cmd=EXCEL_EXPORT") @PostMapping(value = "/onl/kjb/statistics/apiUseStatsMan.json", params = "cmd=EXCEL_EXPORT")
public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException { public void exportToExcel(ApiStatsSearch search, HttpServletResponse response) throws IOException {
log.info("Excel export started - search: {}", search); log.info("Excel export started - search: {}", search);
try { try {
List<ApiStatsDay> dataList = service.selectListForExcel(search); List<ApiStatsUI> uiList = service.selectList(search);
log.info("Data retrieved - count: {}", dataList.size()); log.info("Data retrieved - count: {}", uiList.size());
if (dataList.isEmpty()) { if (uiList.isEmpty()) {
log.warn("No data found for export"); log.warn("No data found for export");
response.setStatus(HttpServletResponse.SC_NO_CONTENT); response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.setContentType("application/json; charset=UTF-8"); response.setContentType("application/json; charset=UTF-8");
@@ -74,11 +57,6 @@ public class ApiUseStatsController {
return; return;
} }
List<ApiStatsUI> uiList = dataList.stream()
.map(mapper::toVo)
.collect(Collectors.toList());
log.info("Data converted to UI list - count: {}", uiList.size());
String fileName = generateFileName(search); String fileName = generateFileName(search);
log.info("Generating Excel file: {}", fileName); log.info("Generating Excel file: {}", fileName);
@@ -101,10 +79,7 @@ public class ApiUseStatsController {
} }
private String generateFileName(ApiStatsSearch search) { private String generateFileName(ApiStatsSearch search) {
String timeRange = StringUtils.isNotBlank(search.getSearchStartDateTime()) return "API사용현황_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
? search.getSearchStartDateTime()
: LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
return "API사용현황_" + timeRange + ".xlsx";
} }
} }
@@ -0,0 +1,256 @@
package com.eactive.eai.rms.ext.djb.statistics;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import com.eactive.ext.kjb.statistics.ui.ApiStatsUI;
import lombok.extern.slf4j.Slf4j;
/**
* API 통계 Excel Export 서비스
* Hour/Minute 통계 데이터를 Excel 파일로 생성
*/
@Slf4j
@Service
public class ApiUseStatsExcelExportService {
private static final String[] COLUMN_HEADERS = {
"구분", "총건수", "성공", "성공율(%)", "실패율(%)", "Timeout", "시스템오류",
"평균응답(ms)", "최소응답(ms)", "최대응답(ms)"
};
/**
* API 통계 데이터를 Excel 파일로 생성하여 HTTP 응답으로 전송
*
* @param dataList API 통계 데이터 리스트
* @param fileName 다운로드 파일명
* @param response HTTP 응답 객체
* @throws IOException 파일 생성 실패 시
*/
public void exportApiStats(List<ApiStatsUI> dataList, String fileName, HttpServletResponse response)
throws IOException {
log.info("Starting Excel export - rows: {}, filename: {}", dataList.size(), fileName);
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("API통계");
log.debug("Excel sheet created");
// 스타일 생성
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle stringStyle = createStringStyle(workbook);
CellStyle numberStyle = createNumberStyle(workbook);
CellStyle decimalStyle = createDecimalStyle(workbook);
log.debug("Excel styles created");
// 헤더 행 생성
createHeaderRow(sheet, headerStyle);
log.debug("Header row created");
// 데이터 행 생성
int rowNum = 1;
for (ApiStatsUI data : dataList) {
createDataRow(sheet, rowNum++, data, stringStyle, numberStyle, decimalStyle);
}
log.info("Data rows created - count: {}", dataList.size());
// 컬럼 너비 자동 조정
autoSizeColumns(sheet);
log.debug("Column widths adjusted");
// HTTP 응답 설정
setHttpResponse(response, fileName, workbook);
log.info("Excel file sent to response");
} catch (Exception e) {
log.error("Error creating Excel file", e);
throw new IOException("Excel file creation failed: " + e.getMessage(), e);
}
}
/**
* 헤더 스타일 생성 (회색 배경, 볼드, 테두리)
*/
private CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
// 배경색
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
// 테두리
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
// 정렬
style.setAlignment(HorizontalAlignment.CENTER);
// 폰트 (볼드)
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
return style;
}
/**
* 문자열 데이터 스타일 생성 (기본 테두리)
*/
private CellStyle createStringStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
return style;
}
/**
* 숫자 데이터 스타일 생성 (우측정렬 + 쉼표 구분)
*/
private CellStyle createNumberStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.RIGHT);
style.setDataFormat(workbook.createDataFormat().getFormat("#,##0"));
return style;
}
/**
* 소수점 데이터 스타일 생성 (우측정렬 + 소수점 2자리)
*/
private CellStyle createDecimalStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.RIGHT);
style.setDataFormat(workbook.createDataFormat().getFormat("0.00"));
return style;
}
/**
* 헤더 행 생성
*/
private void createHeaderRow(Sheet sheet, CellStyle headerStyle) {
Row headerRow = sheet.createRow(0);
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(COLUMN_HEADERS[i]);
cell.setCellStyle(headerStyle);
}
}
/**
* 데이터 행 생성
*/
private void createDataRow(Sheet sheet, int rowNum, ApiStatsUI data,
CellStyle stringStyle, CellStyle numberStyle, CellStyle decimalStyle) {
Row row = sheet.createRow(rowNum);
int colNum = 0;
createStringCell(row, colNum++, data.getOrgName(), stringStyle);
createLongCell(row, colNum++, data.getTotalCnt(), numberStyle);
createLongCell(row, colNum++, data.getSuccessCnt(), numberStyle);
createDecimalCell(row, colNum++, data.getSuccessRate(), decimalStyle);
createDecimalCell(row, colNum++, data.getFailRate(), decimalStyle);
createLongCell(row, colNum++, data.getTimeoutCnt(), numberStyle);
createLongCell(row, colNum++, data.getSystemErrCnt(), numberStyle);
createDecimalCell(row, colNum++, data.getAvgRespTime(), numberStyle);
createDecimalCell(row, colNum++, data.getMinRespTime(), numberStyle);
createDecimalCell(row, colNum++, data.getMaxRespTime(), numberStyle);
}
/**
* 문자열 셀 생성
*/
private void createStringCell(Row row, int colNum, String value, CellStyle style) {
Cell cell = row.createCell(colNum);
cell.setCellValue(value != null ? value : "");
cell.setCellStyle(style);
}
/**
* Long 타입 숫자 셀 생성
*/
private void createLongCell(Row row, int colNum, Long value, CellStyle style) {
Cell cell = row.createCell(colNum);
if (value != null) {
cell.setCellValue(value.doubleValue());
} else {
cell.setCellValue(0);
}
cell.setCellStyle(style);
}
/**
* BigDecimal 타입 소수점 셀 생성
*/
private void createDecimalCell(Row row, int colNum, BigDecimal value, CellStyle style) {
Cell cell = row.createCell(colNum);
if (value != null) {
cell.setCellValue(value.doubleValue());
} else {
cell.setCellValue(0.0);
}
cell.setCellStyle(style);
}
/**
* 컬럼 너비 자동 조정
*/
private void autoSizeColumns(Sheet sheet) {
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
sheet.autoSizeColumn(i);
// 한글 문자 고려하여 약간 여유 공간 추가
sheet.setColumnWidth(i, sheet.getColumnWidth(i) + 512);
}
}
/**
* HTTP 응답 설정 및 파일 전송
*/
private void setHttpResponse(HttpServletResponse response, String fileName, Workbook workbook)
throws IOException {
// Content-Type 설정
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// Content-Disposition 설정 (파일명 UTF-8 인코딩)
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString())
.replaceAll("\\+", "%20");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
// Workbook을 응답 스트림으로 전송
workbook.write(response.getOutputStream());
response.getOutputStream().flush();
}
}
@@ -0,0 +1,12 @@
package com.eactive.eai.rms.ext.djb.statistics;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDay;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsDayId;
/**
* API 사용현황 Repository
*/
interface ApiUseStatsRepository extends BaseRepository<ApiStatsDay, ApiStatsDayId> {
}
@@ -0,0 +1,209 @@
package com.eactive.eai.rms.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) {
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 TSEAIAU01 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 TSEAIAU01 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 TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
+ where
+ " GROUP BY TO_CHAR(STAT_TIME,'YYYY-MM-DD')"
+ " ORDER BY STAT_TIME";
}
Query dataQuery = entityManager.createNativeQuery(dataSql);
if (search.getParsedStartDate() != null) {
dataQuery.setParameter("searchStartDate", search.getParsedStartDate());
dataQuery.setParameter("searchEndDate", search.getParsedEndDate());
}
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
dataQuery.setParameter("searchOrgName", "%" + search.getSearchOrgName().toUpperCase() + "%");
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
dataQuery.setParameter("searchApiName", "%" + search.getSearchApiName().toUpperCase() + "%");
}
return dataQuery;
}
/**
* Native Query 결과를 ApiStatsUI로 변환
*/
private ApiStatsUI toVO(Object[] row) {
ApiStatsUI vo = new ApiStatsUI();
vo.setOrgName(StringUtils.toString(row[1]));
vo.setTotalCnt(StringUtils.toLong(row[2]));
vo.setSuccessCnt(StringUtils.toLong(row[3]));
vo.setSuccessRate(StringUtils.toDecimal(row[4]).setScale(2, RoundingMode.HALF_UP));
vo.setFailRate(StringUtils.toDecimal(row[5]).setScale(2, RoundingMode.HALF_UP));
vo.setTimeoutCnt(StringUtils.toLong(row[6]));
vo.setSystemErrCnt(StringUtils.toLong(row[7]));
vo.setAvgRespTime(StringUtils.toDecimal(row[8]));
vo.setMinRespTime(StringUtils.toDecimal(row[9]));
vo.setMaxRespTime(StringUtils.toDecimal(row[10]));
return vo;
}
/**
* native SQL용 동적 WHERE 절 생성
*/
private StringBuilder buildNativeWhere(ApiStatsSearch search) {
StringBuilder where = new StringBuilder(" WHERE 1=1");
calculateDateRange(search);
if (search.getParsedStartDate() != null) {
where.append(" AND A.STAT_TIME >= :searchStartDate ");
where.append(" AND A.STAT_TIME <= :searchEndDate ");
}
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
where.append(" AND UPPER(B.ORGNAME) LIKE :searchOrgName ");
}
if (StringUtils.isNotBlank(search.getSearchApiName())) {
where.append(" AND UPPER(C.EAISVCDESC) LIKE :searchApiName ");
}
return where;
}
/**
* 조회 기간 계산 (최대 31일 제한)
*/
private void calculateDateRange(ApiStatsSearch search) {
if (StringUtils.isBlank(search.getSearchStartDateTime())) {
return;
}
LocalDate startDate = LocalDate.parse(search.getSearchStartDateTime(),
DateTimeFormatter.ofPattern("yyyyMMdd"));
LocalDate endDate;
if (StringUtils.isNotBlank(search.getSearchEndDateTime())) {
endDate = LocalDate.parse(search.getSearchEndDateTime(),
DateTimeFormatter.ofPattern("yyyyMMdd"));
if (java.time.Period.between(startDate, endDate).getDays() > MAX_DAYS) {
endDate = startDate.plusDays(MAX_DAYS);
}
} else {
endDate = startDate.plusDays(MAX_DAYS);
}
search.setParsedStartDate(startDate);
search.setParsedEndDate(endDate);
}
}
@@ -0,0 +1,89 @@
package com.eactive.eai.rms.ext.djb.util;
import java.util.HashMap;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageHandlerService;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class UmsManager {
private static final Logger log = LoggerFactory.getLogger(UmsManager.class);
private final MessageHandlerService messageHandlerService;
private final UserInfoService userInfoService;
private final WebhookService webhookService;
/**
* role 역할을 가진 내부직원에게 메신저(Swing chat) 발송
* @param role
* @param messageCode
* @param message
* @return
*/
public void sendMessenger(String roleId, MessageCode messageCode, String message) {
List<UserInfo> users = userInfoService.findAllByRoleId(roleId);
if (users.isEmpty()) {
log.info("sendMessenger: no users found for role '{}'", roleId);
return;
}
for (UserInfo user : users) {
MessageRecipient recipient = MessageRecipient.builder()
.userId(user.getUserid())
.username(user.getUsername())
.build();
HashMap<String, Object> params = new HashMap<>();
params.put("message", message);
messageHandlerService.publishEvent(messageCode, recipient, params);
}
}
/**
* 내부직원에게 메신저 발송
* @param user
* @param messageCode
* @param message
*/
public void sendMessenger(UserInfo user, MessageCode messageCode, String message) {
MessageRecipient recipient = MessageRecipient.builder()
.userId(user.getUserid())
.username(user.getUsername())
.build();
HashMap<String, Object> params = new HashMap<>();
params.put("message", message);
messageHandlerService.publishEvent(messageCode, recipient, params);
}
/**
* event를 구독중인 제휴사에게 웹훅 발송 (비동기)
* @param event
* @param apiIds
*/
@Async("webhookExecutor")
public void sendWebhook(String event, List<String> apiIds) {
List<WebhookSendRequest> list = webhookService.findSendList(event, apiIds);
for (WebhookSendRequest req : list) {
webhookService.send(req);
}
}
}
@@ -0,0 +1,26 @@
package com.eactive.eai.rms.ext.djb.webhook.config;
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 WebhookAsyncConfig {
@Bean(name = "webhookExecutor")
public Executor webhookExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("webhook-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
@@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog; import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest; 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.WebhookReceiveService;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService; import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
@@ -38,16 +38,12 @@ public class WebhookSendController {
public ResponseEntity<Map<String, Object>> sendWebhook( public ResponseEntity<Map<String, Object>> sendWebhook(
@RequestBody WebhookSendRequest request) { @RequestBody WebhookSendRequest request) {
WebhookSendLog result = webhookService.send( webhookService.send(request);
request.getTargetUrl(),
request.getEventType(),
request.getData()
);
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
response.put("logId", result.getId()); // response.put("logId", result.getId());
response.put("success", result.getSuccess()); // response.put("success", result.getSuccess());
response.put("statusCode", result.getStatusCode()); // response.put("statusCode", result.getStatusCode());
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
@@ -6,8 +6,9 @@ import lombok.Setter;
@Getter @Getter
@Setter @Setter
public class WebhookSendRequest { public class WebhookSendRequest {
private String orgId;
private String targetUrl; private String targetUrl;
private String eventType; private String eventType;
private String secret;
private Object data; private Object data;
} }
@@ -0,0 +1,11 @@
package com.eactive.eai.rms.ext.djb.webhook.repository;
import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookReq;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
@EMSDataSource
public interface WebhookReqRepository extends JpaRepository<WebhookReq, String> {
}
@@ -10,7 +10,7 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import com.eactive.eai.rms.data.EMSDataSource; import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog; import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
@Repository @Repository
@EMSDataSource @EMSDataSource
@@ -2,12 +2,16 @@ package com.eactive.eai.rms.ext.djb.webhook.service;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.crypto.Mac; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -18,10 +22,17 @@ import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog; import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqApi;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqEvent;
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookPayload; import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookPayload;
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository; import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -34,19 +45,63 @@ public class WebhookService {
private final WebhookSendLogRepository sendLogRepository; private final WebhookSendLogRepository sendLogRepository;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private final MonitoringContext monitoringContext;
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
private String secretKey = "HW8JtFpkPmQqsVmr0Rb81P4qDypaNIVbGf3ZNMMPfyerhOHshoKABLaMBo1HA4BldTxhw1NjYmVnoPu9IIV7cyRXjecL3b2UctyR7DnVaJausltZLLr8Qm4Bzs4wRmgf"; private EntityManager entityManager;
private static final String HMAC_ALGORITHM = "HmacSHA256"; private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final int MAX_RETRY = 3; private static final int DEFAULT_RETRY_COUNT = 3;
private static final int DEFAULT_RETRY_TIME = 10000;
@SuppressWarnings("unchecked")
public List<WebhookSendRequest> findSendList(String event, List<String> apiIds) {
QWebhookReq req = QWebhookReq.webhookReq;
QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent;
QWebhookReqApi api = QWebhookReqApi.webhookReqApi;
List<Tuple> tuples = new JPAQueryFactory(entityManager)
.select(req.orgId, req.targetUrl, req.secret, api.id.apiId)
.from(req)
.join(evt).on(req.id.eq(evt.id.webhookReqId))
.join(api).on(req.id.eq(api.id.webhookReqId))
.where(evt.id.eventType.eq(event)
.and(api.id.apiId.in(apiIds)))
.fetch();
// orgId + targetUrl + secret 기준으로 그룹핑, apiId를 data(List)로 집계
Map<String, WebhookSendRequest> resultMap = new LinkedHashMap<>();
for (Tuple t : tuples) {
String key = t.get(req.orgId) + "|" + t.get(req.targetUrl);
resultMap.computeIfAbsent(key, k -> {
WebhookSendRequest wr = new WebhookSendRequest();
wr.setOrgId(t.get(req.orgId));
wr.setTargetUrl(t.get(req.targetUrl));
wr.setSecret(t.get(req.secret));
wr.setEventType(event);
wr.setData(new ArrayList<String>());
return wr;
});
((List<String>) resultMap.get(key).getData()).add(t.get(api.id.apiId));
}
return new ArrayList<>(resultMap.values());
}
/** /**
* 웹훅 발송 메인 메서드 * 웹훅 발송 메인 메서드
*/ */
@Transactional @Transactional
public WebhookSendLog send(String targetUrl, String eventType, Object data) { public void send(WebhookSendRequest req) {
String targetUrl = req.getTargetUrl();
String secret = req.getSecret();
String eventType = req.getEventType();
Object data = req.getData();
WebhookSendLog sendLog = new WebhookSendLog(); WebhookSendLog sendLog = new WebhookSendLog();
sendLog.setOrgId(req.getOrgId());
sendLog.setTargetUrl(targetUrl); sendLog.setTargetUrl(targetUrl);
sendLog.setEventType(eventType); sendLog.setEventType(eventType);
@@ -57,7 +112,7 @@ public class WebhookService {
sendLog.setPayload(payloadJson); sendLog.setPayload(payloadJson);
// 2. Secret Key로 HMAC-SHA256 서명 생성 // 2. Secret Key로 HMAC-SHA256 서명 생성
String signature = generateSignature(payloadJson); String signature = generateSignature(secret, payloadJson);
sendLog.setSignature(signature); sendLog.setSignature(signature);
// 3. HTTP 요청 헤더 구성 // 3. HTTP 요청 헤더 구성
@@ -67,18 +122,20 @@ public class WebhookService {
headers.set("X-Webhook-Event", eventType); headers.set("X-Webhook-Event", eventType);
headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp())); headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp()));
// 4. 발송 // 4. 발송 (재시도 포함)
int retryCount = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_COUNT, DEFAULT_RETRY_COUNT);
int retryTime = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_TIME, DEFAULT_RETRY_TIME);
sendLog.setSentAt(LocalDateTime.now()); sendLog.setSentAt(LocalDateTime.now());
HttpEntity<String> request = new HttpEntity<>(payloadJson, headers); HttpEntity<String> httpRequest = new HttpEntity<>(payloadJson, headers);
ResponseEntity<String> response = restTemplate.postForEntity(targetUrl, request, String.class); ResponseEntity<String> response = sendWithRetry(targetUrl, httpRequest, sendLog, retryCount, retryTime);
// 5. 성공 로그 // 5. 성공 로그
sendLog.setStatusCode(response.getStatusCode().value()); sendLog.setStatusCode(response.getStatusCode().value());
sendLog.setResponseBody(response.getBody()); sendLog.setResponseBody(response.getBody());
sendLog.setSuccess(response.getStatusCode().is2xxSuccessful()); sendLog.setSuccess(response.getStatusCode().is2xxSuccessful());
log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}", log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}, retryCount: {}",
eventType, targetUrl, response.getStatusCode()); eventType, targetUrl, response.getStatusCode(), sendLog.getRetryCount());
} catch (HttpClientErrorException | HttpServerErrorException e) { } catch (HttpClientErrorException | HttpServerErrorException e) {
sendLog.setStatusCode(e.getStatusCode().value()); sendLog.setStatusCode(e.getStatusCode().value());
@@ -95,40 +152,46 @@ public class WebhookService {
eventType, targetUrl, e.getMessage()); eventType, targetUrl, e.getMessage());
} }
return sendLogRepository.save(sendLog); sendLogRepository.save(sendLog);
} }
/** /**
* 실패 건 재시도 * 재시도 포함 HTTP 발송
* - 4xx: 클라이언트 오류이므로 즉시 throw (재시도 불필요)
* - 5xx / 네트워크 오류: 최대 RETRY_COUNT회까지 exponential backoff 재시도 (1s → 2s → 4s)
*/ */
@Transactional private ResponseEntity<String> sendWithRetry(String targetUrl, HttpEntity<String> request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception {
public void retryFailedWebhooks() { Exception lastException = null;
List<WebhookSendLog> failedLogs = for (int attempt = 0; attempt <= retryCount; attempt++) {
sendLogRepository.findFailedLogs(MAX_RETRY);
for (WebhookSendLog failedLog : failedLogs) {
try { try {
log.info("[Webhook] 재시도 - id: {}, retry: {}", failedLog.getId(), failedLog.getRetryCount()); if (attempt > 0) {
failedLog.setRetryCount(failedLog.getRetryCount() + 1); long delayMs = retryTime * (1L << (attempt - 1));
sendLogRepository.save(failedLog); log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, targetUrl, delayMs);
Thread.sleep(delayMs);
send(failedLog.getTargetUrl(), failedLog.getEventType(), sendLog.setRetryCount(attempt);
objectMapper.readValue(failedLog.getPayload(), Object.class)); }
return restTemplate.postForEntity(targetUrl, request, String.class);
} catch (HttpClientErrorException e) {
throw e; // 4xx는 재시도 불필요
} catch (Exception e) { } catch (Exception e) {
log.error("[Webhook] 재시도 실패 - id: {}", failedLog.getId(), e); lastException = e;
log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, targetUrl, e.getMessage());
} }
} }
throw lastException;
} }
/** /**
* HMAC-SHA256 서명 생성 * HMAC-SHA256 서명 생성
* Java 17 미만은 HexFormat 미지원이므로 직접 변환 * Java 17 미만은 HexFormat 미지원이므로 직접 변환
*/ */
private String generateSignature(String payload) throws Exception { private String generateSignature(String secret, String payload) throws Exception {
Mac mac = Mac.getInstance(HMAC_ALGORITHM); Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec( SecretKeySpec keySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM); secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
mac.init(keySpec); mac.init(keySpec);
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash); return bytesToHex(hash);
@@ -14,6 +14,7 @@ public class PortalApprovalAuthorizer implements ApprovalAuthorizer {
return approval.getApprovers().stream() return approval.getApprovers().stream()
.filter(this::isCurrentApprover) .filter(this::isCurrentApprover)
.map(this::getApproverId) .map(this::getApproverId)
.filter(java.util.Objects::nonNull)
.anyMatch(id -> id.equals(approverId)); .anyMatch(id -> id.equals(approverId));
} }
@@ -22,6 +23,6 @@ public class PortalApprovalAuthorizer implements ApprovalAuthorizer {
} }
private String getApproverId(Approver approver) { private String getApproverId(Approver approver) {
return approver.getUser().getId(); return approver.getUser() != null ? approver.getUser().getId() : null;
} }
} }
@@ -13,10 +13,16 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.web.SortDefault; import org.springframework.data.web.SortDefault;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller @Controller
@RequiredArgsConstructor @RequiredArgsConstructor
public class PortalApprovalManController extends BaseAnnotationController { public class PortalApprovalManController extends BaseAnnotationController {
@@ -24,8 +30,8 @@ public class PortalApprovalManController extends BaseAnnotationController {
private final PortalApprovalManService portalApprovalManService; private final PortalApprovalManService portalApprovalManService;
@GetMapping(value = "/onl/apim/approval/portalApprovalMan.view") @GetMapping(value = "/onl/apim/approval/portalApprovalMan.view")
public void view() { public void view(Model model) {
// view model.addAttribute("hardDeleteEnabled", portalApprovalManService.isHardDeleteEnabled());
} }
@GetMapping(value = "/onl/apim/approval/portalApprovalMan.view", params = "cmd=DETAIL") @GetMapping(value = "/onl/apim/approval/portalApprovalMan.view", params = "cmd=DETAIL")
@@ -76,4 +82,52 @@ 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);
}
@PostMapping(value = "/onl/apim/approval/portalApprovalMan.json", params = "cmd=UPDATE")
public ResponseEntity<Void> update(String id, String expectEndDate) {
portalApprovalManService.update(id, expectEndDate);
return ResponseEntity.ok().build();
}
} }
@@ -6,6 +6,7 @@ import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
import com.eactive.apim.portal.approval.entity.Approval; import com.eactive.apim.portal.approval.entity.Approval;
import com.eactive.apim.portal.approval.entity.ApprovalType; import com.eactive.apim.portal.approval.entity.ApprovalType;
import com.eactive.apim.portal.approval.statemachine.*; import com.eactive.apim.portal.approval.statemachine.*;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.portaluser.entity.PortalUser; import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository; import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup; import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
@@ -25,6 +26,7 @@ import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgManService;
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserManService; import com.eactive.eai.rms.onl.apim.portaluser.PortalUserManService;
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI; import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI;
import com.eactive.eai.rms.common.acl.user.ui.UserUI; import com.eactive.eai.rms.common.acl.user.ui.UserUI;
import com.eactive.eai.rms.onl.common.exception.BizException;
import com.eactive.eai.rms.onl.transaction.apim.ApiInterfaceService; import com.eactive.eai.rms.onl.transaction.apim.ApiInterfaceService;
import com.eactive.eai.rms.onl.transaction.apim.mapping.ApiSpecUIMapper; import com.eactive.eai.rms.onl.transaction.apim.mapping.ApiSpecUIMapper;
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI; import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI;
@@ -36,6 +38,8 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.*; import java.util.*;
@Service @Service
@@ -43,6 +47,9 @@ import java.util.*;
@RequiredArgsConstructor @RequiredArgsConstructor
public class PortalApprovalManService extends BaseService { public class PortalApprovalManService extends BaseService {
private static final String PORTAL_PROPERTY_GROUP = "Portal";
private static final String HARD_DELETE_FLAG_KEY = "approval.hard-delete.enabled";
private final ApprovalService approvalService; private final ApprovalService approvalService;
private final ApprovalStateMachine approvalStateMachine; private final ApprovalStateMachine approvalStateMachine;
private final AppRequestRepository appRequestRepository; private final AppRequestRepository appRequestRepository;
@@ -59,6 +66,10 @@ public class PortalApprovalManService extends BaseService {
private final ApiGroupUIMapper apiGroupUIMapper; private final ApiGroupUIMapper apiGroupUIMapper;
private final PortalApprovalAuthorizer portalApprovalAuthorizer; private final PortalApprovalAuthorizer portalApprovalAuthorizer;
private final PortalOrgManService portalOrgManService; private final PortalOrgManService portalOrgManService;
private final PortalPropertyService portalPropertyService;
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
private EntityManager entityManager;
public Page<PortalApprovalUI> selectList(Pageable pageable, PortalApprovalUISearch portalApprovalUISearch) { public Page<PortalApprovalUI> selectList(Pageable pageable, PortalApprovalUISearch portalApprovalUISearch) {
Page<Approval> approval = approvalService.findAll(pageable, portalApprovalUISearch); Page<Approval> approval = approvalService.findAll(pageable, portalApprovalUISearch);
@@ -76,6 +87,16 @@ public class PortalApprovalManService extends BaseService {
* 마스킹된 상세 정보 조회 * 마스킹된 상세 정보 조회
*/ */
public PortalApprovalUI selectDetail(String id) { public PortalApprovalUI selectDetail(String id) {
//2026.06.05 관리자가 승인요청을 조회시 심사중으로 상태 변경
//APP 유형이고 REQUESTED 상태인 경우 관리자 조회 시 PROCESSING으로 변경
approvalService.findById(id).ifPresent(approval -> {
if (approval.getApprovalType().equals(ApprovalType.APP)
&& approval.getApprovalStatus() instanceof RequestedState) {
approval.setApprovalStatus(new ProcessingState());
approvalService.save(approval);
}
});
PortalApprovalUI result = getDetailWithMaskOption(id, false); PortalApprovalUI result = getDetailWithMaskOption(id, false);
if (result == null) { if (result == null) {
throw new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id); throw new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id);
@@ -320,6 +341,13 @@ public class PortalApprovalManService extends BaseService {
sendEvent(id, ApprovalEvent.DENY, options); sendEvent(id, ApprovalEvent.DENY, options);
} }
public void update(String id, String expectEndDate) {
Approval approval = approvalService.findById(id)
.orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
approval.setExpectEndDate(expectEndDate);
approvalService.save(approval);
}
public void redeploy(String id) { public void redeploy(String id) {
Map options = new HashMap<>(); Map options = new HashMap<>();
Approval approval = approvalService.findById(id).orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id)); Approval approval = approvalService.findById(id).orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다. ID: " + id));
@@ -330,6 +358,28 @@ public class PortalApprovalManService extends BaseService {
} }
public boolean isHardDeleteEnabled() {
Map<String, String> properties = portalPropertyService.getPortalPropertiesAsMap(PORTAL_PROPERTY_GROUP);
return Boolean.parseBoolean(properties.getOrDefault(HARD_DELETE_FLAG_KEY, "false"));
}
public void hardDelete(String id) {
if (!isHardDeleteEnabled()) {
throw new BizException("승인 요청 완전삭제 기능이 비활성화되어 있습니다. (PTL_PROPERTY: " + HARD_DELETE_FLAG_KEY + ")");
}
// Approval.approvers 가 EAGER + Approver.user 가 NotFound 처리 없음 → 사라진 UserInfo 로 인해
// findById/deleteById 가 hydration 단계에서 실패하는 row 가 존재. 네이티브 SQL 로 우회.
entityManager.createNativeQuery("DELETE FROM PTL_APPROVER WHERE APPROVAL_ID = :id")
.setParameter("id", id)
.executeUpdate();
int affected = entityManager.createNativeQuery("DELETE FROM PTL_APPROVAL WHERE ID = :id")
.setParameter("id", id)
.executeUpdate();
if (affected == 0) {
throw new BizException("승인 정보를 찾을 수 없습니다. ID: " + id);
}
}
public void sendEvent(String id, ApprovalEvent event, Map<String, Object> options) { public void sendEvent(String id, ApprovalEvent event, Map<String, Object> options) {
approvalService.findById(id).ifPresent(approval -> { approvalService.findById(id).ifPresent(approval -> {
options.put("authorizer", portalApprovalAuthorizer); options.put("authorizer", portalApprovalAuthorizer);

Some files were not shown because too many files have changed in this diff Show More