Merge remote-tracking branch 'origin/master' into master

Resolve conflict in PortalNoticeManService.java import block:
union both sides — keep Arrays (HEAD) and LocalDateTime/ArrayList/Collections (origin/master),
all of which are used in the merged body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Rinjae-gf63
2026-06-16 16:41:57 +09:00
145 changed files with 5797 additions and 2318 deletions
+132
View File
@@ -0,0 +1,132 @@
name: eapim-admin CI
on:
push:
branches:
- master
- develop
- stage
- feats/ci-test
jobs:
build:
runs-on: [self-hosted, linux, eapim-admin]
env:
JAVA_HOME: /apps/opts/jdk8
JAVA_HOME_TOMCAT: /apps/opts/jdk17
CATALINA_BASE: /prod/eapim/adminportal
CATALINA_HOME: /prod/eapim/apache-tomcat-9.0.116
CATALINA_PID: /prod/eapim/adminportal/temp/catalina.pid
DEPLOY_HTTP_PORT: "30120"
defaults:
run:
working-directory: eapim-admin
steps:
- name: Checkout eapim-admin (trigger SHA)
uses: http://172.30.1.50:3000/actions/checkout@v4
with:
path: eapim-admin
- name: Checkout sibling modules (master)
working-directory: ${{ github.workspace }}
run: |
set -e
mkdir -p eapim-online
# eapim-online/* — 5 modules referenced from settings.gradle
for REPO in elink-online-core elink-online-core-jpa elink-online-transformer elink-online-common elink-online-emsclient; do
rm -rf "eapim-online/$REPO"
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/$REPO.git \
"eapim-online/$REPO"
done
# standalone siblings
for REPO in elink-portal-common eapim-admin-djb; do
rm -rf "$REPO"
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/$REPO.git \
"$REPO"
done
- name: Verify toolchain
run: |
java -version
gradle --version
- name: Gradle build (no tests)
run: gradle clean build -x test --no-daemon
- name: Upload WAR artifact
uses: http://172.30.1.50:3000/actions/upload-artifact@v3
with:
name: eapim-admin-war-${{ github.sha }}
path: eapim-admin/build/libs/eapim-admin.war
if-no-files-found: error
- name: Stop tomcat (pre-clean)
working-directory: ${{ github.workspace }}
run: |
# 1) systemd 관리 인스턴스 정지 (이번 워크플로가 띄운 것)
systemctl --user stop eapim-admin 2>/dev/null || true
# 2) systemd 외부 detached 인스턴스 정리
if [ -f "$CATALINA_PID" ] && kill -0 "$(cat $CATALINA_PID)" 2>/dev/null; then
JAVA_HOME=$JAVA_HOME_TOMCAT "$CATALINA_HOME/bin/shutdown.sh" 30 -force || true
for i in $(seq 1 30); do
[ -f "$CATALINA_PID" ] && kill -0 "$(cat $CATALINA_PID)" 2>/dev/null || break
sleep 1
done
fi
rm -f "$CATALINA_PID"
- name: Deploy war as /monitoring
working-directory: ${{ github.workspace }}
run: |
DEPLOY_DIR="$CATALINA_BASE/webapps"
rm -rf "$DEPLOY_DIR/monitoring" "$DEPLOY_DIR/monitoring.war"
cp eapim-admin/build/libs/eapim-admin.war "$DEPLOY_DIR/monitoring.war"
ls -la "$DEPLOY_DIR"
- name: Start tomcat and readiness probe (timeout 180s)
working-directory: ${{ github.workspace }}
run: |
LOG="$CATALINA_BASE/logs/catalina.$(date +%Y-%m-%d).log"
BEFORE=0
[ -f "$LOG" ] && BEFORE=$(stat -c %s "$LOG")
# systemd 가 Tomcat 을 새 cgroup 으로 띄움 — runner step 종료와 무관하게 살아남음
systemctl --user start eapim-admin
DEADLINE=$(($(date +%s) + 180))
STATUS=000
while [ $(date +%s) -lt $DEADLINE ]; do
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
"http://localhost:$DEPLOY_HTTP_PORT/monitoring/login.do" 2>/dev/null || echo "000")
case "$STATUS" in
200|302|401)
echo "Readiness OK (/monitoring/login.do HTTP $STATUS)"
break
;;
esac
if [ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | grep -qE "Application listener .* Exception|SEVERE.*startup.Catalina"; then
echo "::error::Startup error detected in log"
tail -c +$((BEFORE+1)) "$LOG" | tail -80
exit 1
fi
sleep 2
done
case "$STATUS" in
200|302|401) ;;
*)
echo "::error::Readiness probe failed within 180s (last HTTP status: $STATUS)"
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -100
exit 1
;;
esac
echo "--- key boot log lines ---"
tail -c +$((BEFORE+1)) "$LOG" | grep -E "Server startup|Deployment of web" | head -10 || true
+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,24 +25,24 @@
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"
@@ -25,24 +25,24 @@
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"
@@ -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"
@@ -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"
@@ -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"
@@ -115,7 +115,7 @@
</bean> </bean>
<bean id="ioAcceptor" class="com.eactive.eai.rms.onl.server.UdpServer" init-method="init" destroy-method="destory"> <bean id="ioAcceptor" class="com.eactive.eai.rms.onl.server.UdpServer" init-method="init" destroy-method="destory">
<property name="port" value="10800" /> <property name="port" value="39126" />
<property name="handler" ref="handler"/> <property name="handler" ref="handler"/>
</bean> </bean>
+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> --%>
@@ -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>
+2 -2
View File
@@ -353,10 +353,10 @@
</div> </div>
<!-- SSO 로그인 버튼 (비상모드 시 숨김) --> <!-- SSO 로그인 버튼 (비상모드 시 숨김) -->
<c:if test="${not emergencyMode}"> <c:if test="${not emergencyMode}">
<div class="form-group"> <!-- <div class="form-group">
<input type="button" id="btn_sso_login" class="form-control btn btn-success rounded submit px-3" <input type="button" id="btn_sso_login" class="form-control btn btn-success rounded submit px-3"
value="SSO Login"> value="SSO Login">
</div> </div> -->
</c:if> </c:if>
<p style="color:red;"><%=resultMsg %></p> <p style="color:red;"><%=resultMsg %></p>
</form> </form>
+88 -1
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>
</body>
<%-- 자동 로그아웃 경고 모달 --%>
<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>
</html> </html>
+2 -2
View File
@@ -551,9 +551,9 @@
</h1><!-- /monitoring/images/top_logo.png --> </h1><!-- /monitoring/images/top_logo.png -->
<div class="topmenu_box"> <div class="topmenu_box">
<ul> <ul>
<li style="width:240px;" id="newDashboardShow"> <li style="width:240px;" id="newDashboardShow">
<a style="width:240px;" <a style="width:240px;"
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>)</span> href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>-<%=System.getProperty("inst.Name") %>)</span>
<span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span> <span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span>
<% if (!"".equals(LastLoginYms.trim())) { %> <% if (!"".equals(LastLoginYms.trim())) { %>
<span style="display:block;font-size:10px;"><%=localeMessage.getString("screen.lastLogin") %> : <%=LastLoginYms%> [ <%=LastLoginIp%> ]</span> <span style="display:block;font-size:10px;"><%=localeMessage.getString("screen.lastLogin") %> : <%=LastLoginYms%> [ <%=LastLoginIp%> ]</span>
+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){
$.ajax({ function updateThresholdLabel() {
type : "POST", var timeUnit = $('select[name=thresholdTimeUnit]').val();
url:url, var $input = $('#thresholdInput');
dataType:"json", var $unit = $('#thresholdUnit');
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){ 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=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') {
callback(url,key); if (typeof callback === 'function') {
callback(url, key);
} }
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function detail(url,key){
if (!isDetail)return; function detail(url, key) {
$.ajax({ if (!isDetail) return;
type : "POST", $.ajax({
url:url, type: "POST",
dataType:"json", url: url,
data:{cmd: 'DETAIL', name : key}, dataType: "json",
success:function(json){ data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json; var data = json;
$("input[name=name]").attr('readonly',true); $("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly',true); $("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){ $("#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);
} }
}); });
} }
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); function switchTab(tabName) {
var key ="${param.name}"; $('.detail-tab').removeClass('active');
if (key != "" && key !="null"){ $('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
isDetail = true; $('.tab-content').removeClass('active');
} $('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
init(url,key,detail);
if (tabName === 'status' && isDetail) {
loadBucketStatus();
$("#btn_modify").click(function(){ }
var postData = $('#ajaxForm').serializeArray(); }
if (isDetail){
postData.push({ name: "cmd" , value:"UPDATE"}); function loadBucketStatus() {
}else{ $('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
postData.push({ name: "cmd" , value:"INSERT"});
$.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>');
} }
$.ajax({ });
type : "POST", }
url:url,
data:postData, function formatDateTime(date) {
success:function(args){ var h = date.getHours();
alert("<%= localeMessage.getString("common.saveMsg") %>"); var m = date.getMinutes();
goNav(returnUrl);//LIST로 이동 var s = date.getSeconds();
}, return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
error:function(e){ }
alert(e.responseText);
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(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"});
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl);//LIST로 이동
}, $("#btn_delete").click(function() {
error:function(e){ $(this).blur();
alert(e.responseText); var adapterName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + adapterName + ']</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);//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"/> --%> </div>
<%-- <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 class="title"><%= localeMessage.getString("infAdpConMan.title") %><span class="tooltip" ><%= localeMessage.getString("infAdpConMan.title") %></span></div>
<table id="grid" ></table>
<div id="pager"></div>
<!-- detail -->
<form id="ajaxForm">
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
<td><input type="text" name="name" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.adaDes") %></th>
<td><input type="text" name="desc" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th>
<td><input type="text" name="thresholdPerSecond" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th>
<td><input type="text" name="threshold" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th>
<td>
<div class="select-style">
<select name="thresholdTimeUnit" />
</div>
</td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<div class="select-style">
<select name="useYn" />
</div>
</td>
</tr>
</table>
</form>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
<div class="title-wrap">
<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>
<!-- 탭 네비게이션 -->
<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><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
<td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.adaDes") %></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>
</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,170 +16,501 @@
<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/inflowClientControlMan.json" />'; var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />'; var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
var isDetail = false; var isDetail = false;
function init(url,key,callback){
$.ajax({ function updateThresholdLabel() {
type : "POST", var timeUnit = $('select[name=thresholdTimeUnit]').val();
url:url, var $input = $('#thresholdInput');
dataType:"json", var $unit = $('#thresholdUnit');
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){ 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=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') {
callback(url,key); if (typeof callback === 'function') {
callback(url, key);
} }
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function detail(url,key){
if (!isDetail)return; function detail(url, key) {
$.ajax({ if (!isDetail) return;
type : "POST", $.ajax({
url:url, type: "POST",
dataType:"json", url: url,
data:{cmd: 'DETAIL', name : key}, dataType: "json",
success:function(json){ data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json; var data = json;
$("input[name=name]").attr('readonly',true); $("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly',true); $("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){ $("#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);
} }
}); });
} }
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); function switchTab(tabName) {
var key ="${param.name}"; $('.detail-tab').removeClass('active');
if (key != "" && key !="null"){ $('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
isDetail = true; $('.tab-content').removeClass('active');
} $('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
init(url,key,detail);
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
//버킷 현황 조회
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$("#btn_modify").click(function(){
var postData = $('#ajaxForm').serializeArray(); $.ajax({
if (isDetail){ type: 'POST',
postData.push({ name: "cmd" , value:"UPDATE"}); url: url,
}else{ data: { cmd: 'LIST_BUCKET_STATUS', clientId: "${param.name}" },
postData.push({ name: "cmd" , value:"INSERT"}); 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>');
} }
$.ajax({ });
type : "POST", }
url:url,
data:postData, function formatDateTime(date) {
success:function(args){ var h = date.getHours();
alert("<%= localeMessage.getString("common.saveMsg") %>"); var m = date.getMinutes();
goNav(returnUrl);//LIST로 이동 var s = date.getSeconds();
}, return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
error:function(e){ }
alert(e.responseText);
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(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"});
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl);//LIST로 이동
}, $("#btn_delete").click(function() {
error:function(e){ $(this).blur();
alert(e.responseText); 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);//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();
});
$(document).keydown(function(e) {
if (e.keyCode === 27) {
// ESC 키 처리 (필요시 모달 닫기 등)
}
});
}); });
</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>
</div> </div>
<div class="title">클라이언트 유량제어 <span class="tooltip" >클라이언트 유량제어 </span></div>
<table id="grid" ></table>
<div id="pager"></div>
<!-- detail -->
<form id="ajaxForm">
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;">클라이언트 ID</th>
<td><input type="text" name="name" /></td>
</tr>
<tr>
<th style="width:20%;">클라이언트명</th>
<td><input type="text" name="desc" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th>
<td><input type="text" name="thresholdPerSecond" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th>
<td><input type="text" name="threshold" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th>
<td>
<div class="select-style">
<select name="thresholdTimeUnit" />
</div>
</td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<div class="select-style">
<select name="useYn" />
</div>
</td>
</tr>
</table>
</form>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
<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){
$.ajax({ function updateThresholdLabel() {
type : "POST", var timeUnit = $('select[name=thresholdTimeUnit]').val();
url:url, var $input = $('#thresholdInput');
dataType:"json", var $unit = $('#thresholdUnit');
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){ 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=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') {
callback(url,key); if (typeof callback === 'function') {
callback(url, key);
} }
}, },
error:function(e){ error: function(e) {
alert(e.responseText); alert(e.responseText);
} }
}); });
} }
function detail(url,key){
if (!isDetail)return; function detail(url, key) {
$.ajax({ if (!isDetail) return;
type : "POST", $.ajax({
url:url, type: "POST",
dataType:"json", url: url,
data:{cmd: 'DETAIL', name : key}, dataType: "json",
success:function(json){ data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json; var data = json;
$("input[name=name]").attr('readonly',true); $("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly',true); $("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){ $("#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);
} }
}); });
} }
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn(); function switchTab(tabName) {
var key ="${param.name}"; $('.detail-tab').removeClass('active');
if (key != "" && key !="null"){ $('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
isDetail = true; $('.tab-content').removeClass('active');
} $('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
init(url,key,detail);
if (tabName === 'status' && isDetail) {
loadBucketStatus();
$("#btn_modify").click(function(){ }
var postData = $('#ajaxForm').serializeArray(); }
if (isDetail){
postData.push({ name: "cmd" , value:"UPDATE"}); function loadBucketStatus() {
}else{ $('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
postData.push({ name: "cmd" , value:"INSERT"});
$.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>');
} }
$.ajax({ });
type : "POST", }
url:url,
data:postData, function formatDateTime(date) {
success:function(args){ var h = date.getHours();
alert("<%= localeMessage.getString("common.saveMsg") %>"); var m = date.getMinutes();
goNav(returnUrl);//LIST로 이동 var s = date.getSeconds();
}, return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
error:function(e){ }
alert(e.responseText);
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(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"});
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl);//LIST로 이동
}, $("#btn_delete").click(function() {
error:function(e){ $(this).blur();
alert(e.responseText); var ifName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + ifName + ']</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);//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"/> --%> </div>
<%-- <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 class="title"><%= localeMessage.getString("infIfConMan.title") %><span class="tooltip" ><%= localeMessage.getString("infIfConMan.title") %></span></div>
<table id="grid" ></table>
<div id="pager"></div>
<!-- detail -->
<form id="ajaxForm">
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifNm") %></th>
<td><input type="text" name="name" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infIfConMan.ifDesc") %></th>
<td><input type="text" name="desc" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrPerSecond") %></th>
<td><input type="text" name="thresholdPerSecond" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thr") %></th>
<td><input type="text" name="threshold" /></td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.thrTimeUnit") %></th>
<td>
<div class="select-style">
<select name="thresholdTimeUnit" />
</div>
</td>
</tr>
<tr>
<th style="width:20%;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<div class="select-style">
<select name="useYn" />
</div>
</td>
</tr>
</table>
</form>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
<div class="title-wrap">
<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>
<!-- 탭 네비게이션 -->
<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><%= localeMessage.getString("infIfConMan.ifNm") %></th>
<td><input type="text" name="name" style="width:300px;background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infIfConMan.ifDesc") %></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>
</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>
@@ -34,7 +34,7 @@
, {id:'LLLVAR',name:'[ISO8583] LLLVAR'}, {id:'LLLLVAR',name:'[ISO8583] LLLLVAR'}, {id:'BINARY',name:'[ISO8583] BINARY'}, {id:'LLBIN',name:'[ISO8583] LLBIN'} , {id:'LLLVAR',name:'[ISO8583] LLLVAR'}, {id:'LLLLVAR',name:'[ISO8583] LLLLVAR'}, {id:'BINARY',name:'[ISO8583] BINARY'}, {id:'LLBIN',name:'[ISO8583] LLBIN'}
, {id:'LLLBIN',name:'[ISO8583] LLLBIN'}]; , {id:'LLLBIN',name:'[ISO8583] LLLBIN'}];
const basicDataTypes = [{id:'BigDecimal',name:'BigDecimal'},{id:'String',name:'String'}]; const basicDataTypes = [{id:'BigDecimal',name:'BigDecimal'},{id:'String',name:'String'},{id:'Boolean',name:'Boolean'}];
function gridRenderingInit() { function gridRenderingInit() {
$('#effectGrid').jqGrid({ $('#effectGrid').jqGrid({
@@ -44,14 +44,16 @@
'<%=localeMessage.getString("stdDefaultMan.columDesc")%>', '<%=localeMessage.getString("stdDefaultMan.columDesc")%>',
'<%=localeMessage.getString("stdDefaultMan.columLen")%>', '<%=localeMessage.getString("stdDefaultMan.columLen")%>',
'<%=localeMessage.getString("stdDefaultMan.keyOr")%>', '<%=localeMessage.getString("stdDefaultMan.keyOr")%>',
'<%=localeMessage.getString("stdDefaultMan.ifOr")%>' '<%=localeMessage.getString("stdDefaultMan.ifOr")%>',
'<%=localeMessage.getString("stdDefaultMan.orderSeq")%>'
], ],
colModel:[ colModel:[
{ name : 'COLUMNNAME' , align:'left' , sortable:false }, { name : 'COLUMNNAME' , align:'left' , width: '200px', sortable:false },
{ name : 'COLUMNDESC' , align:'left' }, { name : 'COLUMNDESC' , align:'left' , width: '200px' },
{ name : 'COLUMNLENGTH' , align:'right' }, { name : 'COLUMNLENGTH' , align:'center' , width: '100px' },
{ name : 'ISKEY' , align:'center' }, { name : 'ISKEY' , align:'center', width: '100px' },
{ name : 'ISINTERFACE' , align:'center' } { name : 'ISINTERFACE' , align:'center', width: '100px' },
{ name : 'ORDERSEQ' , align:'center', width: '100px' }
], ],
jsonReader: { jsonReader: {
repeatitems:false repeatitems:false
@@ -38,12 +38,22 @@
var loginUser = '<%= loginVo.getUserId() %>'; var loginUser = '<%= loginVo.getUserId() %>';
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("승인하시겠습니까?")) {
@@ -112,6 +122,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,10 +17,59 @@
<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({
@@ -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() {
@@ -36,6 +36,13 @@
padding-bottom: 10px; padding-bottom: 10px;
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;
@@ -57,6 +64,15 @@
width: 100%; width: 100%;
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;
@@ -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);
@@ -104,6 +134,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();
@@ -114,12 +197,17 @@
placeholder: '여기에 답변을 입력하세요.', placeholder: '여기에 답변을 입력하세요.',
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;
@@ -139,6 +227,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>
@@ -19,10 +19,23 @@
<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}"; var key = "${param.id}";
isDetail = key != "" && key != "null"; isDetail = key != "" && key != "null";
@@ -35,8 +48,7 @@
$("#btn_modify").html('<i class="material-icons">save</i> <%= localeMessage.getString("button.register") %>'); $("#btn_modify").html('<i class="material-icons">save</i> <%= localeMessage.getString("button.register") %>');
buttonControl(false); buttonControl(false);
} }
// 초기화 로직
$.ajax({ $.ajax({
type : "POST", type : "POST",
url:url, url:url,
@@ -45,7 +57,8 @@
success:function(json){ success:function(json){
combo = json; combo = json;
new makeOptions("CODE","NAME").setObj($("select[name=noticeType]")).setNoValueInclude(false).setData(json.noticeTypeList).rendering(); new makeOptions("CODE","NAME").setObj($("select[name=noticeType]")).setNoValueInclude(false).setData(json.noticeTypeList).rendering();
toggleIncidentFields();
if (key) { if (key) {
detail(key); detail(key);
} }
@@ -56,6 +69,22 @@
}); });
} }
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) {
var textArea = document.createElement('textarea'); var textArea = document.createElement('textarea');
textArea.innerHTML = text; textArea.innerHTML = text;
@@ -73,57 +102,100 @@
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>') .text(file.originalFileName + '.' + file.fileExtension + ' (' + formatFileSize(file.size) + ')')
.addClass('fileName') .css('cursor', 'pointer')
.text(file.originalFileName + '.' + file.fileExtension + ' (' + formatFileSize(file.size) + ')') .on('click', function() { if (file.fileId) downloadFile(file.fileId); });
.css('cursor', 'pointer')
.on('click', function() {
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);
@@ -135,7 +207,18 @@
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,
@@ -146,24 +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();
init(); init();
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원) initSummernote('#contents', { placeholder: '여기에 내용을 입력하세요.', height: 300 });
initSummernote('#contents', {
placeholder: '여기에 내용을 입력하세요.',
height: 300
});
$('#noticeType').on('change', toggleIncidentFields);
$('#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];
@@ -171,52 +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();
return; if (isIncidentType(noticeType)) {
if (!$('#startedAt').val()) {
alert('시작 시각을 입력해 주십시오.');
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("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);
}
}); });
}); });
@@ -224,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>
@@ -252,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>
@@ -270,9 +357,9 @@
<col style="width: 40%"/> <col style="width: 40%"/>
</colgroup> </colgroup>
<tr> <tr>
<th>게시유형</th> <th>게시유형 <font color="red">*</font></th>
<td colspan="3"> <td colspan="3">
<div class="select-style" > <div class="select-style">
<select name="noticeType" id="noticeType"></select> <select name="noticeType" id="noticeType"></select>
</div> </div>
</td> </td>
@@ -286,7 +373,7 @@
<td> <td>
<input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용 <input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용
</td> </td>
<th>고정여부</th> <th>상단고정</th>
<td> <td>
<input type="checkbox" name="fixYn" id="fixYn" value="Y" ${portalNoticeUI.fixYn eq 'Y' ? 'checked' : ''}> 고정 <input type="checkbox" name="fixYn" id="fixYn" value="Y" ${portalNoticeUI.fixYn eq 'Y' ? 'checked' : ''}> 고정
</td> </td>
@@ -297,14 +384,53 @@
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea> <textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
</td> </td>
</tr> </tr>
<tr>
<th>첨부파일</th> <!-- ───── 장애/점검 전용 영역 ───── -->
<td colspan="3"> <tr class="incident-row" style="display:none;">
<div style="margin: 5px 0px; display: inline-block;"> <th>시작 <font color="red">*</font></th>
<input type="file" id="fileInput" name="files" style="display:none;" /> <td>
<button type="button" id="addFile" class="cssbtn smallBtn">파일 선택</button> <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>
@@ -312,4 +438,4 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>
@@ -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>
<!-- 요약 그리드 --> <!-- 요약 그리드 -->
@@ -752,7 +622,7 @@
<i class="material-icons">file_download</i> Excel 다운로드 (요약) <i class="material-icons">file_download</i> Excel 다운로드 (요약)
</button> </button>
</div> </div>
<table id="gridSummary"></table> <table id="gridSummary"></table>
<div id="pagerSummary"></div> <div id="pagerSummary"></div>
</div> </div>
@@ -10,7 +10,7 @@
%> %>
<html> <html>
<head> <head>
<title>분별 통계 조회</title> <title>대시보드</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/> <jsp:include page="/jsp/common/include/css.jsp"/>
<style> <style>
@@ -33,7 +33,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.json"/>'; var url = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>'; var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
var totalDonutChart, 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> </tbody>
<th style="width:100px;">API명</th>
<td>
<input type="text" name="searchApiName" value="${param.searchApiName}">
</td>
<th style="width:100px;">인스턴스</th>
<td>
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
</td>
</tr>
<tr>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">클라이언트ID</th>
<td>
<input type="text" name="searchClientId" value="${param.searchClientId}">
</td>
</tr>
<tr>
<th style="width:100px;">Inbound Adapter</th>
<td>
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
</td>
<th style="width:100px;">Outbound Adapter</th>
<td>
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
</td>
</tr>
</tbody>
</table> </table>
<!-- 도넛 차트 (상단 50%씩) --> <!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container"> <div class="chart-container">
<div id="totalDonutChart" class="chart"></div> <div id="totalDonutChart" class="chart"></div>
<div id="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>
@@ -1686,7 +1686,7 @@
<option value="none" selected>NONE</option> <option value="none" selected>NONE</option>
<option value="oauth">OAUTH</option> <option value="oauth">OAUTH</option>
<option value="api_key">API_KEY</option> <option value="api_key">API_KEY</option>
<option value="ca">CA</option> <!-- <option value="ca">CA</option> -->
</select> </select>
</div> </div>
</div> </div>
+10 -6
View File
@@ -201,14 +201,13 @@ 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()
+51
View File
@@ -0,0 +1,51 @@
# DJBank EAPIM Admin — user-scope systemd unit
#
# Install (one-time, on the host as `djb`):
# sudo loginctl enable-linger djb # allow services to outlive ssh logout
# mkdir -p ~/.config/systemd/user
# # workflow copies this file in on each deploy; for first boot you can also do:
# # cp /prod/.../deploy/systemd/eapim-admin.service ~/.config/systemd/user/
# systemctl --user daemon-reload
# systemctl --user enable eapim-admin
# systemctl --user start eapim-admin
#
# Why this exists:
# - `catalina.sh start` + `nohup &` does NOT escape the Gitea Actions runner
# process group. When the workflow step finishes, the runner sends SIGTERM
# to the process tree and Tomcat dies seconds after boot.
# - Running Tomcat as a systemd user service detaches lifecycle from the
# runner entirely. The workflow just calls `systemctl --user restart` and
# systemd owns the process.
[Unit]
Description=DJBank EAPIM Admin (Tomcat 9)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=JAVA_HOME=/apps/opts/jdk17
Environment=CATALINA_BASE=/prod/eapim/adminportal
Environment=CATALINA_HOME=/prod/eapim/apache-tomcat-9.0.116
Environment=CATALINA_PID=/prod/eapim/adminportal/temp/catalina.pid
# `catalina.sh run` keeps Tomcat in the foreground so systemd can supervise it.
ExecStart=/prod/eapim/apache-tomcat-9.0.116/bin/catalina.sh run
# Graceful shutdown via Tomcat's shutdown port, 30s window.
ExecStop=/prod/eapim/apache-tomcat-9.0.116/bin/catalina.sh stop 30
# Tomcat returns 143 on SIGTERM during normal stop — treat as success.
SuccessExitStatus=143
TimeoutStartSec=180
TimeoutStopSec=60
Restart=on-failure
RestartSec=10
# Memory / fd limits — adjust as needed.
LimitNOFILE=65535
[Install]
WantedBy=default.target
+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';
``` ```
+82 -272
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;
``` ```
## 라이선스 ## 라이선스
@@ -254,6 +254,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";
@@ -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);
@@ -35,6 +35,7 @@ public class MonitoringPropertyManService extends BaseService {
public MonitoringPropertyGroupUI selectDetail(String prptyGroupName) { public MonitoringPropertyGroupUI selectDetail(String prptyGroupName) {
MonitoringPropertyGroup propertyGroup = propertyGroupService.getById(prptyGroupName); MonitoringPropertyGroup propertyGroup = propertyGroupService.getById(prptyGroupName);
propertyGroup.getMonitoringProperties().sort(Comparator.comparing(p -> p.getId().getPrptyName()));
return propertyGroupMapper.toVo(propertyGroup); return propertyGroupMapper.toVo(propertyGroup);
} }
@@ -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) {
@@ -150,5 +153,14 @@ public class SchedulerManService extends BaseService {
public void delete(String jobName) { public void delete(String jobName) {
jobInfoService.deleteById(jobName); jobInfoService.deleteById(jobName);
} }
private String dateFormat(Date date) {
if (date == null) {
return "";
} else {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
}
} }
@@ -1,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);
}
} }
@@ -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;
@@ -1,7 +1,8 @@
package com.eactive.eai.rms.data.ext.djb.apistatus; package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
public interface ApiStatusEvent { public interface ApiStatusEvent {
String getEaisvcname(); String getEaisvcname();
String getEaisvcdesc(); String getEaisvcdesc();
String getPrevStatusCode();
String getEvent(); String getEvent();
} }
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.data.ext.djb.inflow; package com.eactive.eai.rms.data.entity.onl.djb.inflow;
public interface InflowTokenInsufficient { public interface InflowTokenInsufficient {
String getEaisvcname(); String getEaisvcname();
@@ -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;
@@ -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;
@@ -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;
@@ -1,9 +1,6 @@
package com.eactive.eai.rms.data.entity.onl.inflow; package com.eactive.eai.rms.data.entity.onl.inflow;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
@@ -18,6 +15,7 @@ import org.springframework.stereotype.Service;
import com.eactive.apim.portal.app.entity.QCredential; 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;
@@ -29,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;
@@ -186,101 +183,67 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
} }
public InflowControlServiceDto findByIdForClient(String clientId) { public InflowControlServiceDto findByIdForClient(String clientId) {
QCredential qCredential = QCredential.credential; JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControl qInflowControl = QInflowControl.inflowControl; QInflowControl qInflowControl = QInflowControl.inflowControl;
QClientEntity qClientEntity = QClientEntity.clientEntity;
// EMSADM: Credential 조회 Tuple tuple = jpaQueryFactory
Tuple credTuple = new JPAQueryFactory(entityManagerForEMS) .select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
.select(qCredential.clientid, qCredential.clientname) .from(qClientEntity)
.from(qCredential) .leftJoin(qInflowControl)
.where(qCredential.clientid.eq(clientId)) .on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
.where(qClientEntity.clientid.eq(clientId))
.fetchOne(); .fetchOne();
// AGWADM: InflowControl 조회 return toDto(qClientEntity, qInflowControl, tuple);
InflowControl inflowControl = new JPAQueryFactory(entityManager)
.selectFrom(qInflowControl)
.where(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.eq(clientId)))
.fetchOne();
InflowControlServiceDto dto;
if (inflowControl != null) {
dto = mapper.toDto(inflowControl);
} else {
dto = new InflowControlServiceDto();
dto.setName(clientId);
dto.setType(CLIENT_TYPE_INFLOW);
}
if (credTuple != null) {
dto.setDesc(credTuple.get(qCredential.clientname));
}
return dto;
} }
public Page<InflowControlServiceDto> findAllForClient(Pageable pageable, String searchName) { public Page<InflowControlServiceDto> findAllForClient(Pageable pageable, String searchName) {
QCredential qCredential = QCredential.credential;
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControl qInflowControl = QInflowControl.inflowControl; QInflowControl qInflowControl = QInflowControl.inflowControl;
QClientEntity qClientEntity = QClientEntity.clientEntity;
// EMSADM: Credential 목록 및 건수 조회 List<Tuple> tupleList = jpaQueryFactory
JPAQueryFactory emsFactory = new JPAQueryFactory(entityManagerForEMS); .select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
BooleanBuilder credPredicate = new BooleanBuilder(); .from(qClientEntity)
if (!StringUtils.isEmpty(searchName)) { .leftJoin(qInflowControl)
credPredicate.and(qCredential.clientname.contains(searchName)); .on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
} .and(qInflowControl.id.name.eq(qClientEntity.clientid)))
.where(qClientEntity.clientname.contains(searchName))
long totalCount = emsFactory .orderBy(qClientEntity.clientname.asc())
.select(qCredential.clientid.count())
.from(qCredential)
.where(credPredicate)
.fetchOne();
List<Tuple> credList = emsFactory
.select(qCredential.clientid, qCredential.clientname)
.from(qCredential)
.where(credPredicate)
.orderBy(qCredential.clientname.asc())
.offset(pageable.getOffset()) .offset(pageable.getOffset())
.limit(pageable.getPageSize()) .limit(pageable.getPageSize())
.fetch(); .fetch();
if (credList.isEmpty()) { List<InflowControlServiceDto> dtoList = tupleList
return new PageImpl<>(Collections.emptyList(), pageable, totalCount);
}
// AGWADM: 해당 clientId의 InflowControl 일괄 조회
List<String> clientIds = credList.stream()
.map(t -> t.get(qCredential.clientid))
.collect(Collectors.toList());
Map<String, InflowControl> inflowMap = new JPAQueryFactory(entityManager)
.selectFrom(qInflowControl)
.where(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.in(clientIds)))
.fetch()
.stream() .stream()
.collect(Collectors.toMap(ic -> ic.getId().getName(), ic -> ic)); .map(tuple -> toDto(qClientEntity, qInflowControl, tuple))
// Java에서 병합
List<InflowControlServiceDto> dtoList = credList.stream()
.map(tuple -> {
String clientId = tuple.get(qCredential.clientid);
InflowControl inflowControl = inflowMap.get(clientId);
InflowControlServiceDto dto;
if (inflowControl != null) {
dto = mapper.toDto(inflowControl);
} else {
dto = new InflowControlServiceDto();
dto.setName(clientId);
dto.setType(CLIENT_TYPE_INFLOW);
}
dto.setDesc(tuple.get(qCredential.clientname));
return dto;
})
.collect(Collectors.toList()); .collect(Collectors.toList());
long totalCount = jpaQueryFactory
.select(qClientEntity.clientname.count())
.from(qClientEntity)
.where(qClientEntity.clientname.contains(searchName))
.fetchOne();
return new PageImpl<>(dtoList, pageable, totalCount); 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)
@@ -5,7 +5,9 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils; 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.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -42,7 +44,12 @@ public class ExtendedColumnDefinitionService
predicate = predicate.and(qExtendedColumnDefinition.columnDesc.containsIgnoreCase(searchColumnDesc)); predicate = predicate.and(qExtendedColumnDefinition.columnDesc.containsIgnoreCase(searchColumnDesc));
} }
return repository.findAll(predicate, pageable); Pageable orderedPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
Sort.by(Sort.Order.asc("orderSeq"), Sort.Order.asc("columnName")));
return repository.findAll(predicate, orderedPageable);
} }
} }
@@ -1,69 +0,0 @@
package com.eactive.eai.rms.data.ext.djb.apistatus;
import java.util.HashMap;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.ext.djb.event.ApiStatusChangedEvent;
@Service
@Transactional
public class ApiStatusService {
private static final Logger log = LoggerFactory.getLogger(ApiStatusService.class);
@Autowired
private ApiStatusRepository apiStatusRepository;
@Autowired
private ApplicationEventPublisher eventPublisher;
public void updateApiStatus(HashMap<String, String> param) {
List<ApiStatusEvent> list = apiStatusRepository.findApiStatusEvents(
Integer.parseInt(param.get("errorRate")),
Integer.parseInt(param.get("errorRangeMinute")),
Integer.parseInt(param.get("delayRangeMinute")),
Integer.parseInt(param.get("delayAvgRespTime"))
);
for (ApiStatusEvent event : list) {
String newStatusCode = resolveStatusCode(event.getEvent());
if (newStatusCode == null) continue;
ApiStatus apiStatus = apiStatusRepository.findById(event.getEaisvcname()).orElse(new ApiStatus());
apiStatus.setEaisvcname(event.getEaisvcname());
apiStatus.setStatusCode(newStatusCode);
apiStatusRepository.save(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
eventPublisher.publishEvent(ApiStatusChangedEvent.from(event)); // 트랜잭션 커밋 후 리스너 실행
log.debug("이벤트 전파");
}
}
private String resolveStatusCode(String event) {
switch (event) {
case "CONTROL_START": return "C"; //점검
case "CONTROL_END": return "N"; //정상
case "ERROR_START": return "E"; //장애
case "ERROR_END": return "N"; //정상
case "DELAY_START": return "D"; //지연
case "DELAY_END": return "N"; //정상
default:
log.warn("알 수 없는 이벤트: {}", event);
return null;
}
}
}
@@ -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> {
@@ -22,9 +24,11 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
@Query(nativeQuery = true, value = @Query(nativeQuery = true, value =
" SELECT EAISVCNAME" " SELECT EAISVCNAME"
+ " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC" + " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC"
+ " , PREV_STATUS_CODE "
+ " , EVENT" + " , 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'"
@@ -34,14 +38,14 @@ 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" + " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X"
+ " WHERE (TO_CHAR(SYSDATE,'HH24MI') BETWEEN SUBSTR(EAICTRLDSTICCTNT,16,4) AND SUBSTR(EAICTRLDSTICCTNT,21,4)" + " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID "
+ " OR SUBSTR(EAICTRLDSTICCTNT,16,9) = '0000|0000')" + " AND SYSTIMESTAMP BETWEEN STARTED_AT AND END_AT) "
+ " AND A.EAISVCNAME = EAICTRLNAME),'N') AS CTRL_YN" + " AND A.EAISVCNAME = X.API_ID),'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"
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL" + " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
+ " , SUM(SUCCESS_CNT) AS SUCCESS" + " , SUM(SUCCESS_CNT) AS SUCCESS"
+ " FROM API_STATS_MINUTE" + " FROM API_STATS_MINUTE"
@@ -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.ums.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);
}
}
@@ -1,25 +0,0 @@
package com.eactive.eai.rms.ext.djb.async;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(20);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@@ -1,40 +0,0 @@
package com.eactive.eai.rms.ext.djb.async;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import com.eactive.eai.rms.ext.djb.event.ApiStatusChangedEvent;
import com.eactive.eai.rms.ext.djb.event.InflowTokenInsufficientEvent;
import com.eactive.eai.rms.ext.djb.swing.service.SwingSendManager;
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookSendManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
@RequiredArgsConstructor
public class AsyncEventListener {
private final WebhookSendManager webhookSendManager;
private final SwingSendManager swingSendManager;
@Async("asyncExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onApiStatusChanged(ApiStatusChangedEvent event) {
log.debug("API 상태 변경 이벤트 수신: {} {}", event.getEaisvcname(), event.getEvent());
webhookSendManager.send(event.getEaisvcname(), event.getEvent());
swingSendManager.send(null, 0, 0);
}
@Async("asyncExecutor")
@EventListener // 트랜잭션 write 없이 read만 하므로 TransactionalEventListener 불필요
public void onInflowTokenFail(InflowTokenInsufficientEvent event) {
log.debug("유량제어 토큰획득 실패 이벤트 수신: {} {}건", event.getEaisvcname(), event.getCnt());
swingSendManager.send(event.getEaisvcname(), event.getCnt(), event.getRangeMinute());
}
}
@@ -1,18 +0,0 @@
package com.eactive.eai.rms.ext.djb.event;
import com.eactive.eai.rms.data.ext.djb.apistatus.ApiStatusEvent;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class ApiStatusChangedEvent {
private final String eaisvcname;
private final String eaisvcdesc;
private final String event;
public static ApiStatusChangedEvent from(ApiStatusEvent source) {
return new ApiStatusChangedEvent(source.getEaisvcname(), source.getEaisvcdesc(), source.getEvent());
}
}
@@ -1,19 +0,0 @@
package com.eactive.eai.rms.ext.djb.event;
import com.eactive.eai.rms.data.ext.djb.inflow.InflowTokenInsufficient;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class InflowTokenInsufficientEvent {
private final String eaisvcname;
private final String eaisvcdesc;
private final long cnt;
private final long rangeMinute;
public static InflowTokenInsufficientEvent from(InflowTokenInsufficient summary, long rangeMinute) {
return new InflowTokenInsufficientEvent(summary.getEaisvcname(), summary.getEaisvcdesc(), summary.getCnt(), rangeMinute);
}
}
@@ -1,9 +1,12 @@
package com.eactive.eai.rms.data.ext.djb.inflow; package com.eactive.eai.rms.ext.djb.inflow;
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.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> { public interface InflowTokenRepository extends BaseRepository<InflowTokenInsufficientLog, InflowTokenInsufficientLogId> {
@@ -1,15 +1,17 @@
package com.eactive.eai.rms.data.ext.djb.inflow; package com.eactive.eai.rms.ext.djb.inflow;
import java.util.List; import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.ext.djb.event.InflowTokenInsufficientEvent; 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.ums.UmsManager;
@Service @Service
@@ -22,16 +24,16 @@ public class InflowTokenService {
private InflowTokenRepository inflowTokenRepository; private InflowTokenRepository inflowTokenRepository;
@Autowired @Autowired
private ApplicationEventPublisher eventPublisher; private UmsManager ums;
public void checkRecentFails(long rangeMinute) { public void checkRecentFails(long rangeMinute) {
List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute); List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute);
for (InflowTokenInsufficient info : rows) { for (InflowTokenInsufficient info : rows) {
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt()); log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
String message = "유량제어 토큰 획득 실패하였습니다 \n" + info.getEaisvcname();
//내부직원 알림 발송
eventPublisher.publishEvent(InflowTokenInsufficientEvent.from(info, rangeMinute)); 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;
/** /**
@@ -88,11 +88,11 @@ public class ApiStatusMonitorJob 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.InflowTokenService; 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;
/** /**
@@ -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);
}
}
@@ -1,17 +0,0 @@
package com.eactive.eai.rms.ext.djb.swing.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class SwingSendManager {
public void send(String eaisvcname, long cnt, long rangeMinute) {
log.info("[Swing] 발송 준비: {} 최근 {}분 {}건", eaisvcname, rangeMinute, cnt);
// TODO: 알림 발송 로직
}
}
@@ -0,0 +1,89 @@
package com.eactive.eai.rms.ext.djb.ums;
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,43 @@
package com.eactive.eai.rms.ext.djb.ums.event;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageEventHandler;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.template.service.MessageSendEvent;
@Component
public class ApiStatusChangedEventHandler implements MessageEventHandler {
@Override
public MessageCode getKey() {
return MessageCode.API_STATUS_CHANGED;
}
@Override
public boolean allowAdditionalRecipients() {
return true;
}
@Override
public String getDisplayName() {
return "API 상태 변화";
}
@Override
public String getDescription() {
return "<div>사용 가능 변수: </div><br/>"
+ "<div> - message 알림 메시지 </div><br/>";
}
@Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> eventParams = new HashMap<>();
eventParams.put("message", (String) params.get("message"));
return new MessageSendEvent(source, MessageCode.API_STATUS_CHANGED, recipient, eventParams);
}
}
@@ -0,0 +1,43 @@
package com.eactive.eai.rms.ext.djb.ums.event;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageEventHandler;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.template.service.MessageSendEvent;
@Component
public class InflowTokenFailedEventHandler implements MessageEventHandler {
@Override
public MessageCode getKey() {
return MessageCode.INFLOW_TOKEN_FAILED;
}
@Override
public boolean allowAdditionalRecipients() {
return true;
}
@Override
public String getDisplayName() {
return "유량제어 토큰 획득 실패";
}
@Override
public String getDescription() {
return "<div>사용 가능 변수: </div><br/>"
+ "<div> - message 알림 메시지 </div><br/>";
}
@Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> eventParams = new HashMap<>();
eventParams.put("message", (String) params.get("message"));
return new MessageSendEvent(source, MessageCode.INFLOW_TOKEN_FAILED, recipient, eventParams);
}
}
@@ -0,0 +1,344 @@
package com.eactive.eai.rms.ext.djb.ums.service;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.eai.common.util.SystemUtil;
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
import com.eactive.eai.rms.ext.djb.ums.vo.EmailDataVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SmsDataVO;
import com.eactive.eai.rms.ext.djb.ums.vo.StdHeadVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SwingCommonVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SwingDataVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SwingReceiverDataVO;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class UmsService {
private static final Gson gson = new Gson();
private static final String SYS_DVCD = "OPA";
private final MonitoringPropertyService propertyService;
public UmsService(MonitoringPropertyService propertyService) {
this.propertyService = propertyService;
}
public boolean send(MessageRequest messageRequest) throws Exception {
if (messageRequest == null) {
throw new IllegalArgumentException("messageRequest is null");
}
this.httpConnection(messageRequest);
return true;
}
private void httpConnection(MessageRequest messageRequest) throws Exception {
HttpURLConnection conn = null;
BufferedReader br = null;
log.debug("<<<HTTP Connection>>>:" + messageRequest.toString());
try {
String host = propertyService.getPropertyValue("Monitoring", "djb.ums."+messageRequest.getMessageType().toLowerCase()+".url", "");
URL url = new URL(host);
int timeoutValue = 5 * 1000; // 타임아웃 설정을 위한 값 (단위: ms)
// Charset charset = Charset.forName("UTF-8");
Charset charset = Charset.forName("MS949");
StringBuffer sb = null;
String bodyData = this.makeBodyData(messageRequest);
String responseData = "";
String method = bodyData.isEmpty() ? "GET" : "POST";
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(method); // 전송방식
// conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Content-Type", "application/json; charset=MS949");
conn.setConnectTimeout(timeoutValue); // 연결 타임아웃 설정(5초)
conn.setReadTimeout(timeoutValue); // 읽기 타임아웃 설정(5초)
conn.setDoOutput(true);
// POst 방식인 경우에만
if (method.equals("POST")) {
OutputStream os = conn.getOutputStream();
byte requestData[] = bodyData.getBytes(charset);
os.write(requestData);
os.close();
}
if (log.isDebugEnabled()) {
log.debug("getContentType();" + conn.getContentType()); // 응답 콘텐츠 유형 구하기
log.debug("getResponsecode();" + conn.getResponseCode()); // 응답 코드 구하기
log.debug("getResponseMessage():" + conn.getResponseMessage()); // 응답 메세지 구하기
}
// http 요청 후 응답 받은 데이타를 버퍼에 쌓는다
if (conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
br = new BufferedReader(new InputStreamReader(conn.getInputStream(), charset));
} else {
br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), charset));
}
String inputLine;
sb = new StringBuffer();
while ((inputLine = br.readLine()) != null) {
sb.append(inputLine);
}
responseData = sb.toString();
} finally {
// http 요청 및 응답 완료 후 BufferedReader를 닫는다
if (br != null) {
br.close();
}
if (conn != null) {
conn.disconnect();
}
}
}
private String makeBodyData(MessageRequest messageRequest) {
// 리턴값
String rtnVal = "";
// guID 및 날짜 시간 정보
String jsonData[] = this.makeStdGuidInfo();
// 표준 헤더
StdHeadVO stdHeadVO = new StdHeadVO();
stdHeadVO.setNxgn_stnd_idfr("JERA");
stdHeadVO.setGuid(jsonData[2]);
stdHeadVO.setGuid_prgs_no("1");
stdHeadVO.setStnd_mesg_ver("R10");
stdHeadVO.setOrtr_guid(jsonData[2]);
stdHeadVO.setSect_ecrpt_yn("N");
stdHeadVO.setFrst_trnm_ipad(propertyService.getPropertyValue("Monitoring", "djb.ums.was_ip_address", ""));
stdHeadVO.setFrst_trnm_ipv4_addr(propertyService.getPropertyValue("Monitoring", "djb.ums.was_ip_address", ""));
stdHeadVO.setFrst_trnm_mac(propertyService.getPropertyValue("Monitoring", "djb.ums.was_mac_address", ""));
stdHeadVO.setFrst_mesg_dman_dt(jsonData[0]);
stdHeadVO.setFrst_mesg_dman_time(jsonData[1]);
stdHeadVO.setFrst_trnm_sys_dvcd(SYS_DVCD);
stdHeadVO.setTrnm_sys_dvcd(SYS_DVCD);
stdHeadVO.setDman_rspn_dvcd("S"); // 요청응답구분코드 S:요청 R:응답
stdHeadVO.setSys_env_dvcd(SystemUtil.getSysOperEvirnDstcd()); // D:개발 T:테스트 P:운영
stdHeadVO.setTx_tycd("O"); // O:온라인 B:배치
stdHeadVO.setSynz_dvcd("S"); // S:동기 A:비동기
stdHeadVO.setChnl_tycd(""); // 채널유형코드
stdHeadVO.setHmab_dvcd("1"); // 내외구분코드 1:내부 2:외부(타발)
stdHeadVO.setIf_id(messageRequest.getEaiInterfaceId()); // 인터페이스ID
stdHeadVO.setTx_id(messageRequest.getServiceId()); // 거래ID
if ("MESSENGER".equals(messageRequest.getMessageType()) ) {
// 스윙챗 알림 공용
SwingCommonVO swiComVO = new SwingCommonVO();
swiComVO.setClientld(propertyService.getPropertyValue("Monitoring", "djb.ums.messenger.client_id", ""));
swiComVO.setClientSecret(propertyService.getPropertyValue("Monitoring", "djb.ums.messenger.client_secret", ""));
swiComVO.setCompanyCode("CB");
swiComVO.setRequestUniqueKey(this.genrateRandomStringNumber26());
// 스윙챗 알림 데이터
SwingDataVO swiDataVO = new SwingDataVO();
swiDataVO.setNotiCode("CNO_FEP_HMP_0002");
swiDataVO.setNotiTitle("알림");
swiDataVO.setNotiContent(messageRequest.getMessage());
// 스윙챗 알림 데이터
List<SwingReceiverDataVO> swiReDataVOList = new ArrayList<SwingReceiverDataVO>();
SwingReceiverDataVO receiver = new SwingReceiverDataVO();
receiver.setCode(messageRequest.getMessengerId());
swiReDataVOList.add(receiver);
swiDataVO.setReceiverInfo(swiReDataVOList);
String header = gson.toJson(stdHeadVO);
String common = gson.toJson(swiComVO);
String data = gson.toJson(swiDataVO);
// 헤더 + 데이터
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
+ " \"common\":\r\n" + common + ",\r\n" + " \"data\":" + data
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
}
else if ("SMS".equals(messageRequest.getMessageType())
|| "KAKAO".equals(messageRequest.getMessageType())) {
// Sms 데이터
SmsDataVO dataVO = new SmsDataVO();
dataVO.setSnd_dman_dt(jsonData[0]);
dataVO.setSnd_dman_id("");
dataVO.setSnd_dvcd("EAI");
dataVO.setMsg_titl("");
dataVO.setMsg_ctnt(messageRequest.getMessage());
dataVO.setRecvr_no(messageRequest.getPhone());
dataVO.setSndn_no("15880079"); //발신번호
dataVO.setSnd_empno(""); //요청자
dataVO.setSnd_emp_dprm_cd(""); //요청부서코드
if ("SMS".equals(messageRequest.getMessageType())) {
dataVO.setLtrs_snd_tycd("1"); // 1:SMS 2:LMS/MMS
dataVO.setMsg_snd_tycd("0000"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
} else if ("KAKAO".equals(messageRequest.getMessageType())) {
dataVO.setLtrs_snd_tycd("2"); // 1:SMS 2:LMS/MMS
dataVO.setMsg_snd_tycd("1008"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
dataVO.setAlmtk_snd_prfl_key(""); // 플러스친구아이디
dataVO.setAlmtk_tmplt_cd(""); // 템플릿코드
dataVO.setAlmtk_err_ltrs_snd_yn("Y"); // 오류자문자발송여부
dataVO.setAlmtk_btn_trnm_tycd("2"); // 카카오버튼전송방식 1-format string 2-JSON 3-XML
}
dataVO.setSms_trnm_bzwk_dvcd(""); //업무구분코드
dataVO.setSms_bzwk_dcls_dvcd(""); //업무세분코드
dataVO.setSms_sys_dvcd("83"); //시스템코드
dataVO.setTx_dt(jsonData[0]); //거래일자
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
List<SmsDataVO> dataList = new ArrayList<SmsDataVO>();
dataList.add(dataVO);
String header = gson.toJson(stdHeadVO);
String data = gson.toJson(dataList);
// 헤더 + 데이터
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
+ " \"ums_if_data_ncnt\":1, \r\n"
+ " \"ums_if_data\":" + data
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
}
else if ("EMAIL".equals(messageRequest.getMessageType())) {
// 이메일 데이터
EmailDataVO dataVO = new EmailDataVO();
dataVO.setUms_evnt_id(""); // UMS이벤트ID(필수)
dataVO.setSnd_dman_dt(jsonData[0]);
dataVO.setSnd_dman_id("");
dataVO.setSnd_dman_sno("1");
dataVO.setEmail_titl(messageRequest.getSubject()); // 메일제목
dataVO.setCstno(""); // 고객번호(필수)
dataVO.setSendr_emaddr(""); // 발신자 이메일
dataVO.setSendr_nm(""); // 발신자 이름
dataVO.setSndbk_emaddr(""); // 리턴 이메일
dataVO.setRecvr_emaddr(messageRequest.getEmail()); // 수신자 이메일(필수)
dataVO.setRecvr_nm(messageRequest.getUsername()); // 수신자명(필수)
dataVO.setTmplt_mpng_info(messageRequest.getMessage()); // 탬플릿매핑정보
dataVO.setTmplt_mpng_rptt_info(""); // 탬플릿매핑반복정보
dataVO.setSnd_empno(""); // 발신자ID(필수)
dataVO.setSnd_emp_dprm_cd(""); // 발신자부서코드(필수)
dataVO.setBzwk_cd(""); // 업무구분코드
dataVO.setSub_bzwk_cd(""); // 업무세부코드
dataVO.setSys_dvcd(SYS_DVCD);
dataVO.setTx_dt(jsonData[0]); //거래일자
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
List<EmailDataVO> dataList = new ArrayList<EmailDataVO>();
dataList.add(dataVO);
String header = gson.toJson(stdHeadVO);
String data = gson.toJson(dataList);
// 헤더 + 데이터
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
+ " \"ums_if_data_ncnt\":1, \r\n"
+ " \"ums_if_data\":" + data
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
}
log.debug(rtnVal);
return rtnVal;
}
private String[] makeStdGuidInfo() {
// 리턴값
String rtnVal[] = new String[3];
// 현재시간
Date currentDate = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
String formattedDate = sdf.format(currentDate);
rtnVal[0] = formattedDate.substring(0,8);
rtnVal[1] = formattedDate.substring(8);
// 랜던값
String random20 = this.genrateRandomStringNumber20();
rtnVal[2] = formattedDate + SYS_DVCD + random20;
return rtnVal;
}
private String genrateRandomStringNumber20() {
String rtnVal= "";
StringBuilder buffer= new StringBuilder(10);
// 랜덤값
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
// 랜덤객체 생성
SecureRandom random = new SecureRandom();
// 반복문
for (int i= 0;i< 10;i++) {
// 랜덤수
int randomIndex = random.nextInt(characters.length());
// 생성된 랜덤값
char randomChar = characters.charAt(randomIndex);
// 랜덤값 저장
buffer.append(randomChar);
}
// 랜덤객체 재생성
random = new SecureRandom();
// 난수 10개 생성
int random10 = (int) (Math.pow(10, 9) + random.nextDouble() * Math.pow(10, 9));
// 문자 숫자 조합(10) + 숫자(10)
rtnVal = buffer.toString() + Integer.toString(random10);
//리턴
return rtnVal;
}
private String genrateRandomStringNumber26() {
StringBuilder buffer = new StringBuilder(26);
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ0123456789";
SecureRandom random = new SecureRandom();
for (int i = 0; i < 26; i++) {
buffer.append(characters.charAt(random.nextInt(characters.length())));
}
return buffer.toString();
}
}
@@ -0,0 +1,67 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class EmailDataVO {
/** UMS이벤트ID */
private String ums_evnt_id;
/** 발송요청일자 */
private String snd_dman_dt;
/** UUID */
private String snd_dman_id;
/** 발송요청순번 */
private String snd_dman_sno;
/** 메일제목 */
private String email_titl;
/** 고객번호 */
private String cstno;
/** 발신자 이메일 */
private String sendr_emaddr;
/** 발신자 이름 */
private String sendr_nm;
/** 리턴 이메일 */
private String sndbk_emaddr;
/** 수신자 이메일 */
private String recvr_emaddr;
/** 수신자명 */
private String recvr_nm;
/** 탬플릿매핑정보 */
private String tmplt_mpng_info;
/** 탬플릿매핑반복정보 */
private String tmplt_mpng_rptt_info;
/** 발신자ID */
private String snd_empno;
/** 발신자부서코드 */
private String snd_emp_dprm_cd;
/** 업무구분코드 */
private String bzwk_cd;
/** 업무세부코드 */
private String sub_bzwk_cd;
/** 시스템코드 */
private String sys_dvcd;
/** 거래일자 */
private String tx_dt;
/** 거래시간 */
private String tx_tktm;
}
@@ -0,0 +1,67 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class SmsDataVO {
/** 발송요청일자 */
private String snd_dman_dt;
/** UUID */
private String snd_dman_id;
/** 발송구분 */
private String snd_dvcd;
/** 발송타입 */
private String ltrs_snd_tycd;
/** LMS 제목 */
private String msg_titl;
/** 메세지 */
private String msg_ctnt;
/** 수신자번호 */
private String recvr_no;
/** 발신번호 */
private String sndn_no;
/** 요청자(직원번호) */
private String snd_empno;
/** 요청부서코드 */
private String snd_emp_dprm_cd;
/** 알림톡 메시지 종류 */
private String msg_snd_tycd;
/** 플러스친구아이디 */
private String almtk_snd_prfl_key;
/** 템플릿코드 */
private String almtk_tmplt_cd;
/** 오류자문자발송여부 */
private String almtk_err_ltrs_snd_yn;
/** 카카오톡전송방식 */
private String almtk_btn_trnm_tycd;
/** 업무구분 코드 */
private String sms_trnm_bzwk_dvcd;
/** 업무세부코드 */
private String sms_bzwk_dcls_dvcd;
/** 시스템코드 */
private String sms_sys_dvcd;
/** 거래일자 */
private String tx_dt;
/** 거래시간 */
private String tx_tktm;
}
@@ -0,0 +1,61 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class StdHeadVO {
private String nxgn_stnd_idfr;
private String guid;
private String guid_prgs_no;
private String stnd_mesg_ver;
private String ortr_guid;
private String sect_ecrpt_yn;
private String frst_trnm_ipad;
private String frst_trnm_ipv4_addr;
private String frst_trnm_mac;
private String frst_mesg_dman_dt;
private String frst_mesg_dman_time;
private String frst_trnm_sys_dvcd;
private String trnm_sys_dvcd;
private String dman_rspn_dvcd;
private String sys_env_dvcd;
private String tx_tycd;
private String synz_dvcd;
private String tx_id;
private String osdch_dvcd;
private String chnl_tycd;
private String if_id;
private String hmab_dvcd;
private String tx_brcd;
private String tlrno;
private String insl_brcd;
private String blng_brcd;
private String mgmt_brcd;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class SwingCommonVO {
/** 클라이업트 D */
private String clientld;
/** 클라이언트 시크릿 */
private String clientSecret;
/** 회사코드 */
private String companyCode;
/** 요청고유키 */
private String requestUniqueKey;
}
@@ -0,0 +1,21 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import java.util.List;
import lombok.Data;
@Data
public class SwingDataVO {
/** 알림코드 */
private String notiCode;
/** 알림제목 */
private String notiTitle;
/** 알림내용 */
private String notiContent;
/** 수신자정보 */
private List<SwingReceiverDataVO> receiverInfo;
}
@@ -0,0 +1,22 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class SwingReceiverDataVO {
/** 알림코드 */
private String name;
/** 알림제목 */
private String type;
/** 알림내용 */
private String companyCode;
/** 수신자정보 */
private String code;
/** 상품명 */
private String prdNm;
}

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