75 Commits

Author SHA1 Message Date
eastargh bc478ad4ed 배치잡 수정시 clustered에 무조건 추가되는 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-06-17 10:58:00 +09:00
Rinjae 161a68a0a2 - Tomcat readiness probe 타임아웃 180s → 300s 연장
eapim-admin CI / build (push) Has been cancelled
- readiness probe URL /monitoring/login.do → /monitoring/loginForm.do 수정
- readiness 실패 시 로그 출력 내용 및 포트 점검 추가
2026-06-16 18:46:28 +09:00
Rinjae-gf63 3b59f7acd7 Merge branch 'master' of ssh://172.30.1.50:2222/djb-eapim/eapim-admin
eapim-admin CI / build (push) Has been cancelled
2026-06-16 16:44:16 +09:00
Rinjae-gf63 1e29d1d721 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>
2026-06-16 16:41:57 +09:00
Rinjae-gf63 9342b6484c 포탈 공지사항 삭제 기능 추가 / ADM -> APP 2026-06-16 16:37:56 +09:00
eastargh dc3842aa9b 배치잡 CLUSTERED 기능 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-16 16:33:55 +09:00
eastargh a440ac842d API상태 변화 감지 (점검 게시판 적용)
eapim-admin CI / build (push) Has been cancelled
2026-06-16 16:05:44 +09:00
Rinjae df5bbb162a ci 수정
eapim-admin CI / build (push) Has been cancelled
2026-06-16 15:56:46 +09:00
Rinjae 3eec267769 Merge commit '58fe176715b2027f783a2dae98e1583beaf34c8c'
eapim-admin CI / build (push) Has been cancelled
2026-06-16 15:48:41 +09:00
Rinjae 6ab7e1fe16 ci 수정 2026-06-16 15:48:30 +09:00
eastargh 58fe176715 조회시 정렬순서 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-16 15:05:48 +09:00
Rinjae 6b6e2863cd 공지사항 관리 - 장애 점검 등록 UI 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-16 14:55:39 +09:00
eastargh cfafccc34d CA 인증타입 제거
eapim-admin CI / build (push) Has been cancelled
2026-06-16 14:32:35 +09:00
eastargh fe98ac45c4 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin 2026-06-16 14:26:21 +09:00
eastargh 5f9de93d35 인증타입 CA 제거 2026-06-16 14:19:27 +09:00
Rinjae 48dfcf7714 Merge branch 'feats/ci-test'
eapim-admin CI / build (push) Has been cancelled
2026-06-16 13:22:38 +09:00
Rinjae 6e3906878a Merge commit 'fafe7e45cb35b7475cdc54f6b390b6c4845777d5' 2026-06-16 13:21:55 +09:00
eastargh fafe7e45cb 서버명 표시 2026-06-16 13:19:10 +09:00
eastargh eee7f46aa7 ums 관련 수정 2026-06-16 11:10:36 +09:00
eastargh b5eb63448a 레이아웃 기본 데이타타입 Boolean 추가 2026-06-15 17:52:34 +09:00
eastargh 54291e5cbb ums 관련 정리 2026-06-15 13:16:23 +09:00
Rinjae a0743465ab - generic package 업로드 단계 제거
eapim-admin CI / build (push) Has been cancelled
- 인증 정리 전까지 artifact만 업로드 유지
2026-06-12 20:51:36 +09:00
Rinjae d3e05256b7 - package upload 인증 헤더 방식 변경
- basic oauth2 user → Authorization token 헤더
2026-06-12 20:28:17 +09:00
Rinjae 4d5572f880 로그 설정 변경
eapim-admin CI / build (push) Has been cancelled
2026-06-12 19:43:33 +09:00
Rinjae e21014f992 - gitea actions 워크플로우 추가
- 7개 sibling clone 후 monitoring.war 빌드
- systemd user unit으로 tomcat 라이프사이클 분리
- /monitoring/login.do 로 readiness probe (120s)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 19:39:01 +09:00
Rinjae 43fe63964f Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/com/eactive/eai/rms/onl/apim/approval/PortalApprovalManController.java
2026-06-12 16:08:41 +09:00
eastargh ac0d081a9a 인터페이스 아웃바운드 path에 {} 포함시 variableUrlRequest로 설정 2026-06-12 10:18:39 +09:00
eastargh 164ab92919 BAP 서버 목록조회 쿼리에 rownum=1 제거 2026-06-12 09:27:08 +09:00
eastargh a6e7d1adb3 아웃바운드 URL restoption type 판단시 {}안에 .도 포함되도록 변경 2026-06-11 11:38:38 +09:00
Rinjae e51331986d Merge remote-tracking branch 'origin/master' 2026-06-09 19:08:38 +09:00
eastargh 1aa40f2c61 UMS관련 정리 2026-06-09 15:39:05 +09:00
eastargh c8caeeda8c 유량제어 모니터링 2026-06-09 13:04:32 +09:00
eastargh 06ad1230b2 유량제어 모니터링 2026-06-09 11:34:10 +09:00
eastargh 8109f91aa2 유량제어 현황 모니터링 기능 추가 2026-06-09 09:51:05 +09:00
eastargh 62ccebd773 Q&A 7일후 자동 종료 배치 2026-06-08 17:19:04 +09:00
eastargh ec8d6bfd7b Q&A 게시판 댓글 작성 기능 2026-06-05 17:41:37 +09:00
eastargh 480d7dac5a 앱승인시 예상완료일 항목 추가 & 승인요청 상세 조회시 "진행중"으로 상태변경 2026-06-05 15:10:06 +09:00
eastargh feb4f845a8 팝업 두번째 이후 오픈시 선택버그 수정 2026-06-05 10:11:21 +09:00
eastargh 7857f5b45c 자동 Log-off 기능 추가 2026-06-04 18:11:31 +09:00
eastargh 68c8e18e8d 스윙채 & webhook 발송 수정 2026-06-04 17:38:52 +09:00
eastargh 35e5564252 webhook 발송 2026-06-02 09:43:03 +09:00
eastargh 0d7d006f92 api 상태 변화 감지 & 웹훅 발송 2026-06-01 17:37:06 +09:00
eastargh 737b493f7c API사용현황 통계 2026-05-28 16:02:52 +09:00
eastargh af0a878a39 DB스키마변경 BAPADM->BAPAPP 2026-05-28 11:32:31 +09:00
eastargh b639e29039 DB스키마 변경 : AGWADM->AGWAPP, EMSADM->EMSAPP 2026-05-28 10:53:33 +09:00
eastargh 350297aaa2 클라이언트 유량제어 테이블 수정 2026-05-27 16:54:19 +09:00
Rinjae 23ba7993c3 Merge remote-tracking branch 'origin/master' 2026-05-21 15:16:41 +09:00
eastargh 7677e3b26e 거래로그 집계시 공통코드 테이블 변경 2026-05-21 15:10:24 +09:00
Rinjae 90c43ea7b0 - K8sUtil 파일 삭제
- 승인관리 완전삭제 기능 추가
- DJBank로 명칭 업데이트
2026-05-21 09:51:39 +09:00
eastargh 191857811f 공지사항 첨부파일 안씀 2026-05-20 11:50:40 +09:00
eastargh 88ffc1cc28 API 사용현황 신규개발 2026-05-20 11:45:00 +09:00
eastargh d3bd4acb62 요청/응답구분 선택 버그 수정 2026-05-19 10:57:48 +09:00
eastargh 1f7166502f API통계 업무오류 제외 2026-05-15 14:53:52 +09:00
eastargh 91f36605e8 스케쥴러 실행시간 format 변경 2026-05-14 11:22:47 +09:00
eastargh 0ebd31e256 API 통계 화면 리뉴얼 2026-05-14 11:21:14 +09:00
eastargh 45c6edbbeb 거래로그>시간별 수동집계 기능 추가 2026-05-14 11:20:33 +09:00
eastargh 335c7ec53a 거래로그 > 시간별 집계하는 배치잡 2026-05-14 11:19:55 +09:00
eastargh 0b341b85f1 평균응답속도 계산시 분모를 성공건수에서 총건수로 변경 2026-05-14 11:17:55 +09:00
eastargh e9ac62d3ed 인터페이스 상세 저장 버그 수정 2026-05-12 10:37:32 +09:00
eastargh 133c05f1d7 알림 발송 관련 배치 리네이밍 2026-05-08 17:24:17 +09:00
eastargh 4a1a4be54d 검색조건 유지 2026-05-08 10:37:57 +09:00
curry772 dbd36f28d4 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin 2026-05-07 15:39:27 +09:00
curry772 4ee50be000 Merge branch 'feature/crypto-module' into master 2026-05-07 15:39:13 +09:00
curry772 3d916eda53 feat: 암호화 모듈 저장/삭제 후 online 서버 broadcast 결과 alert 출력
- CryptoModuleConfigManController: INSERT/UPDATE/DELETE 모두 broadcast()
  결과(CommandResult)를 broadcastResult 필드로 응답에 포함
- cryptoModuleManDetail.jsp: 저장/삭제 success 콜백에서 broadcastResult를
  alert 메시지에 추가 출력 (서버명=결과 형태)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 15:35:46 +09:00
curry772 49fdc3b577 암호화 모듈 설정 관리 화면 추가 (feature/crypto-module)
- CryptoModuleConfig CRUD 관리 화면 신규 개발
  - JPA DataService/Repository, MapStruct UiMapper, ManService, Controller
  - 목록(jqGrid, 4가지 검색조건), 상세/등록/수정/삭제 화면
  - INSERT/UPDATE/DELETE 후 게이트웨이 전 인스턴스에 ReloadCryptoModuleCommand broadcast
- 버그 수정
  - getById(getReferenceById) → findById 교체: Persistable.isNew=true 유지로 인한 persist 에러
  - insert 시 entity.setCryptoId(null): 빈 문자열 cryptoId 전송 시 UUID 생성기 오동작 방지
  - 검색 버튼 클릭 시 algType/keySourceType/useYn getSearchForJqgrid 누락 수정
- UX 개선
  - 암호화키/복호화키/IV 필드에 텍스트→HEX 변환 보조 입력 및 키 길이 실시간 표시
  - 상세 화면에 수정자/수정일시 정보성 표시
- DB: EMSADM.CRYPTO_MODULE_CONFIG 테이블 (MODIFIED_BY/MODIFIED_AT 포함 전체 DDL)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 10:51:39 +09:00
eastargh 9d55db3281 게시판관리 - 게시유형, 고정여부 추가 2026-05-07 09:50:49 +09:00
eastargh 9a5d5e4815 클라이언트 유량제어 신규생성 2026-05-07 09:39:59 +09:00
eastargh e9bff78cbf 웹훅 발송/수신 sample 2026-04-30 17:44:52 +09:00
eastargh 6ccd669ec6 유량제어 토큰 획득 실패 감시 JOB 2026-04-30 17:44:06 +09:00
eastargh 5db046eb10 API 상태 변화 감시 job 2026-04-30 17:42:20 +09:00
eastargh 8bd8558038 API수정시 삭제버그 수정 2026-04-30 17:14:08 +09:00
eastargh c3f5d6a42e orderSeq 정렬 추가 2026-04-30 17:12:02 +09:00
eastargh 72cdb04261 이중로그인 프라퍼티 적용 & 이중로그인 로그아웃 버그 수정 2026-04-29 15:36:25 +09:00
curry772 d3601c3991 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin 2026-04-22 14:42:19 +09:00
eastargh e213c96994 인터페이스 헤더 어댑터 REST PATH 추가가능 2026-04-16 11:00:26 +09:00
171 changed files with 8873 additions and 1929 deletions
+140
View File
@@ -0,0 +1,140 @@
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: "39120"
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 300s)
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
# /loginForm.do 는 GET 매핑(MainController#loginForm) → tomcat·webapp 양쪽 살아있으면 200
# /login.do 는 POST 처리 — GET 으로 probe 시 405/잘못된 응답 가능성 있어 회피
PROBE_URL="http://localhost:$DEPLOY_HTTP_PORT/monitoring/loginForm.do"
DEADLINE=$(($(date +%s) + 300))
STATUS=000
while [ $(date +%s) -lt $DEADLINE ]; do
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
"$PROBE_URL" 2>/dev/null || echo "000")
case "$STATUS" in
200|302|401)
echo "Readiness OK ($PROBE_URL 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 300s (last HTTP status: $STATUS)"
echo "--- last 300 lines of catalina log ---"
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -300
echo "--- systemd unit status ---"
systemctl --user status eapim-admin --no-pager || true
echo "--- listening ports ---"
ss -tlnp 2>/dev/null | grep -E ":$DEPLOY_HTTP_PORT\b" || echo "(port $DEPLOY_HTTP_PORT 미점유)"
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
*.claude.md
tmpclaude-*-cwd
logs
rinjae_
tomcat-base
+36 -28
View File
@@ -10,7 +10,7 @@ AI 어시스턴트가 작업을 시작하기 전에 반드시 이해해야 할
- **신규 기능**: 항상 **JPA**와 Spring Data 리포지토리를 사용해야 합니다.
- **레거시 코드**: 기존 iBATIS (`*.xml` 매퍼) 코드를 수정할 때는 해당 패턴을 따라야 합니다.
- **멀티 모듈 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` 패키지는 특정 고객사(광주은행)를 위한 코드를 포함하므로, 일반적인 기능 수정 시에는 주의가 필요합니다.
## 프로젝트 개요
@@ -39,50 +39,48 @@ eLink EMS (eLink Management System)는 eLink의 웹 기반 관리 서비스로,
### 빌드 명령어
**⚠️ 중요**: 환경변수 문제로 인해 `gradle` 대신 `kjb-gradle.sh` 스크립트를 사용해야 합니다.
- 스크립트 위치: `/c/eactive/workspaces/shell-scripts/kjb-gradle.sh`
- PATH에 등록되어 있으므로 바로 `kjb-gradle.sh` 명령어 사용 가능
프로젝트 루트의 Gradle wrapper(`./gradlew`)를 사용합니다. JDK 8 toolchain 위치는 `.envrc` 등으로 잡습니다(아래 "기술 스택" 섹션 참조).
```bash
# 표준 빌드
kjb-gradle.sh build
./gradlew build
# Weblogic 배포용 빌드 (테스트 제외)
kjb-gradle.sh build -x test -Pprofile=weblogic
./gradlew build -x test -Pprofile=weblogic
# WAR 파일 빌드
kjb-gradle.sh war
./gradlew war
# 클린 빌드
kjb-gradle.sh clean build
./gradlew clean build
```
### 테스트 실행
```bash
# 모든 테스트 실행
kjb-gradle.sh test
./gradlew test
# 특정 테스트 클래스 실행
kjb-gradle.sh test --tests "com.example.ClassName"
./gradlew test --tests "com.example.ClassName"
# 특정 테스트 패키지 실행 (JUnit 플랫폼 사용)
kjb-gradle.sh test --tests "com.eactive.eai.rms.*"
./gradlew test --tests "com.eactive.eai.rms.*"
```
### 개발 태스크
```bash
# 패키징 없이 클래스만 컴파일
kjb-gradle.sh classes
./gradlew 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회)**
```bash
# Eclipse 사용 시 (기본)
kjb-gradle.sh eclipse
./gradlew eclipse
# IntelliJ IDEA 사용 시 (build.gradle.intellij 파일 사용 필요)
# 1. build.gradle을 build.gradle.eclipse로 백업
# 2. build.gradle.intellij를 build.gradle로 복사
# 3. kjb-gradle.sh idea 실행
# 3. ./gradlew idea 실행
```
2. **QueryDSL 등 소스 코드 생성**
```bash
kjb-gradle.sh compileJava
./gradlew compileJava
```
3. **전체 빌드 및 테스트 실행**
```bash
kjb-gradle.sh build
./gradlew build
```
4. **IDE에서 프로젝트 열기**
- Eclipse: `kjb-gradle.sh eclipse` 실행 후, IDE에서 프로젝트를 엽니다.
- Eclipse: `./gradlew eclipse` 실행 후, IDE에서 프로젝트를 엽니다.
- IntelliJ: IntelliJ 전용 `build.gradle.intellij`를 `build.gradle`로 교체 후 사용합니다.
5. **실행 구성(Run Configuration) 설정**
@@ -162,14 +160,14 @@ eapim-admin (root project)
#### Q-Class 생성 위치
Q-classes는 `build/generated/java/` 디렉토리에 생성됩니다.
- `kjb-gradle.sh compileJava` 또는 `kjb-gradle.sh build` 실행 시 Gradle이 자동으로 생성
- `./gradlew compileJava` 또는 `./gradlew build` 실행 시 Gradle이 자동으로 생성
- 이 디렉토리는 `.gitignore`에 포함되어 있으며 커밋해서는 안 됩니다
#### 생성된 코드 작업하기
- **Gradle 빌드**: `build/generated/java/`의 Q-classes가 자동으로 classpath에 포함됩니다
- **업데이트 pull 후**: `kjb-gradle.sh compileJava`를 실행하여 Q-classes를 재생성합니다
- **Q-classes IDE 오류 시**: `kjb-gradle.sh clean compileJava`로 재생성합니다
- **업데이트 pull 후**: `./gradlew compileJava` 를 실행하여 Q-classes를 재생성합니다
- **Q-classes IDE 오류 시**: `./gradlew clean compileJava` 로 재생성합니다
#### IDE별 빌드 설정 파일
@@ -255,12 +253,21 @@ IntelliJ를 사용하려면 `build.gradle.intellij`를 `build.gradle`로 교체
WebLogic용 빌드 시, DefaultServlet 문제를 해결하기 위해 `web.xml`을 `weblogic-web.xml`로 교체하는 `weblogic` 프로필을 사용합니다:
```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 Data JPA 2.5.2**: 리포지토리 추상화
- **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 처리
- **Logback 1.2.10**: 로깅 프레임워크 (SLF4J와 함께)
- **AWS SDK 2.20.142**: S3 연동
- **Kubernetes Client 18.0.1**: K8s 관리
- **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 (권장)
```bash
kjb-gradle.sh idea
./gradlew idea
```
또는 IntelliJ에서 직접 "Import Gradle Project"를 사용합니다.
@@ -395,7 +403,7 @@ cp build.gradle build.gradle.intellij
cp build.gradle.eclipse build.gradle
# 3. Eclipse 프로젝트 생성
kjb-gradle.sh eclipse
./gradlew eclipse
```
Eclipse 전용 설정(`build.gradle.eclipse`)은 다음을 포함합니다:
@@ -25,31 +25,31 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW"
p:text="APIGW"
p:schema="AGWADM"
p:schema="AGWAPP"
p:dynamic="true"
p:online="true"
p:jndiName="jdbc/dsOBP_AGW"
p:render="ONL,COM"
/>
<!-- BAP 일괄전송FTP -->
<!-- <bean
<bean
id="BAP"
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP"
p:text="BAP"
p:schema="BAPADM"
p:schema="BAPAPP"
p:dynamic="true"
p:online="true"
p:jndiName="jdbc/dsOBP_BAP"
p:render="BAP,COM"
/> -->
/>
<!-- RMS_DEFAULT -->
<bean
id="MONITORING"
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING"
p:text="MONITORING"
p:schema="EMSADM"
p:schema="EMSAPP"
p:dynamic="false"
p:online="false"
p:jndiName="jdbc/dsOBP_EMS"
@@ -25,31 +25,31 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW"
p:text="APIGW"
p:schema="AGWADM"
p:schema="AGWAPP"
p:dynamic="true"
p:online="true"
p:jndiName="jdbc/dsOBP_AGW"
p:render="ONL,COM"
/>
<!-- BAP 일괄전송FTP -->
<!-- <bean-->
<!-- id="BAP"-->
<!-- class="com.eactive.eai.rms.common.datasource.DataSourceType"-->
<!-- p:name="BAP"-->
<!-- p:text="BAP"-->
<!-- p:schema="BAPADM"-->
<!-- p:dynamic="true"-->
<!-- p:online="true"-->
<!-- p:jndiName="jdbc/dsOBP_BAP"-->
<!-- p:render="BAP,COM"-->
<!-- />-->
<bean
id="BAP"
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP"
p:text="BAP"
p:schema="BAPAPP"
p:dynamic="true"
p:online="true"
p:jndiName="jdbc/dsOBP_BAP"
p:render="BAP,COM"
/>
<!-- RMS_DEFAULT -->
<bean
id="MONITORING"
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING"
p:text="MONITORING"
p:schema="EMSADM"
p:schema="EMSAPP"
p:dynamic="false"
p:online="false"
p:jndiName="jdbc/dsOBP_EMS"
@@ -25,7 +25,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW"
p:text="APIGW"
p:schema="AGWADM"
p:schema="AGWAPP"
p:dynamic="true"
p:online="true"
p:jndiName="jdbc/dsOBP_AGW"
@@ -37,7 +37,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP"
p:text="BAP"
p:schema="BAPADM"
p:schema="BAPAPP"
p:dynamic="true"
p:online="true"
p:jndiName="jdbc/dsOBP_BAP"
@@ -49,7 +49,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING"
p:text="MONITORING"
p:schema="EMSADM"
p:schema="EMSAPP"
p:dynamic="false"
p:online="false"
p:jndiName="jdbc/dsOBP_EMS"
@@ -25,7 +25,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="APIGW"
p:text="APIGW"
p:schema="AGWADM"
p:schema="AGWAPP"
p:dynamic="true"
p:online="true"
p:jndiName="jdbc/dsOBP_AGW"
@@ -37,7 +37,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="BAP"
p:text="BAP"
p:schema="BAPADM"
p:schema="BAPAPP"
p:dynamic="true"
p:online="true"
p:jndiName="jdbc/dsOBP_BAP"
@@ -49,7 +49,7 @@
class="com.eactive.eai.rms.common.datasource.DataSourceType"
p:name="MONITORING"
p:text="MONITORING"
p:schema="EMSADM"
p:schema="EMSAPP"
p:dynamic="false"
p:online="false"
p:jndiName="jdbc/dsOBP_EMS"
@@ -30,9 +30,9 @@
<entry
key="APIGW"
value-ref="APIGW" />
<!-- <entry
<entry
key="BAP"
value-ref="BAP" /> -->
value-ref="BAP" />
</map>
</property>
<property
@@ -91,7 +91,7 @@
<property name="configLocations">
<list>
<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}/sqlMapConfigOfService.xml</value> -->
</list>
@@ -30,9 +30,9 @@
<entry
key="APIGW"
value-ref="APIGW" />
<!-- <entry-->
<!-- key="BAP"-->
<!-- value-ref="BAP" />-->
<entry
key="BAP"
value-ref="BAP" />
</map>
</property>
<property
@@ -91,7 +91,7 @@
<property name="configLocations">
<list>
<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}/sqlMapConfigOfService.xml</value> -->
</list>
@@ -69,6 +69,7 @@
<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.djb" />
<bean id="cacheManager"
@@ -115,7 +115,7 @@
</bean>
<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"/>
</bean>
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option
inst.Name=emsSvr11
eai.tableowner=EMSADM
eai.tableowner=EMSAPP
# P/T/D/S
eai.systemmode=D
eai.drmode=N
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option
inst.Name=emsSvr96
eai.tableowner=EMSADM
eai.tableowner=EMSAPP
# P/T/D/S
eai.systemmode=L
eai.drmode=N
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option
inst.Name=emsSvr11
eai.tableowner=EMSADM
eai.tableowner=EMSAPP
# P/T/D/S
eai.systemmode=P
eai.drmode=N
@@ -10,7 +10,7 @@ hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# Priority of the was -D option
inst.Name=emsSvr11
eai.tableowner=EMSADM
eai.tableowner=EMSAPP
# P/T/D/S
eai.systemmode=T
eai.drmode=N
+12 -9
View File
@@ -9,15 +9,7 @@
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<!-- XSS Filter -->
<filter>
<filter-name>CrossScriptingFilter</filter-name>
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CrossScriptingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>encodingFilter</filter-name>
@@ -56,6 +48,17 @@
<filter-name>encodingFilter</filter-name>
<url-pattern>*.json</url-pattern>
</filter-mapping>
<!-- XSS Filter -->
<filter>
<filter-name>CrossScriptingFilter</filter-name>
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CrossScriptingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--
<filter>
+8
View File
@@ -36,6 +36,14 @@
// save the returnValue in options so that it is available in the callback function
optns.returnValue = $frame[0].contentWindow.window.returnValue;
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){
$('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/custom.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.searchabledropdown-1.0.8.min.js"/>"></script>
<%-- <script language="javascript" src="<c:url value="/js/jquery.searchable-1.1.0.min.js"/>"></script> --%>
@@ -31,19 +31,19 @@ function init(){
new makeOptions("CODE","NAME").setObj($("select[name=searchJobInst]")).setNoValueInclude(true).setNoValue("",
"<%= localeMessage.getString("combo.all")%>").setData(json.instanceList).rendering();
if(typeof callback === 'function') {
callback(url,key);
}
putSelectFromParam();
initGrid();
},
error:function(e){
alert(e.responseText);
}
});
}
$(document).ready(function() {
init();
function initGrid() {
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
console.log(gridPostData);
$('#grid').jqGrid({
datatype:"json",
mtype: 'POST',
@@ -125,9 +125,13 @@ $(document).ready(function() {
}
});
resizeJqGridWidth('grid','content_middle','1000');
}
$(document).ready(function() {
init();
$("#btn_search").click(function(){
var postData = getSearchForJqgrid("cmd","LIST");
$("#grid").setGridParam({ postData: postData ,page:"1" }).trigger("reloadGrid");
@@ -465,21 +465,21 @@ $(document).ready(function() {
<table class="table_row" cellspacing="0">
<tr>
<th style="width:180px;">Default Scheduler</th>
<td>On Memory: <span id="deft_onMemory"></span></td>
<td style="width: 260px;">Previous Fired: <span id="deft_previous"></span></td>
<td style="width: 260px;">Next Fire Time: <span id="deft_next"></span></td>
<td style="width: 220px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td>
<td style="width: 200px;">PersistJobData: <span id="deft_persistJobData"></span></td>
<td style="width: 200px;">Durable: <span id="deft_durable"></span></td>
<td style="width:150px">On Memory: <span id="deft_onMemory"></span></td>
<td >Previous Fired: <span id="deft_previous"></span></td>
<td >Next Fire Time: <span id="deft_next"></span></td>
<td style="width: 250px;">isConcurrentExectionDisallowed: <span id="deft_concurruntExecution"></span></td>
<td style="width: 120px;">PersistJobData: <span id="deft_persistJobData"></span></td>
<td style="width: 120px;">Durable: <span id="deft_durable"></span></td>
</tr>
<tr>
<th style="width:180px;">Clustered Scheduler</th>
<td>On Memory: <span id="clus_onMemory"></span></td>
<td style="width: 260px;">Previous Fired: <span id="clus_previous"></span></td>
<td style="width: 260px;">Next Fire Time: <span id="clus_next"></span></td>
<td style="width: 220px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td>
<td style="width: 200px;">PersistJobData: <span id="clus_persistJobData"></span></td>
<td style="width: 200px;">Durable: <span id="clus_durable"></span></td>
<td style="width:150px">On Memory: <span id="clus_onMemory"></span></td>
<td>Previous Fired: <span id="clus_previous"></span></td>
<td>Next Fire Time: <span id="clus_next"></span></td>
<td style="width: 250px;">isConcurrentExectionDisallowed: <span id="clus_concurruntExecution"></span></td>
<td style="width: 120px;">PersistJobData: <span id="clus_persistJobData"></span></td>
<td style="width: 120px;">Durable: <span id="clus_durable"></span></td>
</tr>
</table>
+2 -2
View File
@@ -353,10 +353,10 @@
</div>
<!-- SSO 로그인 버튼 (비상모드 시 숨김) -->
<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"
value="SSO Login">
</div>
</div> -->
</c:if>
<p style="color:red;"><%=resultMsg %></p>
</form>
+88 -1
View File
@@ -99,6 +99,93 @@
<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="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>
+2 -2
View File
@@ -551,9 +551,9 @@
</h1><!-- /monitoring/images/top_logo.png -->
<div class="topmenu_box">
<ul>
<li style="width:240px;" id="newDashboardShow">
<li style="width:240px;" id="newDashboardShow">
<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>
<% if (!"".equals(LastLoginYms.trim())) { %>
<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
optns.returnValue = $frame[0].contentWindow.window.returnValue;
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){
$('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/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_view ='<c:url value="/onl/admin/inflow/inflowAdapterControlMan.view" />';
var isDetail = false;
function init(url,key,callback){
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
if(typeof callback === 'function') {
callback(url,key);
updateThresholdLabel();
if (typeof callback === 'function') {
callback(url, key);
}
},
error:function(e){
error: function(e) {
alert(e.responseText);
}
});
}
function detail(url,key){
if (!isDetail)return;
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'DETAIL', name : key},
success:function(json){
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json;
$("input[name=name]").attr('readonly',true);
$("input[name=desc]").attr('readonly',true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){
$("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
});
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
},
error:function(e){
error: function(e) {
alert(e.responseText);
}
});
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key ="${param.name}";
if (key != "" && key !="null"){
isDetail = true;
}
init(url,key,detail);
$("#btn_modify").click(function(){
var postData = $('#ajaxForm').serializeArray();
if (isDetail){
postData.push({ name: "cmd" , value:"UPDATE"});
}else{
postData.push({ name: "cmd" , value:"INSERT"});
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', adapterId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl);//LIST로 이동
},
error:function(e){
alert(e.responseText);
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '어댑터가 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.name}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 어댑터를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
if (isDetail) {
postData.push({name: "cmd", value: "UPDATE"});
} else {
postData.push({name: "cmd", value: "INSERT"});
}
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"});
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl);//LIST로 이동
},
error:function(e){
alert(e.responseText);
$("#btn_delete").click(function() {
$(this).blur();
var adapterName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + adapterName + ']</strong> 어댑터를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
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);
titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div><!-- end content_top -->
<div class="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
<%-- <img src="<c:url value="/img/btn_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div>
<div 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>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div><!-- end content_top -->
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title-wrap">
<div class="title"><%= 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>
@@ -0,0 +1,138 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript" >
var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
$(document).ready(function() {
$('#grid').jqGrid({
datatype:"json",
mtype: 'POST',
url: url,
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val()},
colNames:['클라이언트 ID',
'클라이언트명',
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
'<%= localeMessage.getString("infAdpConMan.thr") %>',
'<%= localeMessage.getString("infAdpConMan.thrTimeUnit") %>',
'<%= localeMessage.getString("infAdpConMan.useYn") %>'
],
colModel:[
{ name : 'NAME' , align:'left' , width:'100' , sortable:false },
{ name : 'DESC' , align:'left' , width:'200'},
{ name : 'THRESHOLDPERSECOND' , align:'right' , width:'60'},
{ name : 'THRESHOLD' , align:'right' , width:'60'},
{ name : 'THRESHOLDTIMEUNIT' , align:'center' , width:'80'},
{ name : 'USEYN' , align:'center' , width:'60' , editoptions:{value:"0:사용안함;1:사용함"}, formatter:"select" }
],
jsonReader: {
repeatitems:false
},
pager : $('#pager'),
page : '${param.page}',
rowNum : '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList : eval('[${rmsDefaultRowList}]'),
ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId);
var name = rowData['NAME'];
var url2 = url_view;
url2 += '?cmd=DETAIL';
url2 += '&page='+$(this).getGridParam("page");
url2 += '&returnUrl='+getReturnUrl();
url2 += '&menuId='+'${param.menuId}';
//검색값
url2 += '&searchName='+$("input[name=searchName]").val();
//key값
url2 += '&name='+name;
goNav(url2);
},
gridComplete:function (d){
var colModel = $(this).getGridParam("colModel");
for(var i = 0 ; i< colModel.length; i++){
$(this).setColProp(colModel[i].name, {sortable : false});
}
}
});
resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val()}, page:1 }).trigger("reloadGrid");
});
$("#btn_new").click(function(){
var url2 = url_view;
url2 += '?cmd=DETAIL';
url2 += '&page='+$("#grid").getGridParam("page");
url2 += '&returnUrl='+getReturnUrl();
url2 += '&menuId='+'${param.menuId}';
//검색값
url2 += '&searchName=';
goNav(url2);
});
$("input[name^=search]").keydown(function(key){
if (key.keyCode == 13){
$("#btn_search").click();
}
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div><!-- end content_top -->
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
</div>
<div class="title">클라이언트 유량제어<span class="tooltip">클라이언트(API Key) 유량제어를 관리한다</span></div>
<table class="search_condition" cellspacing=0;>
<tbody>
<tr>
<th style="width:180px;">클라이언트명</th>
<td><input type="text" name="searchName" value="${param.searchName}"></td>
</tr>
</tbody>
</table>
<table id="grid" ></table>
<div id="pager"></div>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -0,0 +1,516 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<!-- 페이지 커스텀 스타일 (fragment) -->
<jsp:include page="fragments/inflowGroupDetailStyle.jsp"/>
<script language="javascript">
var url ='<c:url value="/onl/admin/inflow/inflowClientControlMan.json" />';
var url_view ='<c:url value="/onl/admin/inflow/inflowClientControlMan.view" />';
var isDetail = false;
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
updateThresholdLabel();
if (typeof callback === 'function') {
callback(url, key);
}
},
error: function(e) {
alert(e.responseText);
}
});
}
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json;
$("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
});
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
},
error: function(e) {
alert(e.responseText);
}
});
}
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
//버킷 현황 조회
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', clientId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
// 전체 합산 계산
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
// 요약 영역 표시
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '그룹이 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.name}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 클라이언트를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
if (isDetail) {
postData.push({name: "cmd", value: "UPDATE"});
} else {
postData.push({name: "cmd", value: "INSERT"});
}
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_delete").click(function() {
$(this).blur();
var clientName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + clientName + ']</strong> 클라이언트를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"});
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.deleteMsg") %>", {
type: 'success',
title: '삭제 완료',
onClose: function() { goNav(returnUrl); }
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_previous").click(function() {
goNav(returnUrl);
});
buttonControl(isDetail);
titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
$(document).keydown(function(e) {
if (e.keyCode === 27) {
// ESC 키 처리 (필요시 모달 닫기 등)
}
});
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div><!-- end content_top -->
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title-wrap">
<div class="title">클라이언트 유량제어<span class="tooltip">클라이언트 유량제어</span></div>
<span id="modifiedAtInfo" class="modified-badge" style="display:none;">
<i class="material-icons">update</i><span id="modifiedAtText">-</span>
</span>
</div>
<!-- 탭 네비게이션 -->
<div class="detail-tabs">
<button type="button" class="detail-tab active" data-tab="config">
<i class="material-icons">settings</i> 설정
</button>
<button type="button" class="detail-tab" data-tab="status" id="tabStatus" style="display:none;">
<i class="material-icons">analytics</i> 현황
</button>
</div>
<!-- 설정 탭 -->
<div class="tab-content active" id="tabConfigContent">
<form id="ajaxForm">
<span class="section-label">기본 정보</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>클라이언트 ID</th>
<td><input type="text" name="name" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th>클라이언트명</th>
<td><input type="text" name="desc" style="width:300px; background-color: #ebebec;" /></td>
</tr>
<tr>
<th><%= localeMessage.getString("infAdpConMan.useYn") %></th>
<td>
<select name="useYn" class="styled-select"></select>
</td>
</tr>
</tbody>
</table>
<span class="section-label">유량제어 설정</span>
<table class="form-table" cellspacing="0">
<colgroup>
<col style="width:150px" />
<col />
</colgroup>
<tbody>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">1초 단위의 순간 트래픽 제한입니다. 매 초마다 설정한 건수만큼 토큰이 리필되며, 초과 시 요청이 거부됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<input type="number" name="thresholdPerSecond" placeholder="0" />
<span class="unit">req/sec</span>
</div>
</td>
</tr>
<tr>
<th>
<%= localeMessage.getString("infAdpConMan.thr") %>
<span class="hint-icon"><i class="material-icons">help_outline</i>
<span class="hint-bubble">장시간 누적 트래픽 제한입니다. 시간단위를 선택하면 해당 주기마다 토큰이 리필됩니다.</span>
</span>
</th>
<td>
<div class="threshold-group">
<select name="thresholdTimeUnit" class="styled-select"></select>
<input type="number" name="threshold" id="thresholdInput" placeholder="0" />
<span class="unit" id="thresholdUnit">requests</span>
</div>
</td>
</tr>
</tbody>
</table>
</form>
</div><!-- /tabConfigContent -->
<!-- 현황 탭 -->
<div class="tab-content" id="tabStatusContent">
<div class="bucket-status-wrap">
<div class="bucket-refresh-bar">
<span class="last-refresh">마지막 갱신: <span id="lastRefreshTime">-</span></span>
<button type="button" class="cssbtn" id="btn_refresh_bucket"><i class="material-icons">refresh</i> 새로고침</button>
</div>
<!-- 전체 합산 요약 -->
<div class="bucket-summary" id="bucketSummary" style="display:none;">
<div class="bucket-summary-title"><i class="material-icons">functions</i> 전체 인스턴스 합산</div>
<div class="bucket-summary-grid">
<div class="bucket-summary-item">
<span class="bucket-summary-label">활성 인스턴스</span>
<span class="bucket-summary-value" id="summaryInstanceCount">0<span class="unit">개</span></span>
</div>
<div class="bucket-summary-item" id="summaryPerSecondWrap">
<span class="bucket-summary-label">초당 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryPerSecond">0<span class="unit">req/sec</span></span>
<span class="bucket-summary-sub" id="summaryPerSecondCalc"></span>
</div>
<div class="bucket-summary-item" id="summaryThresholdWrap">
<span class="bucket-summary-label">추가 임계치 (전체)</span>
<span class="bucket-summary-value" id="summaryThreshold">0<span class="unit">req</span></span>
<span class="bucket-summary-sub" id="summaryThresholdCalc"></span>
</div>
<div class="bucket-summary-item">
<span class="bucket-summary-label">적용 API</span>
<span class="bucket-summary-value" id="summaryActiveApi">0<span class="unit">개</span></span>
<span class="bucket-summary-sub" id="summaryActiveApiList"></span>
</div>
</div>
</div>
<div class="bucket-server-list" id="bucketServerList">
<div class="bucket-empty-msg">
<i class="material-icons">hourglass_empty</i>
버킷 현황을 조회하려면 새로고침 버튼을 클릭하세요
</div>
</div>
</div>
</div><!-- /tabStatusContent -->
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -16,173 +16,486 @@
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/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_view ='<c:url value="/onl/admin/inflow/inflowInterfaceControlMan.view" />';
var isDetail = false;
function init(url,key,callback){
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
function updateThresholdLabel() {
var timeUnit = $('select[name=thresholdTimeUnit]').val();
var $input = $('#thresholdInput');
var $unit = $('#thresholdUnit');
var unitLabels = {
'': '',
'SEC': 'req/sec',
'MIN': 'req/min',
'HOU': 'req/hour',
'DAY': 'req/day',
'MON': 'req/month'
};
$unit.text(unitLabels[timeUnit] || 'requests');
if (timeUnit === '') {
$input.prop('disabled', true).val('');
$unit.css('visibility', 'hidden');
} else {
$input.prop('disabled', false);
$unit.css('visibility', 'visible');
}
}
function init(url, key, callback) {
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function(json) {
new makeOptions("CODE","NAME").setObj($("select[name=useYn]")).setData(json.useYnRows).setFormat(codeName3OptionFormat).rendering();
new makeOptions("CODE","NAME").setObj($("select[name=thresholdTimeUnit]")).setNoValueInclude(true).setData(json.timeUnitRows).setFormat(codeName3OptionFormat).rendering();
if(typeof callback === 'function') {
callback(url,key);
updateThresholdLabel();
if (typeof callback === 'function') {
callback(url, key);
}
},
error:function(e){
error: function(e) {
alert(e.responseText);
}
});
}
function detail(url,key){
if (!isDetail)return;
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'DETAIL', name : key},
success:function(json){
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {cmd: 'DETAIL', name: key},
success: function(json) {
var data = json;
$("input[name=name]").attr('readonly',true);
$("input[name=desc]").attr('readonly',true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function(){
$("input[name=name]").attr('readonly', true);
$("input[name=desc]").attr('readonly', true);
$("#ajaxForm input[type!=radio],#ajaxForm select,#ajaxForm textarea").each(function() {
var name = $(this).attr("name");
var tag = $(this).prop("tagName").toLowerCase();
$(tag+"[name="+name+"]").val(data[name.toUpperCase()]);
});
updateThresholdLabel();
var modifiedAt = data.MODIFIEDAT || data.modifiedAt;
if (modifiedAt) {
$('#modifiedAtText').text(modifiedAt);
$('#modifiedAtInfo').show();
}
},
error:function(e){
error: function(e) {
alert(e.responseText);
}
});
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key ="${param.name}";
if (key != "" && key !="null"){
isDetail = true;
}
init(url,key,detail);
$("#btn_modify").click(function(){
var postData = $('#ajaxForm').serializeArray();
if (isDetail){
postData.push({ name: "cmd" , value:"UPDATE"});
}else{
postData.push({ name: "cmd" , value:"INSERT"});
function switchTab(tabName) {
$('.detail-tab').removeClass('active');
$('.detail-tab[data-tab="' + tabName + '"]').addClass('active');
$('.tab-content').removeClass('active');
$('#tab' + tabName.charAt(0).toUpperCase() + tabName.slice(1) + 'Content').addClass('active');
if (tabName === 'status' && isDetail) {
loadBucketStatus();
}
}
function loadBucketStatus() {
$('#bucketServerList').html('<div class="bucket-empty-msg"><i class="material-icons">hourglass_empty</i>조회 중...</div>');
$.ajax({
type: 'POST',
url: url,
data: { cmd: 'LIST_BUCKET_STATUS', interfaceId: "${param.name}" },
dataType: 'json',
success: function(result) {
renderBucketStatus(result.servers || []);
$('#lastRefreshTime').text(formatDateTime(new Date()));
},
error: function(e) {
$('#bucketServerList').html('<div class="bucket-empty-msg bucket-error-msg"><i class="material-icons">error_outline</i>조회 실패: ' + e.statusText + '</div>');
}
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl);//LIST로 이동
},
error:function(e){
alert(e.responseText);
});
}
function formatDateTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function renderBucketStatus(servers) {
var container = $('#bucketServerList');
container.empty();
if (servers.length === 0) {
container.html('<div class="bucket-empty-msg"><i class="material-icons">dns</i>등록된 서버가 없습니다</div>');
$('#bucketSummary').hide();
return;
}
var onlineCount = 0;
var perSecondCapacity = 0;
var thresholdCapacity = 0;
var thresholdTimeUnit = '';
$.each(servers, function(i, server) {
if (server.status === 'online' && server.data && server.data.buckets) {
onlineCount++;
$.each(server.data.buckets, function(j, bucket) {
if (bucket.type === 'perSecond') {
perSecondCapacity += bucket.capacity || 0;
} else if (bucket.type === 'threshold') {
thresholdCapacity += bucket.capacity || 0;
if (!thresholdTimeUnit) thresholdTimeUnit = bucket.timeUnit;
}
});
}
});
if (onlineCount > 0) {
$('#summaryInstanceCount').html(onlineCount + '<span class="unit">개</span>');
var configPerSecond = parseInt($('input[name=thresholdPerSecond]').val()) || 0;
var configThreshold = parseInt($('input[name=threshold]').val()) || 0;
if (configPerSecond > 0) {
$('#summaryPerSecond').html(formatNumber(perSecondCapacity) + '<span class="unit">req/sec</span>');
$('#summaryPerSecondCalc').text(formatNumber(configPerSecond) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryPerSecondWrap').show();
} else {
$('#summaryPerSecondWrap').hide();
}
if (configThreshold > 0) {
var timeUnitLabel = getTimeUnitText(thresholdTimeUnit);
$('#summaryThreshold').html(formatNumber(thresholdCapacity) + '<span class="unit">req/' + timeUnitLabel + '</span>');
$('#summaryThresholdCalc').text(formatNumber(configThreshold) + ' × ' + onlineCount + ' 인스턴스');
$('#summaryThresholdWrap').show();
} else {
$('#summaryThresholdWrap').hide();
}
$('#bucketSummary').show();
} else {
$('#bucketSummary').hide();
}
$.each(servers, function(i, server) {
var statusClass = server.status || 'offline';
var statusText = { online: '정상', offline: '오프라인', no_data: '데이터 없음' };
var card = '<div class="bucket-server-card">' +
'<div class="bucket-server-header">' +
'<div>' +
'<span class="bucket-server-name">' + server.serverName + '</span> ' +
'<span class="bucket-server-ip">' + server.serverIp + ':' + server.serverPort + '</span>' +
'</div>' +
'<span class="bucket-server-status ' + statusClass + '">' + (statusText[statusClass] || statusClass) + '</span>' +
'</div>' +
'<div class="bucket-server-body">' + renderBucketBody(server) + '</div>' +
'</div>';
container.append(card);
});
}
function renderBucketBody(server) {
if (server.status === 'offline') {
return '<div class="bucket-error-msg">' + (server.message || '서버 연결 실패') + '</div>';
}
if (server.status === 'no_data') {
return '<div class="bucket-empty-msg" style="padding:10px;"><i class="material-icons" style="font-size:18px;">info_outline</i> ' + (server.message || '인터페이스가 로드되지 않음') + '</div>';
}
var data = server.data;
if (!data || !data.buckets || data.buckets.length === 0) {
return '<div class="bucket-empty-msg" style="padding:10px;">버킷 정보 없음</div>';
}
var html = '<div class="bucket-info-grid">';
$.each(data.buckets, function(j, bucket) {
var typeLabel = bucket.type === 'perSecond' ? '초당 임계치' : '추가 임계치';
var timeUnitText = getTimeUnitText(bucket.timeUnit);
var usagePercent = bucket.usagePercent || 0;
var progressClass = usagePercent < 50 ? 'low' : (usagePercent < 80 ? 'medium' : 'high');
html += '<div class="bucket-card">' +
'<div class="bucket-card-header">' +
'<span class="bucket-type">' + typeLabel + '</span>' +
'<span class="bucket-type-badge">' + timeUnitText + '</span>' +
'</div>' +
'<div class="bucket-progress-wrap">' +
'<div class="bucket-progress-bar">' +
'<div class="bucket-progress-fill ' + progressClass + '" style="width:' + usagePercent + '%"></div>' +
'</div>' +
'</div>' +
'<div class="bucket-stats">' +
'<span>사용량: <span class="bucket-stats-value">' + usagePercent.toFixed(1) + '%</span></span>' +
'<span>잔여: <span class="bucket-stats-value">' + formatNumber(bucket.availableTokens) + '</span> / ' + formatNumber(bucket.capacity) + '</span>' +
'</div>' +
'</div>';
});
html += '</div>';
return html;
}
function getTimeUnitText(timeUnit) {
var units = {SEC: '초', MIN: '분', HOU: '시간', DAY: '일', MON: '월'};
return units[timeUnit] || timeUnit || '-';
}
function formatNumber(num) {
if (num === undefined || num === null) return '-';
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = "${param.name}";
if (key != "" && key != "null") {
isDetail = true;
}
init(url, key, detail);
$("select[name=thresholdTimeUnit]").on('change', function() {
updateThresholdLabel();
});
$("#btn_modify").click(function() {
$(this).blur();
var confirmMsg = isDetail ? '변경사항을 저장하시겠습니까?' : '새 인터페이스를 등록하시겠습니까?';
showConfirm(confirmMsg, {
title: '저장 확인',
confirmText: '저장',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
if (isDetail) {
postData.push({name: "cmd", value: "UPDATE"});
} else {
postData.push({name: "cmd", value: "INSERT"});
}
var isInsert = !isDetail;
$.ajax({
type: "POST",
url: url,
data: postData,
success: function(args) {
showAlert("<%= localeMessage.getString("common.saveMsg") %>", {
type: 'success',
title: '저장 완료',
onClose: isInsert ? function() { goNav(returnUrl); } : undefined
});
},
error: function(e) {
showAlert(e.responseText, {type: 'error'});
}
});
}
});
});
$("#btn_delete").click(function(){
if ( confirm( '<%=localeMessage.getString("common.confirmMsg")%>' ) != true ) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: "cmd" , value:"DELETE"});
$.ajax({
type : "POST",
url:url,
data:postData,
success:function(args){
alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl);//LIST로 이동
},
error:function(e){
alert(e.responseText);
$("#btn_delete").click(function() {
$(this).blur();
var ifName = $('input[name=desc]').val() || $('input[name=name]').val();
showConfirm('<strong>[' + ifName + ']</strong> 인터페이스를 삭제하시겠습니까?', {
type: 'error',
title: '삭제 확인',
confirmText: '삭제',
onConfirm: function() {
var postData = $('#ajaxForm').serializeArray();
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);
titleControl(isDetail);
$('.detail-tab').click(function() {
switchTab($(this).data('tab'));
});
if (isDetail) {
$('#tabStatus').show();
}
$('#btn_refresh_bucket').click(function() {
loadBucketStatus();
});
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div><!-- end content_top -->
<div class="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
<%-- <img src="<c:url value="/img/btn_delete.png"/>" alt="" id="btn_delete" level="W" status="DETAIL"/> --%>
<%-- <img src="<c:url value="/img/btn_modify.png"/>" alt="" id="btn_modify" level="W" status="DETAIL,NEW" /> --%>
<%-- <img src="<c:url value="/img/btn_previous.png"/>" alt="" id="btn_previous" level="R" status="DETAIL,NEW"/> --%>
</div>
<div 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>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div><!-- end content_top -->
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.save") %></button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title-wrap">
<div class="title"><%= 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:'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() {
$('#effectGrid').jqGrid({
@@ -0,0 +1,153 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript">
var url = '<c:url value="/onl/admin/security/cryptoModuleMan.json" />';
var url_view = '<c:url value="/onl/admin/security/cryptoModuleMan.view" />';
$(document).ready(function() {
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
url: url,
postData: {
cmd: 'LIST',
searchName: $('input[name=searchName]').val(),
algType: $('select[name=algType]').val(),
keySourceType: $('select[name=keySourceType]').val(),
useYn: $('select[name=useYn]').val()
},
colNames: ['ID', '모듈명', '설명', '알고리즘', '운영모드', '키 소스', '사용'],
colModel: [
{ name: 'CRYPTO_ID', hidden: true },
{ name: 'CRYPTO_NAME', align: 'left' },
{ name: 'CRYPTO_DESC', align: 'left', sortable: false },
{ name: 'ALG_TYPE', align: 'center', width: 80 },
{ name: 'CIPHER_MODE', align: 'center', width: 80 },
{ name: 'KEY_SOURCE_TYPE', align: 'center', width: 100 },
{ name: 'USE_YN', align: 'center', width: 60 }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId);
var cryptoId = rowData['CRYPTO_ID'];
var url2 = url_view + '?cmd=DETAIL';
url2 += '&page=' + $(this).getGridParam("page");
url2 += '&returnUrl=' + getReturnUrl();
url2 += '&menuId=' + '${param.menuId}';
url2 += '&searchName=' + $("input[name=searchName]").val();
url2 += '&cryptoId=' + cryptoId;
goNav(url2);
},
gridComplete: function() {
var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, { sortable: false });
}
},
loadError: function(jqXHR, textStatus, errorThrown) {
var location = '<%=request.getContextPath()%>/';
comloadError(jqXHR, textStatus, errorThrown, location);
}
});
resizeJqGridWidth('grid', 'content_middle', '1000');
$("#btn_search").click(function() {
var postData = getSearchForJqgrid("cmd", "LIST");
postData['algType'] = $('select[name=algType]').val();
postData['keySourceType'] = $('select[name=keySourceType]').val();
postData['useYn'] = $('select[name=useYn]').val();
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
});
$("#btn_new").click(function() {
var url2 = url_view + '?cmd=DETAIL';
url2 += '&page=' + $("#grid").getGridParam("page");
url2 += '&returnUrl=' + getReturnUrl();
url2 += '&menuId=' + '${param.menuId}';
goNav(url2);
});
$("input[name=searchName]").keydown(function(key) {
if (key.keyCode == 13) { $("#btn_search").click(); }
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> 신규</button>
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> 검색</button>
</div>
<div class="title">암호화 모듈 설정</div>
<form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing="0">
<tbody>
<tr>
<th style="width:100px;">모듈명</th>
<td><input type="text" name="searchName" value="${param.searchName}"></td>
<th style="width:100px;">알고리즘</th>
<td>
<select name="algType">
<option value="">전체</option>
<option value="AES">AES</option>
<option value="ARIA">ARIA</option>
</select>
</td>
<th style="width:100px;">키 소스</th>
<td>
<select name="keySourceType">
<option value="">전체</option>
<option value="STATIC">STATIC</option>
<option value="DYNAMIC">DYNAMIC</option>
</select>
</td>
<th style="width:80px;">사용</th>
<td>
<select name="useYn">
<option value="">전체</option>
<option value="Y">Y</option>
<option value="N">N</option>
</select>
</td>
</tr>
</tbody>
</table>
</form>
<table id="grid"></table>
<div id="pager"></div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,365 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script language="javascript">
var url = '<c:url value="/onl/admin/security/cryptoModuleMan.json" />';
var url_view = '<c:url value="/onl/admin/security/cryptoModuleMan.view" />';
var isDetail = false;
function isValid() {
if ($('input[name=cryptoName]').val().trim() === '') {
alert('모듈명을 입력하세요.');
$('input[name=cryptoName]').focus();
return false;
}
if ($('select[name=algType]').val() === '') {
alert('알고리즘을 선택하세요.');
return false;
}
if ($('select[name=cipherMode]').val() === '') {
alert('운영모드를 선택하세요.');
return false;
}
var keySourceType = $('select[name=keySourceType]').val();
if (keySourceType === '') {
alert('키 소스 유형을 선택하세요.');
return false;
}
if (keySourceType === 'STATIC') {
if ($('input[name=encKeyHex]').val().trim() === '') {
alert('암호화 키(Hex)를 입력하세요.');
$('input[name=encKeyHex]').focus();
return false;
}
}
if (keySourceType === 'DYNAMIC') {
if ($('input[name=keyDerivStrategy]').val().trim() === '') {
alert('키 도출 전략 FQCN을 입력하세요.');
$('input[name=keyDerivStrategy]').focus();
return false;
}
if ($('textarea[name=keyDerivParams]').val().trim() === '') {
alert('키 도출 파라미터(JSON)를 입력하세요.');
$('textarea[name=keyDerivParams]').focus();
return false;
}
}
return true;
}
function textToHex(text) {
if (!text) return '';
var encoder = new TextEncoder();
var bytes = encoder.encode(text);
return Array.from(bytes).map(function(b) {
return b.toString(16).padStart(2, '0');
}).join('').toUpperCase();
}
function updateHexFromText(textEl, hexName, lenSpanId) {
var hex = textToHex($(textEl).val());
$('input[name=' + hexName + ']').val(hex);
updateHexLength(hex, lenSpanId);
}
function updateHexLength(hex, lenSpanId) {
var charLen = hex ? hex.replace(/\s/g, '').length : 0;
var byteLen = Math.floor(charLen / 2);
var msg = charLen > 0 ? byteLen + ' bytes (' + charLen + '자)' : '';
$('#' + lenSpanId).text(msg);
}
function toggleKeySourceFields() {
var keySourceType = $('select[name=keySourceType]').val();
if (keySourceType === 'STATIC') {
$('.static-section').show();
$('.dynamic-section').hide();
} else if (keySourceType === 'DYNAMIC') {
$('.static-section').hide();
$('.dynamic-section').show();
} else {
$('.static-section').show();
$('.dynamic-section').show();
}
}
function detail(url, key) {
if (!isDetail) return;
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: { cmd: 'DETAIL', cryptoId: key },
success: function(data) {
$('input[name=cryptoId]').val(data['CRYPTO_ID']);
$('input[name=cryptoName]').val(data['CRYPTO_NAME']).attr('readonly', true);
$('input[name=cryptoDesc]').val(data['CRYPTO_DESC']);
$('select[name=algType]').val(data['ALG_TYPE']);
$('select[name=cipherMode]').val(data['CIPHER_MODE']);
$('select[name=padding]').val(data['PADDING'] || '');
$('input[name=ivHex]').val(data['IV_HEX']);
$('select[name=keySourceType]').val(data['KEY_SOURCE_TYPE']);
$('input[name=encKeyHex]').val(data['ENC_KEY_HEX']);
$('input[name=decKeyHex]').val(data['DEC_KEY_HEX']);
$('input[name=keyDerivStrategy]').val(data['KEY_DERIV_STRATEGY']);
$('textarea[name=keyDerivParams]').val(data['KEY_DERIV_PARAMS']);
$('select[name=cacheYn]').val(data['CACHE_YN']);
$('input[name=cacheTtlSec]').val(data['CACHE_TTL_SEC']);
$('select[name=useYn]').val(data['USE_YN']);
updateHexLength(data['ENC_KEY_HEX'], 'encKeyLen');
updateHexLength(data['DEC_KEY_HEX'], 'decKeyLen');
updateHexLength(data['IV_HEX'], 'ivHexLen');
if (data['MODIFIED_BY'] || data['MODIFIED_AT']) {
$('#span_modified_by').text(data['MODIFIED_BY'] || '');
$('#span_modified_at').text((data['MODIFIED_AT'] || '').replace('T', ' '));
$('#row_modified_info').show();
}
toggleKeySourceFields();
},
error: function(e) {
alert(e.responseText);
}
});
}
$(document).ready(function() {
var returnUrl = getReturnUrlForReturn();
var key = '${param.cryptoId}';
if (key !== '' && key !== 'null') {
isDetail = true;
}
detail(url, key);
$('select[name=keySourceType]').change(function() {
toggleKeySourceFields();
});
$('input[name=encKeyHex]').on('input', function() {
updateHexLength($(this).val(), 'encKeyLen');
});
$('input[name=decKeyHex]').on('input', function() {
updateHexLength($(this).val(), 'decKeyLen');
});
$('input[name=ivHex]').on('input', function() {
updateHexLength($(this).val(), 'ivHexLen');
});
toggleKeySourceFields();
$('#btn_modify').click(function() {
if (!isValid()) return;
var postData = $('#ajaxForm').serializeArray();
postData.push({ name: 'cmd', value: isDetail ? 'UPDATE' : 'INSERT' });
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: postData,
success: function(data) {
var msg = '<%= localeMessage.getString("common.saveMsg") %>';
if (data && data.broadcastResult) {
msg += '\n\n[서버 반영 결과]\n' + data.broadcastResult;
}
alert(msg);
goNav(returnUrl);
},
error: function(e) {
alert(e.responseText);
}
});
});
$('#btn_delete').click(function() {
if (confirm('<%= localeMessage.getString("common.confirmMsg") %>') !== true) return;
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: { cmd: 'DELETE', cryptoId: $('input[name=cryptoId]').val() },
success: function(data) {
var msg = '<%= localeMessage.getString("common.deleteMsg") %>';
if (data && data.broadcastResult) {
msg += '\n\n[서버 반영 결과]\n' + data.broadcastResult;
}
alert(msg);
goNav(returnUrl);
},
error: function(e) {
alert(e.responseText);
}
});
});
$('#btn_previous').click(function() {
goNav(returnUrl);
});
buttonControl(isDetail);
titleControl(isDetail);
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> 삭제</button>
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i class="material-icons">save</i> 저장</button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> 이전</button>
</div>
<div class="title">암호화 모듈 설정 상세</div>
<form id="ajaxForm">
<input type="hidden" name="cryptoId" />
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;">모듈명 *</th>
<td><input type="text" name="cryptoName" style="width:300px;" /></td>
</tr>
<tr>
<th>설명</th>
<td><input type="text" name="cryptoDesc" style="width:400px;" /></td>
</tr>
<tr>
<th>알고리즘 *</th>
<td>
<select name="algType">
<option value="">선택</option>
<option value="AES">AES</option>
<option value="ARIA">ARIA</option>
</select>
</td>
</tr>
<tr>
<th>운영모드 *</th>
<td>
<select name="cipherMode">
<option value="">선택</option>
<option value="CBC">CBC</option>
<option value="GCM">GCM</option>
<option value="ECB">ECB</option>
<option value="FF1">FF1</option>
<option value="FF3-1">FF3-1</option>
</select>
</td>
</tr>
<tr>
<th>패딩</th>
<td>
<select name="padding">
<option value="">없음</option>
<option value="PKCS5Padding">PKCS5Padding</option>
<option value="NoPadding">NoPadding</option>
</select>
</td>
</tr>
<tr>
<th>IV (Hex)</th>
<td>
<input type="text" name="ivHex" style="width:380px;" placeholder="HEX 직접 입력" />
<span id="ivHexLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
<br/>
<input type="text" id="ivHexText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
oninput="updateHexFromText(this, 'ivHex', 'ivHexLen')" />
<span class="help-inline">CBC: 16바이트(32자) 필수 / GCM: 설정 시 기본 AAD로 사용</span>
</td>
</tr>
<tr>
<th>키 소스 유형 *</th>
<td>
<select name="keySourceType">
<option value="">선택</option>
<option value="STATIC">STATIC</option>
<option value="DYNAMIC">DYNAMIC</option>
</select>
</td>
</tr>
<!-- STATIC 전용 -->
<tr class="static-section">
<th>암호화 키 (Hex) *</th>
<td>
<input type="text" name="encKeyHex" style="width:380px;" placeholder="HEX 직접 입력" />
<span id="encKeyLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
<br/>
<input type="text" id="encKeyText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
oninput="updateHexFromText(this, 'encKeyHex', 'encKeyLen')" />
<span class="help-inline">AES-128: 16bytes(32자) / AES-192: 24bytes(48자) / AES-256: 32bytes(64자)</span>
</td>
</tr>
<tr class="static-section">
<th>복호화 키 (Hex)</th>
<td>
<input type="text" name="decKeyHex" style="width:380px;" placeholder="HEX 직접 입력" />
<span id="decKeyLen" style="margin-left:8px; color:#888; font-size:12px;"></span>
<br/>
<input type="text" id="decKeyText" style="width:260px; margin-top:4px;" placeholder="텍스트 입력 → HEX 자동 변환"
oninput="updateHexFromText(this, 'decKeyHex', 'decKeyLen')" />
<span class="help-inline">미입력 시 암호화 키와 동일</span>
</td>
</tr>
<!-- DYNAMIC 전용 -->
<tr class="dynamic-section">
<th>키 도출 전략 (FQCN) *</th>
<td><input type="text" name="keyDerivStrategy" style="width:500px;" /></td>
</tr>
<tr class="dynamic-section">
<th>키 도출 파라미터 (JSON) *</th>
<td><textarea name="keyDerivParams" style="width:500px; height:80px;"></textarea></td>
</tr>
<tr>
<th>동적 키 캐시 *</th>
<td>
<select name="cacheYn">
<option value="N">N</option>
<option value="Y">Y</option>
</select>
</td>
</tr>
<tr>
<th>캐시 TTL (초)</th>
<td>
<input type="number" name="cacheTtlSec" style="width:100px;" />
<span class="help-inline">기본값 300</span>
</td>
</tr>
<tr>
<th>사용 여부 *</th>
<td>
<select name="useYn">
<option value="Y">Y</option>
<option value="N">N</option>
</select>
</td>
</tr>
<tr id="row_modified_info" style="display:none;">
<th>수정자 / 수정일시</th>
<td>
<span id="span_modified_by" style="font-weight:bold;"></span>
<span style="margin:0 8px; color:#ccc;">|</span>
<span id="span_modified_at" style="color:#555;"></span>
</td>
</tr>
</table>
</form>
</div>
</div>
</body>
</html>
@@ -44,14 +44,16 @@
'<%=localeMessage.getString("stdDefaultMan.columDesc")%>',
'<%=localeMessage.getString("stdDefaultMan.columLen")%>',
'<%=localeMessage.getString("stdDefaultMan.keyOr")%>',
'<%=localeMessage.getString("stdDefaultMan.ifOr")%>'
'<%=localeMessage.getString("stdDefaultMan.ifOr")%>',
'<%=localeMessage.getString("stdDefaultMan.orderSeq")%>'
],
colModel:[
{ name : 'COLUMNNAME' , align:'left' , sortable:false },
{ name : 'COLUMNDESC' , align:'left' },
{ name : 'COLUMNLENGTH' , align:'right' },
{ name : 'ISKEY' , align:'center' },
{ name : 'ISINTERFACE' , align:'center' }
{ name : 'COLUMNNAME' , align:'left' , width: '200px', sortable:false },
{ name : 'COLUMNDESC' , align:'left' , width: '200px' },
{ name : 'COLUMNLENGTH' , align:'center' , width: '100px' },
{ name : 'ISKEY' , align:'center', width: '100px' },
{ name : 'ISINTERFACE' , align:'center', width: '100px' },
{ name : 'ORDERSEQ' , align:'center', width: '100px' }
],
jsonReader: {
repeatitems:false
@@ -38,12 +38,22 @@
var loginUser = '<%= loginVo.getUserId() %>';
var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />';
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 () {
var key = "${param.id}";
if (key) {
detail(key);
}
$("#expectEndDate").datepicker().inputmask("yyyy-mm-dd", {'autoUnmask': true});
$("#btn_app_approve").click(function () {
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 () {
window.close();
@@ -126,12 +158,25 @@
data: {cmd: 'DETAIL', id: key},
success: function (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 사용");
$("#approvalStatus").text(data.approvalStatus.description);
$("#requesterName").text(data.requester.userName);
$("#approvalSubject").text(data.approvalSubject);
$("#approvalDetail").html(data.approvalDetail);
$("#createdDate").text(data.createdDate).inputmask("9999-99-99 99:99:99.999", {'autoUnmask': true});
$("#expectEndDate").val(data.expectEndDate);
if (data.approvers.some(approver =>
approver.approverStatus === 'CURRENT' &&
@@ -144,7 +189,7 @@
document.getElementById("btn_app_reject").style.display = "none";
}
if (data.approvalStatus.description === '배포실패') {
if (data.approvalStatus.code === 'FAILED') {
document.getElementById("btn_app_redeploy").style.display = "";
}
@@ -242,9 +287,14 @@
</tr>
<tr>
<th>상세 내용</th>
<td colspan="3">
<td>
<div id="approvalDetail" style="white-space: pre-wrap;"></div>
</td>
<th>예상완료일</th>
<td>
<input type="text" id="expectEndDate" style="width: 120px; text-align:center"/>
<span id="spExpectEndDate"></span>
</td>
</tr>
</table>
@@ -282,6 +332,9 @@
</table>
</div>
<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
class="material-icons">check</i>승인
</button>
@@ -17,10 +17,59 @@
<script language="javascript" >
var url = '<c:url value="/onl/apim/approval/portalApprovalMan.json" />';
var url_view = '<c:url value="/onl/apim/approval/portalApprovalMan.view" />';
var hardDeleteEnabled = ${hardDeleteEnabled};
const APPROVAL_TYPE_DISPLAY = {
'USER': '법인사용자',
'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() {
$('#grid').jqGrid({
@@ -33,7 +82,7 @@
searchApprovalStatus: $('select[name=searchApprovalStatus]').val(),
searchApprovalSubject: $('input[name=searchApprovalSubject]').val()
},
colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일'],
colNames: ['No.', 'id', '제목', '승인 유형', '승인 유형', '승인 상태','요청자', '생성일', '예상완료일'],
colModel: [
{ name: 'rowNum', align: 'center', width: '30', sortable: false},
{ name: 'id', align: 'center', key: true, hidden: true },
@@ -42,7 +91,8 @@
{ name: 'approvalType', align: 'center', width: 80, hidden: true},
{ name: 'approvalStatus.description', align: 'center', width: 80 },
{ 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: {
repeatitems: false
@@ -53,6 +103,8 @@
viewrecords: true,
autowidth: true,
height: 'auto',
multiselect: true,
multiboxonly: true,
ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId);
var id = rowData['id'];
@@ -93,6 +145,10 @@
$("#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) {
if (key.keyCode == 13) {
$("#btn_search").click();
@@ -112,9 +168,17 @@
</div><!-- end content_top -->
<div class="content_middle" id="content_middle">
<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>
</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;">
<table class="search_condition" cellspacing="0">
<tbody>
@@ -134,8 +198,8 @@
<div class="select-style">
<select name="searchApprovalStatus">
<option value="">전체</option>
<option value="CREATED">생성됨</option>
<option value="REQUESTED">요청됨</option>
<option value="PROCESSING">진행중</option>
<option value="DENIED">반려됨</option>
<option value="END">승인됨</option>
</select>
@@ -20,15 +20,22 @@
var select_orgId = "";
function formatInquiryStatus(cellvalue, options, rowObject) {
return cellvalue === 'PENDING'
? '<span style="color: red;">문의중</span>'
: '<span>답변완료</span>';
if (cellvalue == 'PENDING') {
return '<span style="color: red;">문의중</span>';
} else if (cellvalue == 'RESPONDED') {
return '<span >답변완료</span>'
} else if (cellvalue == 'CLOSED') {
return '<span >답변종료</span>'
} else {
return cellvalue;
}
}
function formatFile(cellvalue, options, rowObject) {
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;">' : '';
return cellvalue + icon;
var commentCount = rowObject.commentCount > 0 ? ' <span style="color: #337ab7;">[' + rowObject.commentCount + ']</span>' : '';
return cellvalue + icon + commentCount;
}
function deleteSelectedRows() {
@@ -36,6 +36,13 @@
padding-bottom: 10px;
border-bottom: 2px solid #e0e0e0; /* 제목 아래 줄 긋기 */
}
.comment-section {
margin-bottom: 30px;
border: 1px solid #e0e0e0; /* 동일한 박스 스타일 */
padding: 20px;
border-radius: 5px;
}
.info-row {
margin-bottom: 10px;
@@ -57,6 +64,15 @@
width: 100%;
min-height: 200px;
}
#commentDetail {
min-height: 100px;
border: 1px solid #00000032;
border-radius: 4px;
padding: 10px;
flex: 1;
font-family: '맑은고딕'
}
.required-mark {
color: red;
@@ -68,6 +84,7 @@
var url = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.json" />';
var url_view = '<c:url value="/onl/apim/portalinquiry/portalInquiryMan.view" />';
var file_download = '<c:url value="/file/download.do" />';
var inquiryStatus = 'PENDING';
function decodeHTMLEntities(text) {
var textArea = document.createElement('textarea');
@@ -82,10 +99,21 @@
dataType: "json",
data: {cmd: 'DETAIL', id: key},
success: function (data) {
$("#id").val(key);
$("#inquirySubject").text(data.inquirySubject);
$("#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);
$("#createdDate").text(timeStampFormat(data.createdDate));
$("#responder").text(data.responderName || '');
@@ -94,6 +122,8 @@
var decodedContent = decodeHTMLEntities(data.responseDetail);
$("#responseDetail").summernote('code', decodedContent || '');
// 댓글 표시
listComment(key);
// 첨부 파일 표시
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 () {
var returnUrl = getReturnUrlForReturn();
@@ -114,12 +197,17 @@
placeholder: '여기에 답변을 입력하세요.',
height: 200
});
if (key) {
detail(key);
}
$("#btn_modify").click(function () {
if (inquiryStatus == 'CLOSED') {
alert('답변이 종료되어 수정할 수 없습니다');
return;
}
if (!checkRequired("ajaxForm")) 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 () {
goNav(returnUrl);
@@ -206,6 +320,10 @@
<!-- 답변 작성 부분 -->
<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-label">답변자</div>
<div id="responder" class="info-content"></div>
@@ -221,6 +339,19 @@
</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">
</form>
</div>
@@ -17,12 +17,29 @@
<script language="javascript" >
var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />';
var url_view = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.view" />';
var combo;
function formatNoticeType(cellvalue, options, rowObject) {
var name = "";
for (var i = 0; i < combo.noticeTypeList.length; i++) {
if (combo.noticeTypeList[i].CODE == cellvalue) {
return combo.noticeTypeList[i].NAME;
}
}
return cellvalue;
}
function formatuseYn(cellvalue, options, rowObject) {
return cellvalue === 'N'
? '<span style="color: red;">미사용</span>'
: '<span>사용</span>';
}
function formatFixYn(cellvalue, options, rowObject) {
return cellvalue === 'Y'
? '고정'
: '';
}
function formatFile(cellvalue, options, rowObject) {
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
@@ -30,21 +47,76 @@
return cellvalue + icon;
}
function deleteSelectedRows() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
if (selectedIds.length === 0) {
alert('항목을 선택하세요.');
return;
}
if (!confirm('선택된 ' + selectedIds.length + '개 항목을 삭제하시겠습니까?')) {
return;
}
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {
cmd: 'DELETE_BULK',
ids: selectedIds.join(',')
},
success: function(data) {
alert('삭제되었습니다.');
$("#grid").jqGrid('clearGridData', true);
$("#grid").trigger("reloadGrid");
},
error: function(e) {
alert('삭제 중 오류가 발생했습니다.');
}
});
}
function init(){
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
console.log('init', json);
combo = json;
new makeOptions("CODE","NAME").setObj($("select[name=searchNoticeType]")).setNoValueInclude(true).setNoValue('','<%=localeMessage.getString("combo.all")%>').setData(json.noticeTypeList).rendering();
putSelectFromParam();
},
error:function(e){
alert(e.responseText);
}
});
}
$(document).ready(function() {
$('#grid').jqGrid({
datatype:"json",
mtype: 'POST',
url: url,
postData : { cmd : 'LIST',
searchNoticeType: $('select[name=searchNoticeType]').val(),
searchUseYn: $('select[name=searchUseYn]').val(),
searchNoticeSubject: $('input[name=searchNoticeSubject]').val(),
searchNoticeDetail: $('input[name=searchNoticeDetail]').val()
},
colNames:['id', 'No.', '제목', '사용', '마지막 수정일', '등록일', '등록자', '조회수'],
colNames:['id', 'No.', '게시유형', '제목', '고정여부', '사용', '마지막 수정일', '등록일', '등록자', '조회수'],
colModel:[
{ name : 'id' , align:'center', key:true, hidden:true},
{ name : 'rowNum' , align:'center', width:80 },
{ name : 'noticeSubject' , align:'left' , width:200 , formatter: formatFile },
{ name : 'rowNum' , align:'center', width:60 },
{ name : 'noticeType' , align:'center', width:60, formatter: formatNoticeType },
{ name : 'noticeSubject' , align:'left' , width:300, formatter: formatFile },
{ name : 'fixYn' , align:'center', width:60, formatter: formatFixYn },
{ name : 'useYn' , align:'center', width:60, formatter: formatuseYn },
{ name : 'lastModifiedDate', align:'center', width:120 , formatter: timeStampFormat},
{ name : 'createdDate' , align:'center', width:120, formatter: timeStampFormat },
@@ -62,6 +134,8 @@
autowidth: true,
viewrecords: true,
rowList : eval('[${rmsDefaultRowList}]'),
multiselect: true,
multiboxonly: true,
ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId);
var id = rowData['id'];
@@ -98,6 +172,8 @@
}
});
init();
resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){
@@ -114,6 +190,10 @@
goNav(url2);
});
$("#btn_delete_selected").click(function(){
deleteSelectedRows();
});
$("select[name=searchUseYn], input[name^=search]").keydown(function(key){
if (key.keyCode == 13){
$("#btn_search").click();
@@ -136,13 +216,30 @@
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> <%= localeMessage.getString("button.new") %></button>
<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_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
</div>
<div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div>
<form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing=0;>
<colgroup>
<col style="width:180px;">
<col style="width:240px;">
<col style="width:180px;">
<col style="width:240px;">
<col style="width:180px;">
<col style="width:240px;">
</colgroup>
<tbody>
<tr>
<th style="width:180px;">사용여부</th>
<th>게시유형</th>
<td>
<div class="select-style">
<select name="searchNoticeType"></select>
</div>
</td>
<th>제목/내용</th>
<td><input type="text" name="searchSubjectDetail"></td>
<th>사용여부</th>
<td>
<div class="select-style">
<select name="searchUseYn">
@@ -152,8 +249,6 @@
</select>
</div>
</td>
<th style="width:180px;">제목/내용</th>
<td><input type="text" name="searchSubjectDetail"></td>
</tr>
</tbody>
</table>
@@ -19,12 +19,70 @@
<script language="javascript" >
var url = '<c:url value="/onl/apim/portalnotice/portalNoticeMan.json" />';
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 isDetail = false;
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() {
// 초기화 로직
var key = "${param.id}";
isDetail = key != "" && key != "null";
if (isDetail) {
$("#title").append(" 수정");
buttonControl(true);
} else {
$("#title").append(" 등록");
$("#btn_modify").html('<i class="material-icons">save</i> <%= localeMessage.getString("button.register") %>');
buttonControl(false);
}
$.ajax({
type : "POST",
url:url,
dataType:"json",
data:{cmd: 'LIST_INIT_COMBO'},
success:function(json){
combo = json;
new makeOptions("CODE","NAME").setObj($("select[name=noticeType]")).setNoValueInclude(false).setData(json.noticeTypeList).rendering();
toggleIncidentFields();
if (key) {
detail(key);
}
},
error:function(e){
alert(e.responseText);
}
});
}
function toggleIncidentFields() {
var t = $('#noticeType').val();
if (isIncidentType(t)) {
$('.incident-row').show();
// INCIDENT 만 상태 영역 표시
if (t === NOTICE_TYPE_INCIDENT) {
$('.incident-state-row').show();
} else {
$('.incident-state-row').hide();
}
} else {
$('.incident-row').hide();
$('.incident-state-row').hide();
}
}
function decodeHTMLEntities(text) {
@@ -44,67 +102,123 @@
function displayAttachFile(file) {
var $attachFiles = $('#attachFiles');
$attachFiles.empty();
if (file) {
var iconPath = '${pageContext.request.contextPath}/images/icon_file.png';
var fileItem = $('<div>').addClass('file-item');
$('<img>').attr('src', iconPath).css({'marginLeft': '15px','marginRight': '5px' ,'height': '1.2em'}).appendTo(fileItem);
var fileNameSpan = $('<span>')
.addClass('fileName')
.text(file.originalFileName + '.' + file.fileExtension + ' (' + formatFileSize(file.size) + ')')
.css('cursor', 'pointer')
.on('click', function() {
if (file.fileId) {
downloadFile(file.fileId);
}
});
var fileNameSpan = $('<span>').addClass('fileName')
.text(file.originalFileName + '.' + file.fileExtension + ' (' + formatFileSize(file.size) + ')')
.css('cursor', 'pointer')
.on('click', function() { if (file.fileId) downloadFile(file.fileId); });
fileNameSpan.appendTo(fileItem);
$('<button>').text('X').addClass('delete-file').css('marginLeft', '15px')
.on('click', function() {
fileInfo = null;
displayAttachFile(null);
// 폼에 삭제 플래그 추가
$('<input>').attr({
type: 'hidden',
name: 'fileDeleted',
value: 'true'
}).appendTo('#ajaxForm');
})
.appendTo(fileItem);
.on('click', function() {
fileInfo = null;
displayAttachFile(null);
$('<input>').attr({type: 'hidden', name: 'fileDeleted', value: 'true'}).appendTo('#ajaxForm');
}).appendTo(fileItem);
fileItem.appendTo($attachFiles);
}
}
function downloadFile(fileId) {
if (fileId) {
var fileSn = 1; // 단일 파일이므로 시리얼 넘버를 1로 고정
window.location.href = file_download + '?fileId=' + fileId + '&fileSn=' + fileSn;
} else {
console.log("File not yet uploaded or doesn't have an ID");
window.location.href = file_download + '?fileId=' + fileId + '&fileSn=1';
}
}
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) {
if (!isDetail) return;
$.ajax({
type: "POST",
url: url,
dataType: "json",
type: "POST", url: url, dataType: "json",
data: {cmd: 'DETAIL', id: key},
success: function (data) {
$("#id").val(key);
$('#noticeType').val(data.noticeType);
$("#noticeSubject").val(data.noticeSubject);
$("#useYn").prop("checked", data.useYn === "Y");
$("#fixYn").prop("checked", data.fixYn === "Y");
var decodedContent = decodeHTMLEntities(data.noticeDetail);
$('#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) {
fileInfo = {
originalFileName: data.fileInfo.fileDetails[0].originalFileName,
@@ -115,37 +229,21 @@
displayAttachFile(fileInfo);
}
},
error: function (e) {
alert(e.responseText);
}
error: function (e) { alert(e.responseText); }
});
}
$(document).ready(function () {
var returnUrl = getReturnUrlForReturn();
var key = "${param.id}";
isDetail = key != "" && key != "null";
if (isDetail) {
$("#title").append(" 수정");
buttonControl(true);
} else {
$("#title").append(" 등록");
$("#btn_modify").html('<i class="material-icons">save</i> <%= localeMessage.getString("button.register") %>');
buttonControl(false);
}
init();
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
initSummernote('#contents', {
placeholder: '여기에 내용을 입력하세요.',
height: 300
});
initSummernote('#contents', { placeholder: '여기에 내용을 입력하세요.', height: 300 });
if (key) {
detail(key);
}
$('#noticeType').on('change', toggleIncidentFields);
$('#btn_addAffectedApi').click(openApiPopup);
$('#btn_removeAffectedApi').click(removeSelectedAffectedApis);
$('#fileInput').change(function(e) {
var file = e.target.files[0];
@@ -153,51 +251,66 @@
fileInfo = {
originalFileName: file.name.split('.').slice(0, -1).join('.'),
fileExtension: file.name.split('.').pop(),
file: file,
size: file.size
file: file, size: file.size
};
displayAttachFile(fileInfo);
}
this.value = ''; // 입력 필드 초기화
});
$('#addFile').click(function() {
$('#fileInput').click();
this.value = '';
});
$('#addFile').click(function() { $('#fileInput').click(); });
$("#btn_modify").click(function () {
if (!checkRequired("ajaxForm")) return;
if (confirm("<%= localeMessage.getString("common.checkSave")%>") != true)
return;
var noticeType = $('#noticeType').val();
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]);
formData.set("useYn", $("#useYn").is(":checked") ? "Y" : "N");
formData.set("fixYn", $("#fixYn").is(":checked") ? "Y" : "N");
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");
// 파일 처리
if (fileInfo && fileInfo.file) {
formData.set("files", fileInfo.file);
formData.set("fileName", fileInfo.originalFileName + '.' + fileInfo.fileExtension);
}
$.ajax({
type: "POST",
url: url,
data: formData,
processData: false,
contentType: false,
success: function (json) {
type: "POST", url: url, data: formData,
processData: false, contentType: false,
success: function () {
alert("<%= localeMessage.getString("common.saveMsg") %>");
goNav(returnUrl);
},
error: function (e) {
alert(e.responseText);
}
error: function (e) { alert(e.responseText); }
});
});
@@ -205,25 +318,18 @@
if (confirm("<%= localeMessage.getString("common.confirmMsg")%>")) {
var postData = $('#ajaxForm').serializeArray();
postData.push({name: "cmd", value: "DELETE"});
$.ajax({
type: "POST",
url: url,
data: postData,
success: function (args) {
type: "POST", url: url, data: postData,
success: function () {
alert("<%= localeMessage.getString("common.deleteMsg") %>");
goNav(returnUrl);
},
error: function (e) {
alert(e.responseText);
}
error: function (e) { alert(e.responseText); }
});
}
});
$("#btn_previous").click(function () {
goNav(returnUrl);
});
$("#btn_previous").click(function () { goNav(returnUrl); });
});
</script>
</head>
@@ -233,7 +339,7 @@
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div><!-- end content_top -->
</div>
<div class="content_middle">
<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>
@@ -244,29 +350,87 @@
<form id="ajaxForm" enctype="multipart/form-data" accept-charset="UTF-8">
<input type="hidden" name="id" id="id">
<table class="table_row" cellspacing="0">
<colgroup>
<col style="width: 10%"/>
<col style="width: 40%"/>
<col style="width: 10%"/>
<col style="width: 40%"/>
</colgroup>
<tr>
<th style="width:20%;">사용여부</th>
<td>
<input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용</td>
</tr>
<tr>
<th style="width:20%;">제목 <font color="red">*</font></th>
<td><input type="text" id="noticeSubject" name="noticeSubject" style="width:100%" data-required data-warning="제목을 입력하여 주십시오."/></td>
</tr>
<tr>
<th style="width:20%;">본문 <font color="red">*</font></th>
<td>
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
<th>게시유형 <font color="red">*</font></th>
<td colspan="3">
<div class="select-style">
<select name="noticeType" id="noticeType"></select>
</div>
</td>
</tr>
<tr>
<th style="width:20%;">첨부파일</th>
<th>제목 <font color="red">*</font></th>
<td colspan="3"><input type="text" id="noticeSubject" name="noticeSubject" style="width:100%" data-required data-warning="제목을 입력하여 주십시오."/></td>
</tr>
<tr>
<th>사용여부</th>
<td>
<div style="margin: 5px 0px; display: inline-block;">
<input type="file" id="fileInput" name="files" style="display:none;" />
<button type="button" id="addFile" class="cssbtn smallBtn">파일 선택</button>
<input type="checkbox" name="useYn" id="useYn" value="Y" ${portalNoticeUI.useYn eq 'Y' ? 'checked' : ''}> 사용
</td>
<th>상단고정</th>
<td>
<input type="checkbox" name="fixYn" id="fixYn" value="Y" ${portalNoticeUI.fixYn eq 'Y' ? 'checked' : ''}> 고정
</td>
</tr>
<tr>
<th>본문 <font color="red">*</font></th>
<td colspan="3">
<textarea id="contents" name="noticeDetail" style="width:100%;height:300px" data-required data-warning="본문을 입력하여 주십시오."></textarea>
</td>
</tr>
<!-- ───── 장애/점검 전용 영역 ───── -->
<tr class="incident-row" style="display:none;">
<th>시작 <font color="red">*</font></th>
<td>
<input type="datetime-local" id="startedAt" name="startedAt" style="width:90%">
</td>
<th>종료</th>
<td>
<input type="datetime-local" id="endAt" name="endAt" style="width:90%" placeholder="해소시 자동 채움">
</td>
</tr>
<tr class="incident-row incident-state-row" style="display:none;">
<th>상태</th>
<td>
<div class="select-style">
<select id="state" name="state">
<option value="INVESTIGATING">INVESTIGATING - 조사중</option>
<option value="IDENTIFIED">IDENTIFIED - 원인 파악</option>
<option value="MONITORING">MONITORING - 모니터링</option>
<option value="RESOLVED">RESOLVED - 해소</option>
<option value="CANCELED">CANCELED - 취소</option>
</select>
</div>
<div 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>
</tr>
</table>
@@ -274,4 +438,4 @@
</div>
</div>
</body>
</html>
</html>
@@ -82,6 +82,54 @@
</style>
<jsp:include page="/jsp/common/include/script.jsp"/>
<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() {
var targetDate = $("#targetDate").val().replace(/-/g, "");
var resultDiv = $("#dailyResult");
@@ -241,6 +289,7 @@
var year = yesterday.getFullYear();
var month = String(yesterday.getMonth() + 1).padStart(2, '0');
var day = String(yesterday.getDate()).padStart(2, '0');
$("#targetHour").val(year + month + day);
$("#targetDate").val(year + month + day);
// 전월
@@ -255,6 +304,12 @@
$("#targetYear").val(lastYear);
// 버튼 이벤트
$("#btn_execute_hourly").click(function() {
if (confirm("시간별 통계 집계를 실행하시겠습니까?")) {
executeAggregationHourly();
}
});
$("#btn_execute_daily").click(function() {
if (confirm("시간별→일별 통계 집계를 실행하시겠습니까?")) {
executeHourlyToDaily();
@@ -297,9 +352,26 @@
</ul>
</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">
<h3>1. 시간별 → 일별 통계 집계</h3>
<h3>2. 시간별 → 일별 통계 집계</h3>
<div class="aggregation-form">
<label>대상 날짜:</label>
<input type="text" id="targetDate" placeholder="yyyyMMdd" maxlength="8">
@@ -316,7 +388,7 @@
<!-- 일별 → 월별 집계 -->
<div class="aggregation-section">
<h3>2. 일별 → 월별 통계 집계</h3>
<h3>3. 일별 → 월별 통계 집계</h3>
<div class="aggregation-form">
<label>대상 월:</label>
<input type="text" id="targetMonth" placeholder="yyyyMM" maxlength="6">
@@ -333,7 +405,7 @@
<!-- 월별 → 연별 집계 -->
<div class="aggregation-section">
<h3>3. 월별 → 연별 통계 집계</h3>
<h3>4. 월별 → 연별 통계 집계</h3>
<div class="aggregation-form">
<label>대상 연도:</label>
<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_view = '<c:url value="/onl/kjb/statistics/apiStatsDayMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart;
var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
@@ -54,9 +54,7 @@
function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트
var totalDonutOption = {
@@ -73,7 +71,7 @@
legend: {
orient: 'horizontal',
bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류']
data: ['성공', 'Timeout', '시스템오류']
},
series: [{
name: '총건수',
@@ -98,8 +96,7 @@
data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
]
}],
graphic: [{
@@ -118,64 +115,7 @@
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 = {
@@ -191,32 +131,13 @@
series: [
{ 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: '시스템오류', 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: [] }
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
]
};
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) {
@@ -268,8 +189,7 @@
data: [
{ value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' },
{ value: totalBizErr, name: '업무오류' }
{ value: totalSystemErr, name: '시스템오류' }
]
}],
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({
@@ -302,24 +206,13 @@
series: [
{ data: successData },
{ data: timeoutData },
{ data: systemErrData },
{ data: bizErrData }
{ data: systemErrData }
]
});
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(6, 8) + '일'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data;
respChart.rawData = data;
}
function fetchChartData() {
@@ -535,8 +428,7 @@
postData: gridPostData,
colNames: [
'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
@@ -545,10 +437,6 @@
{ name: 'successCnt', 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: '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: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -580,9 +468,8 @@
colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
'총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -596,15 +483,9 @@
{ name: 'successCnt', 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: '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: 'minRespTime', 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 }
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
@@ -685,9 +566,7 @@
// 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize();
if (respChart) respChart.resize();
});
buttonControl();
@@ -712,7 +591,7 @@
<tbody>
<tr>
<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="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;">
@@ -724,22 +603,20 @@
<td>
<input type="text" name="searchApiName" value="${param.searchApiName}">
</td>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</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}">
@@ -754,12 +631,7 @@
<!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container">
<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="respChart" class="chart"></div>
</div>
<!-- 요약 그리드 -->
@@ -41,7 +41,7 @@
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsHourMan.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) {
if (cellvalue == null || cellvalue == '') return '0';
@@ -55,9 +55,7 @@
function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트
var totalDonutOption = {
@@ -74,7 +72,7 @@
legend: {
orient: 'horizontal',
bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류']
data: ['성공', 'Timeout', '시스템오류']
},
series: [{
name: '총건수',
@@ -99,8 +97,7 @@
data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
]
}],
graphic: [{
@@ -119,64 +116,7 @@
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 = {
@@ -185,47 +125,24 @@
trigger: 'axis',
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 },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 },
series: [
{ 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: '시스템오류', 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: [] }
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
]
};
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) {
drillDownToMinute(callChart, params.dataIndex);
});
respChart.on('click', function(params) {
drillDownToMinute(respChart, params.dataIndex);
});
}
function drillDownToMinute(chart, dataIndex) {
@@ -308,8 +225,7 @@
data: [
{ value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' },
{ value: totalBizErr, name: '업무오류' }
{ value: totalSystemErr, name: '시스템오류' }
]
}],
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({
@@ -342,24 +242,13 @@
series: [
{ data: successData },
{ data: timeoutData },
{ data: systemErrData },
{ data: bizErrData }
{ data: systemErrData }
]
});
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times.map(function(t) { return t.substring(8, 10) + '시'; }) },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
// 드릴다운을 위해 원본 데이터 저장
callChart.rawData = data;
respChart.rawData = data;
}
function fetchChartData() {
@@ -541,8 +430,7 @@
postData: gridPostData,
colNames: [
'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
@@ -551,10 +439,6 @@
{ name: 'successCnt', 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: '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: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -586,9 +470,8 @@
colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
'총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -602,15 +485,9 @@
{ name: 'successCnt', 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: '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: 'minRespTime', 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 }
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
@@ -669,9 +546,7 @@
// 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize();
if (respChart) respChart.resize();
});
buttonControl();
@@ -736,12 +611,7 @@
<!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container">
<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="respChart" class="chart"></div>
</div>
<!-- 요약 그리드 -->
@@ -752,7 +622,7 @@
<i class="material-icons">file_download</i> Excel 다운로드 (요약)
</button>
</div>
<table id="gridSummary"></table>
<table id="gridSummary"></table>
<div id="pagerSummary"></div>
</div>
@@ -10,7 +10,7 @@
%>
<html>
<head>
<title>분별 통계 조회</title>
<title>대시보드</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<style>
@@ -33,7 +33,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMinuteMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart;
var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
@@ -47,9 +47,7 @@
function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트
var totalDonutOption = {
@@ -66,7 +64,7 @@
legend: {
orient: 'horizontal',
bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류']
data: ['성공', 'Timeout', '시스템오류']
},
series: [{
name: '총건수',
@@ -91,8 +89,7 @@
data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
]
}],
graphic: [{
@@ -111,64 +108,7 @@
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 = {
@@ -177,7 +117,7 @@
trigger: 'axis',
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 },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 },
@@ -188,34 +128,12 @@
series: [
{ 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: '시스템오류', 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 }
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, 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);
respChart.setOption(respOption);
}
function updateCharts(data) {
@@ -269,8 +187,7 @@
data: [
{ value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' },
{ value: totalBizErr, name: '업무오류' }
{ value: totalSystemErr, name: '시스템오류' }
]
}],
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({
@@ -303,20 +204,10 @@
series: [
{ data: successData },
{ data: timeoutData },
{ data: systemErrData },
{ data: bizErrData }
{ data: systemErrData }
]
});
// 응답시간 차트 업데이트
respChart.setOption({
xAxis: { data: times },
series: [
{ data: p95RespData },
{ data: p50RespData },
{ data: avgRespData }
]
});
}
// 조회 기간 검증 및 조정 (최대 1시간)
@@ -552,8 +443,7 @@
postData: gridPostData,
colNames: [
'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
@@ -562,10 +452,6 @@
{ name: 'successCnt', 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: '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: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -597,9 +483,8 @@
colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
'총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -613,15 +498,9 @@
{ name: 'successCnt', 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: '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: 'minRespTime', 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 }
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
@@ -711,9 +590,7 @@
// 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize();
if (respChart) respChart.resize();
});
buttonControl();
@@ -733,7 +610,7 @@
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button>
</div>
<div class="title" id="title">API 분단위 통계</div>
<div class="title" id="title">대시보드</div>
<table class="search_condition" cellspacing="0">
<tbody>
<tr>
@@ -747,72 +624,28 @@
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 1시간, 초과시 자동 조정)</span>
</td>
</tr>
<tr>
<th style="width:100px;">API명</th>
<td>
<input type="text" name="searchApiName" value="${param.searchApiName}">
</td>
<th style="width:100px;">인스턴스</th>
<td>
<input type="text" name="searchGwInstanceId" value="${param.searchGwInstanceId}">
</td>
</tr>
<tr>
<th style="width:100px;">업무구분</th>
<td>
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
</td>
<th style="width:100px;">클라이언트ID</th>
<td>
<input type="text" name="searchClientId" value="${param.searchClientId}">
</td>
</tr>
<tr>
<th style="width:100px;">Inbound Adapter</th>
<td>
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
</td>
<th style="width:100px;">Outbound Adapter</th>
<td>
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
</td>
</tr>
</tbody>
</tbody>
</table>
<!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container">
<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="respChart" class="chart"></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;">API요약 통계</div>
<div class="title" style="margin: 0;">API별 사용현황</div>
<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>
</div>
</div>
<table id="gridSummary"></table>
<div id="pagerSummary"></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>
</body>
@@ -40,7 +40,7 @@
var url = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiStatsMonthMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart;
var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
@@ -54,9 +54,7 @@
function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트
var totalDonutOption = {
@@ -73,7 +71,7 @@
legend: {
orient: 'horizontal',
bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류']
data: ['성공', 'Timeout', '시스템오류']
},
series: [{
name: '총건수',
@@ -98,8 +96,7 @@
data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
]
}],
graphic: [{
@@ -118,64 +115,7 @@
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 = {
@@ -184,39 +124,18 @@
trigger: 'axis',
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 },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 },
series: [
{ 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: '시스템오류', 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: [] }
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
]
};
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) {
@@ -268,8 +187,7 @@
data: [
{ value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' },
{ value: totalBizErr, name: '업무오류' }
{ value: totalSystemErr, name: '시스템오류' }
]
}],
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({
@@ -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;
respChart.rawData = data;
}
function fetchChartData() {
@@ -539,8 +433,7 @@
postData: gridPostData,
colNames: [
'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
@@ -549,10 +442,6 @@
{ name: 'successCnt', 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: '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: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -584,9 +473,8 @@
colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
'총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -600,15 +488,9 @@
{ name: 'successCnt', 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: '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: 'minRespTime', 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 }
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
@@ -675,9 +557,7 @@
// 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize();
if (respChart) respChart.resize();
});
buttonControl();
@@ -744,14 +624,10 @@
<!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container">
<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="respChart" class="chart"></div>
</div>
<!-- 요약 그리드 -->
<div style="margin-top: 20px;">
<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_view = '<c:url value="/onl/kjb/statistics/apiStatsYearMan.view"/>';
var totalDonutChart, seq900DonutChart, callChart, respChart;
var totalDonutChart, callChart;
function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
@@ -54,9 +54,7 @@
function initCharts() {
totalDonutChart = echarts.init(document.getElementById('totalDonutChart'));
seq900DonutChart = echarts.init(document.getElementById('seq900DonutChart'));
callChart = echarts.init(document.getElementById('callChart'));
respChart = echarts.init(document.getElementById('respChart'));
// 총건수 도넛 차트
var totalDonutOption = {
@@ -73,7 +71,7 @@
legend: {
orient: 'horizontal',
bottom: 10,
data: ['성공', 'Timeout', '시스템오류', '업무오류']
data: ['성공', 'Timeout', '시스템오류']
},
series: [{
name: '총건수',
@@ -98,8 +96,7 @@
data: [
{ value: 0, name: '성공', itemStyle: { color: '#91CC75' } },
{ value: 0, name: 'Timeout', itemStyle: { color: '#FAC858' } },
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } },
{ value: 0, name: '업무오류', itemStyle: { color: '#FC8452' } }
{ value: 0, name: '시스템오류', itemStyle: { color: '#EE6666' } }
]
}],
graphic: [{
@@ -118,64 +115,7 @@
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 = {
@@ -184,39 +124,20 @@
trigger: 'axis',
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 },
xAxis: { type: 'category', boundaryGap: false, data: [] },
yAxis: { type: 'value', minInterval: 1 },
series: [
{ 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: '시스템오류', 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: [] }
{ name: '시스템오류', type: 'line', stack: 'Total', smooth: true, areaStyle: { opacity: 0.5 }, itemStyle: { color: '#EE6666' }, data: [] }
]
};
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) {
@@ -268,8 +189,7 @@
data: [
{ value: totalSuccess, name: '성공' },
{ value: totalTimeout, name: 'Timeout' },
{ value: totalSystemErr, name: '시스템오류' },
{ value: totalBizErr, name: '업무오류' }
{ value: totalSystemErr, name: '시스템오류' }
]
}],
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({
@@ -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;
respChart.rawData = data;
}
function fetchChartData() {
@@ -513,8 +409,7 @@
postData: gridPostData,
colNames: [
'API명',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
@@ -523,10 +418,6 @@
{ name: 'successCnt', 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: '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: 'minRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: decimalFormatter, sortable: false }
@@ -558,9 +449,8 @@
colNames: [
'통계시간', 'API명', '인스턴스', '업무구분', '클라이언트ID',
'Inbound Adapter', 'Outbound Adapter',
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)', 'P50(ms)', 'P95(ms)'
'총건수', '성공', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
{ name: 'statTime', align: 'center', width: '120', sortable: false },
@@ -574,15 +464,9 @@
{ name: 'successCnt', 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: '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: 'minRespTime', 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 }
{ name: 'maxRespTime', align: 'right', width: '80', formatter: decimalFormatter, sortable: false }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
@@ -644,9 +528,7 @@
// 윈도우 리사이즈 시 차트 리사이즈
$(window).resize(function() {
if (totalDonutChart) totalDonutChart.resize();
if (seq900DonutChart) seq900DonutChart.resize();
if (callChart) callChart.resize();
if (respChart) respChart.resize();
});
buttonControl();
@@ -712,12 +594,7 @@
<!-- 도넛 차트 (상단 50%씩) -->
<div class="chart-container">
<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="respChart" class="chart"></div>
</div>
<!-- 요약 그리드 -->
@@ -0,0 +1,313 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.io.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
request.setCharacterEncoding("UTF-8");
%>
<html>
<head>
<title>대시보드</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<script src="<c:url value="/addon/echarts/echarts.min.js"/>"></script>
<script language="javascript">
var url = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.json"/>';
var url_view = '<c:url value="/onl/kjb/statistics/apiUseStatsMan.view"/>';
function numberFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
return Number(cellvalue).toLocaleString();
}
function decimalFormatter(cellvalue, options, rowObject) {
if (cellvalue == null || cellvalue == '') return '0';
return (cellvalue).toFixed(2);
}
function getPostData() {
var postData = {}
if (arguments.length == 2) {
postData[arguments[0]] = arguments[1];
}
var searchStartDate = $("input[name=searchStartDate]").val().replace(/-/g, "");
var searchEndDate = $("input[name=searchEndDate]").val().replace(/-/g, "");
if (searchStartDate && searchEndDate) {
var start = new Date(searchStartDate.substring(0, 4), parseInt(searchStartDate.substring(4, 6)) - 1, searchStartDate.substring(6, 8));
var end = new Date(searchEndDate.substring(0, 4), parseInt(searchEndDate.substring(4, 6)) - 1, searchEndDate.substring(6, 8));
var diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
if (diffDays > 31) {
alert('조회 기간은 최대 31일까지 가능합니다.');
return null;
}
}
postData.searchType = $('input[name="searchType"]:checked').val();
postData.searchOrgName = $('#searchOrgName').val();
postData.searchApiName = $('#searchApiName').val();
postData.searchStartDateTime = searchStartDate;
postData.searchEndDateTime = searchEndDate;
return postData;
}
function search() {
var postData = getPostData("cmd", "LIST");
if (postData) {
$("#grid").setGridParam({ url: url, postData: postData, page: 1 }).trigger("reloadGrid");
}
}
function exportToExcel() {
var postData = getPostData("cmd", "EXCEL_EXPORT");
if (!postData) {
return;
}
postData.serviceType = '${param.serviceType}';
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onload = function() {
console.log('[Excel Export] Response received - Status:', xhr.status);
if (xhr.status === 200) {
var blob = xhr.response;
var contentType = xhr.getResponseHeader('Content-Type');
console.log('[Excel Export] Blob size:', blob.size, 'Content-Type:', contentType);
// JSON 에러 응답인지 확인
if (contentType && contentType.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function() {
try {
var errorObj = JSON.parse(reader.result);
alert(errorObj.message || 'Excel 파일 생성에 실패했습니다.');
} catch (e) {
alert('Excel 파일 생성에 실패했습니다.');
}
};
reader.readAsText(blob);
return;
}
// Blob 크기가 0이면 에러
if (blob.size === 0) {
alert('Excel 파일 생성에 실패했습니다. (빈 응답)');
return;
}
// 파일명 추출
var filename = 'api_stats.xlsx';
var disposition = xhr.getResponseHeader('Content-Disposition');
console.log('[Excel Export] Content-Disposition:', disposition);
if (disposition && disposition.indexOf('filename=') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
filename = decodeURIComponent(filename);
}
}
console.log('[Excel Export] Downloading as:', filename);
// 파일 다운로드
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
console.log('[Excel Export] Download triggered');
} else if (xhr.status === 204) {
alert('조회된 데이터가 없습니다.');
} else {
// 에러 응답 처리
var reader = new FileReader();
reader.onload = function() {
var message = 'Excel 다운로드 중 오류가 발생했습니다.';
console.log('reader.result', reader.result)
try {
var errorObj = JSON.parse(reader.result);
if (errorObj.message) message = errorObj.message;
} catch (e) {
console.error('[Excel Export] Error parsing error response:', e);
}
alert(message);
};
reader.readAsText(xhr.response);
}
};
xhr.onerror = function() {
console.error('[Excel Export] Network error');
alert('네트워크 오류가 발생했습니다.');
};
// Form data 생성
var formData = [];
for (var key in postData) {
if (postData.hasOwnProperty(key) && postData[key] != null && postData[key] !== '') {
formData.push(encodeURIComponent(key) + '=' + encodeURIComponent(postData[key]));
}
}
xhr.send(formData.join('&'));
}
function list() {
var gridPostData = getPostData("cmd", "LIST");
$('#grid').jqGrid({
datatype: "json",
mtype: 'POST',
postData: gridPostData,
colNames: [
'구분',
'총건수', '성공', '성공율(%)', '실패율(%)', 'Timeout', '시스템오류',
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
],
colModel: [
{ name: 'orgName', align: 'left', width: '250', sortable: false },
{ name: 'totalCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'successCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'successRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'failRate', align: 'right', width: '80', formatter: decimalFormatter, sortable: false },
{ name: 'timeoutCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'systemErrCnt', align: 'right', width: '80', formatter: numberFormatter, sortable: false },
{ name: 'avgRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false },
{ name: 'minRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false },
{ name: 'maxRespTime', align: 'right', width: '90', formatter: numberFormatter, sortable: false }
],
jsonReader: { repeatitems: false },
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: 'auto',
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
loadComplete: function(d) {
var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, { sortable: false });
}
}
});
}
$(document).ready(function() {
// 날짜/시간 입력 마스크
$("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true });
$("input[name=searchStartDate], input[name=searchEndDate]").datepicker();
var today = getToday();
var startDate = today;
var endDate = today;
if (!$("input[name=searchStartDate]").val()) {
$("input[name=searchStartDate]").val(startDate);
}
if (!$("input[name=searchEndDate]").val()) {
$("input[name=searchEndDate]").val(endDate);
}
list();
resizeJqGridWidth('grid', 'content_middle', '1200');
$("#btn_search").click(function() {
search();
});
$("#btn_excel").click(function() {
exportToExcel();
});
$("input[name^=search]").keydown(function(key) {
if (key.keyCode == 13) {
$("#btn_search").click();
}
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path">
<li><a href="#">${rmsMenuPath}</a></li>
</ul>
</div>
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_excel" level="R" style="display: inline-block;"><i class="material-icons">table_view</i> 엑셀</button>
<button type="button" class="cssbtn" id="btn_search" level="R" style="display: inline-block;">
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
</button>
</div>
<div class="title" id="title">API 사용현황</div>
<table class="search_condition" cellspacing="0">
<tbody>
<tr>
<th style="width:120px;">조회기간</th>
<td colspan="5">
<input type="text" name="searchStartDate" id="searchStartDate" value="${param.searchStartDate}" style="width:100px;">
~
<input type="text" name="searchEndDate" id="searchEndDate" value="${param.searchEndDate}" style="width:100px;">
<span style="color:#888; font-size:12px; margin-left:10px;">(최대 31일)</span>
</td>
</tr>
<tr>
<th style="width:120px;">조회구분</th>
<td>
<input type="radio" name="searchType" id="searchTypeORG" value="ORG" checked="checked"><label for="searchTypeORG">제휴사</label></input>
<input type="radio" name="searchType" id="searchTypeAPI" value="API"><label for="searchTypeAPI">API</label></input>
<input type="radio" name="searchType" id="searchTypeDATE" value="DATE"><label for="searchTypeDATE">사용일</label></input>
</td>
<th style="width:120px;">제휴사명</th>
<td>
<input type="text" name="searchOrgName" id="searchOrgName" value="${param.searchOrgName}">
</td>
<th style="width:120px;">API명</th>
<td>
<input type="text" name="searchApiName" id="searchApiName" value="${param.searchApiName}">
</td>
</tr>
</tbody>
</table>
<div>
<table id="grid"></table>
<div id="pager"></div>
</div>
</div>
</div>
</body>
</html>
@@ -411,7 +411,7 @@
function detail(url,key){
jsonUrl = url;
if (!isDetail)return;
$("input[name='btnRadioSyncAsync']").attr('disabled', true);
// $("input[name='btnRadioSyncAsync']").attr('disabled', true);
$.ajax({
type : "POST",
url:url,
@@ -962,11 +962,13 @@
headerNames = messageKeyList;
isHeaderRouting = true;
initHeaders(headerNames);
$('#slideInboundRoutingRule').carousel(0);
//$('#slideInboundRoutingRule').carousel(0);
$('#collapseInboundRoutingInfo').collapse('show')
setHeaderRoutingLabel();
}else{
isHeaderRouting = false;
$('#slideInboundRoutingRule').carousel(1);
$('#collapseInboundRoutingInfo').collapse('hide')
//$('#slideInboundRoutingRule').carousel(1);
}
}
@@ -1572,7 +1574,7 @@
<fieldset class="groupbox-border">
<legend class="groupbox-border">INTERFACE TYPE</legend>
<div class="row">
<div class="form-group col-md-3">
<!-- <div class="form-group col-md-3">
<label for="eaiBzwkDstcd"><span class="material-icons-outlined">sync</span> Sync/Async 타입</label><br>
<div class="btn-group" id="btnGroupSyncAsync" role="group">
<input type="radio"
@@ -1615,19 +1617,19 @@
<i class="bi bi-share-fill"></i> S > A
</label>
</div>
</div>
</div> -->
<div class="form-group col-md-3">
<label for="eaiBzwkDstcd">요청/응답 구분</label><br>
<div class="btn-group" role="group" aria-label="Basic radio toggle button group"
x-effect="if (apiInterface.syncAsyncType !== 'async') apiInterface.requestType = 'S'">
<input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeRequest"
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>
<input type="radio" class="btn-check" name="btnRadioReqRes" id="btnTypeResponse"
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>
</div>
</div>
@@ -1684,7 +1686,7 @@
<option value="none" selected>NONE</option>
<option value="oauth">OAUTH</option>
<option value="api_key">API_KEY</option>
<option value="ca">CA</option>
<!-- <option value="ca">CA</option> -->
</select>
</div>
</div>
@@ -1771,36 +1773,34 @@
</select>
</div>
<div class="form-group col-md-8">
<div id="slideInboundRoutingRule" class="carousel slide" data-ride="carousel" data-interval="false">
<div class="carousel-inner" style="margin-bottom:-20px">
<div class="carousel-item">
<label>
<span class="material-icons justify-content-md-start">http</span>
수신 라우팅 정보
<div id="slideInboundRoutingRule">
<div class="row">
<div class="col-md-12">
<label for="inboundRestPath">
<span class="material-icons">link</span>
수신 REST PATH(URL)
</label>
<div class="d-grid gap-2">
<button type="button" id="headerRoutingButton" class="btn btn-primary"
data-bs-toggle="modal" data-bs-target="#routingModal">
수정
</button>
</div>
</div>
<div class="carousel-item active">
<div class="row">
<div class="col-md-12">
<label for="inboundRestPath">
<span class="material-icons">link</span>
수신 REST PATH(URL)
</label>
<input type="text" class="form-control" id="inboundRestPath"
name="inboundRestPath" disabled>
</div>
</div>
<input type="text" class="form-control" id="inboundRestPath"
name="inboundRestPath" disabled>
</div>
</div>
</div>
</div>
</div>
<div class="collapse mt-2" id="collapseInboundRoutingInfo">
<div class="form-group col-md-12">
<label>
<span class="material-icons justify-content-md-start">http</span>
수신 라우팅 정보
</label>
<div class="d-grid gap-2">
<button type="button" id="headerRoutingButton" class="btn btn-primary"
data-bs-toggle="modal" data-bs-target="#routingModal">
수정
</button>
</div>
</div>
</div>
<div id="inboundResponseMethodPathRow"
x-show="showInboundResponse">
<div id="inboundResponseRestPart" class="row">
+10 -6
View File
@@ -201,14 +201,13 @@ dependencies {
}
implementation 'software.amazon.awssdk:sso: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'
// 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'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
@@ -228,6 +227,11 @@ configurations.all {
cacheDynamicVersionsFor 10, 'minutes'
// Do not cache changing modules
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),
SEQ NUMBER(22),
@@ -9,23 +9,23 @@ CREATE TABLE EMSADM.API_HISTORY
LOB (SAVEDDATA) STORE AS SECUREFILE (TABLESPACE TS_AGW_D01)
TABLESPACE TS_EMS_D01;
COMMENT ON TABLE EMSADM.API_HISTORY IS 'API 변경이력 테이블';
COMMENT ON COLUMN EMSADM.API_HISTORY.API_ID IS 'API ID(TSEAIHE01.EAISVCNAME)';
COMMENT ON COLUMN EMSADM.API_HISTORY.SEQ IS 'Sequence 값';
COMMENT ON COLUMN EMSADM.API_HISTORY.SAVEDDATA IS '변경저장 정보';
COMMENT ON COLUMN EMSADM.API_HISTORY.USERID IS '사용자ID';
COMMENT ON COLUMN EMSADM.API_HISTORY.SAVEDDT IS '저장일시';
COMMENT ON TABLE EMSAPP.API_HISTORY IS 'API 변경이력 테이블';
COMMENT ON COLUMN EMSAPP.API_HISTORY.API_ID IS 'API ID(TSEAIHE01.EAISVCNAME)';
COMMENT ON COLUMN EMSAPP.API_HISTORY.SEQ IS 'Sequence 값';
COMMENT ON COLUMN EMSAPP.API_HISTORY.SAVEDDATA IS '변경저장 정보';
COMMENT ON COLUMN EMSAPP.API_HISTORY.USERID IS '사용자ID';
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)
TABLESPACE TS_EMS_I01;
ALTER TABLE EMSADM.API_HISTORY ADD (
ALTER TABLE EMSAPP.API_HISTORY ADD (
CONSTRAINT PK_API_HISTORY
PRIMARY KEY
(API_ID, SEQ)
USING INDEX EMSADM.PK_API_HISTORY
USING INDEX EMSAPP.PK_API_HISTORY
ENABLE VALIDATE);
GRANT DELETE, INSERT, SELECT, UPDATE ON EMSADM.API_HISTORY TO RL_EMS_ALL;
GRANT SELECT ON EMSADM.API_HISTORY TO RL_EMS_SEL;
GRANT DELETE, INSERT, SELECT, UPDATE ON EMSAPP.API_HISTORY TO RL_EMS_ALL;
GRANT SELECT ON EMSAPP.API_HISTORY TO RL_EMS_SEL;
+30 -30
View File
@@ -39,37 +39,37 @@
```sql
-- 개발 서버용
insert into EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
insert into EMSAPP.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.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSAPP.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.backup_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 EMSAPP.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.interface_id', 'UIEFF00001UAG');
insert into EMSAPP.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.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
-- autogen template
insert into EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host', '');
insert into EMSADM.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 EMSADM.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.eai.batch.url', '');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSAPP.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.backup_dir', '');
insert into EMSAPP.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.interface_id', '');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.eai_batch.retention_date', '60');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host', '');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.api_key', '');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
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
-- SMS 인증 설정 초기화
INSERT INTO EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL)
INSERT INTO EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL)
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');
INSERT INTO EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL)
INSERT INTO EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL)
VALUES ('Monitoring', 'kjb.sms_auth.fixed_value', '654321');
```
@@ -187,7 +187,7 @@ SMS 발송을 위해 UMS 관련 프로퍼티가 설정되어 있어야 합니다
### Q: SMS 인증을 일시적으로 비활성화하려면?
```sql
UPDATE EMSADM.TSEAIRM24
UPDATE EMSAPP.TSEAIRM24
SET PRPTY2VAL = 'false'
WHERE PRPTYGROUPNAME = 'Monitoring' AND PRPTYNAME = 'kjb.sms_auth.enabled';
```
+82 -272
View File
@@ -9,7 +9,7 @@
## 기술 스택
- Spring Framework 5.3.27
- Java 8
- Java 17
- Gradle 8.7
- Oracle 19c
- Spring Data JPA 2.5.2
@@ -23,251 +23,61 @@
### 사전 요구사항
- JDK 8 이상
- JDK 17 이상
- Gradle 8.7
- Git
- 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.T.properties` - 테스트/스테이징 환경
- `WebContent/WEB-INF/properties/env.P.properties` - 운영 환경
### 주의사항
- **gradlew 사용 금지**: 반드시 시스템에 설치된 gradle을 직접 사용하거나 환경에 맞는 스크립트를 사용하세요
- **Gradle 명령어 변수화**: 위의 `GRADLE_CMD` 변수를 설정하면 환경별로 다른 gradle 명령어 사용 가능
### 주의사항
- **SafeDB**: 암호화 기능이 필요한 경우 `-Dkjb_safedb.mode=fake` 설정 (개발 환경) 또는 실제 SafeDB 라이브러리 설치
- **멀티 모듈 구조**: eapim-online의 여러 모듈들을 참조하므로 반드시 eapim-online도 클론해야 합니다
## 빌드 및 실행
### Gradle 명령어 설정
### Gradle 명령어
사용 환경에 따라 적절한 gradle 명령어를 설정하세요:
**Git Bash:**
```bash
# 일반 환경
export GRADLE_CMD="gradle"
# msi-gf63 환경
export GRADLE_CMD="/c/eactive/workspaces/shell-scripts/kjb-gradle.sh"
```
**Windows CMD:**
```cmd
REM 일반 환경
set GRADLE_CMD=gradle
REM msi-gf63 환경
set GRADLE_CMD=C:\eactive\workspaces\shell-scripts\kjb-gradle.sh
```
프로젝트 루트의 Gradle wrapper(`./gradlew`) 를 사용합니다.
### 빌드 및 실행 명령어
```bash
# 표준 빌드
$GRADLE_CMD build
./gradlew build
# Weblogic 배포용 빌드 (테스트 제외)
$GRADLE_CMD build -x test -Pprofile=weblogic
./gradlew build -x test -Pprofile=weblogic
# WAR 파일 빌드
$GRADLE_CMD war
# WAR 파일 빌드[ems_stdout.log](../../logs/emsSvr-RinjaeMA/ems_stdout.log)
./gradlew war
# 클린 빌드
$GRADLE_CMD clean build
./gradlew clean build
# 테스트 실행
$GRADLE_CMD test
./gradlew test
# 패키징 없이 클래스만 컴파일
$GRADLE_CMD classes
./gradlew 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
-Dinst.Name=emsSvr11
-Deai.tableowner=EMSADM
-Deai.tableowner=EMSAPP
-Deai.systemmode=D
-Dfile.encoding=utf-8
-DLOGBACK_LOG_LEVEL=info
@@ -438,7 +248,7 @@ WebContent/
```
-Deai.datasource.type=DEV
-Dinst.Name=emsSvr99
-Deai.tableowner=EMSADM
-Deai.tableowner=EMSAPP
-Deai.systemmode=D
-Dfile.encoding=utf-8
-Dlogin.mode=db
@@ -577,82 +387,82 @@ gradle test --tests "com.eactive.eai.rms.*"
```sql
-- 로컬 개발용 (외부)
-- EAI, UMS 주소 미기입시 내부적으론 호출 시도 하지 않고 warning 로그만 남기며 정상 처리
--insert into EMSADM.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 EMSADM.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 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.url', '');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
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 EMSADM.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 EMSADM.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.send_dir', '/Data/eapim/portal/sendmail/snd');
insert into EMSAPP.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.error_dir', '/Data/eapim/portal/sendmail/bak');
insert into EMSAPP.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.interface_id', 'UIEFF00001UAG');
-- insert into EMSADM.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 EMSADM.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 EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
-- insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.host_url', '');
insert into EMSAPP.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.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
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 EMSADM.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 EMSADM.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 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.host_url', '');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSAPP.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_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSAPP.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.create_partnercode_uri', '/not-yet-implement/%s');
commit;
-- 은행 개발 서버용
insert into EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
insert into EMSAPP.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.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSAPP.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.backup_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 EMSAPP.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.interface_id', 'UIEFF00001UAG');
insert into EMSAPP.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.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
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 EMSADM.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 EMSADM.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 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.host_url', 'http://192.168.246.15:8181');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSAPP.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_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSAPP.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.create_partnercode_uri', '/not-yet-implement/%s');
commit;
-- 은행 운영 서버용
insert into EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.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 EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.timeout', '10');
insert into EMSAPP.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.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.response_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.timeout', '10');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.eai.batch.charset', 'euc-kr');
insert into EMSAPP.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.backup_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 EMSAPP.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.interface_id', 'UIEFF00001UAG');
insert into EMSAPP.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.api_key', '7cca6d58-e4f7-474d-9edd-525806bc99ff');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.connection_timeout', '5');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.ums.response_timeout', '5');
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 EMSADM.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 EMSADM.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 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.host_url', 'http://192.168.246.15:8181');
insert into EMSAPP.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', 'kjb.obp.trust_system', 'APIMPT-0002-QVBJTVBULTAwMDI=');
insert into EMSAPP.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_print_uri', '/api/billing/billing/findBillingForCondition/print');
insert into EMSAPP.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.create_partnercode_uri', '/not-yet-implement/%s');
commit;
```
## 라이선스
@@ -251,6 +251,18 @@ public interface MonitoringContext {
public static final String MENU_RENDER_ADDITIONAL_SERVICES = "menu.render.additional.services";
public static final String KEYSTORE_UPLOAD_PATH = "keystore.upload.path";
// 이중 로그인 허용여부
public static final String RMS_DUAL_LOGIN_ENABLED = "rms.DUAL_LOGIN_ENABLED";
// 자동 로그아웃 타임아웃 (단위: , 기본값: 10)
public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout";
// 웹훅 재전송 설정
public static final String API_WEBHOOK_RETRY_COUNT = "api.webhook.retry_count";
public static final String API_WEBHOOK_RETRY_TIME = "api.webhook.retry_time";
public abstract String getStringProperty(String propertyName);
@@ -1,15 +1,18 @@
package com.eactive.eai.rms.common.interceptor;
import com.eactive.eai.rms.common.login.LoginVo;
import com.eactive.eai.rms.common.login.SessionManager;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.login.LoginVo;
import com.eactive.eai.rms.common.login.SessionManager;
public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
@@ -20,6 +23,8 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
private static final Logger logger = Logger
.getLogger(SessionCheckInterceptor.class);
@Autowired
private MonitoringContext monitoringContext;
@Override
public boolean preHandle(HttpServletRequest request,
@@ -39,6 +44,7 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
//craft
boolean isSkippable = ClassUtils.isAssignable(clazz, InterceptorSkipController.class);
boolean isDualLogin = monitoringContext.getBooleanProperty(MonitoringContext.RMS_DUAL_LOGIN_ENABLED, true);
if (isSkippable) {
valid = true;
@@ -46,7 +52,7 @@ public class SessionCheckInterceptor extends HandlerInterceptorAdapter {
SessionManager.setLoginVo(loginVo); // ThreadLocal에 설정
// 이중 로그인 검사
if (!SessionManager.isValidSession(request)) {
if (!SessionManager.isValidSession(request, isDualLogin)) {
logger.warn("Duplicate login detected for user: " + loginVo.getUserId());
valid = false;
} else {
@@ -391,20 +391,24 @@ public class MainController implements InterceptorSkipController {
session.setAttribute("dualLogin", "N");
// 이중 로그인 확인
boolean isNewLogin = SessionManager.registerUserSession(request, dto);
boolean isDualLogin = monitoringContext.getBooleanProperty(MonitoringContext.RMS_DUAL_LOGIN_ENABLED, true);
if (!isNewLogin) {
// 1.
SessionManager.forceRegisterUserSession(request, dto);
session.setAttribute("dualLogin", "Y");
// 또는
// 2. 이중 로그인 에러 표시
// model.addAttribute("error", "다른 기기에서 이미 로그인 중입니다.");
// return "/";
}
if (!isDualLogin) {
// 이중 로그인 확인
boolean isNewLogin = SessionManager.registerUserSession(request, dto);
if (!isNewLogin) {
// 1.
SessionManager.forceRegisterUserSession(request, dto);
session.setAttribute("dualLogin", "Y");
// 또는
// 2. 이중 로그인 에러 표시
// model.addAttribute("error", "다른 기기에서 이미 로그인 중입니다.");
// return "/";
}
}
// 세션에 로그인 정보 저장
request.getSession().setAttribute(CommonConstants.LOGIN, dto);
@@ -613,6 +617,8 @@ public class MainController implements InterceptorSkipController {
menuId = menuId == null ? "" : menuId;
model.addAttribute("mainPage", mainPage);
model.addAttribute("menuId", menuId);
model.addAttribute("autoLogoutTimeout",
monitoringContext.getIntProperty(MonitoringContext.RMS_AUTO_LOGOUT_TIMEOUT, 10));
if (logger.isDebugEnabled()) {
logger.debug("CURRENT SERVICE TYPE : " + service);
@@ -665,7 +671,8 @@ public class MainController implements InterceptorSkipController {
if (loginVo != null) {
// 로그인 세션 제거
SessionManager.removeUserSession(loginVo.getUserId());
//SessionManager.removeUserSession(loginVo.getUserId());
SessionManager.removeUserSessionBySessionId(request.getSession().getId()); //2026.04.29 이중로그인 로그아웃 버그 수정
request.getSession().removeAttribute(CommonConstants.LOGIN);
@@ -27,6 +27,7 @@ public class SessionDestructionListener implements HttpSessionListener {
HttpSession session = se.getSession();
String userId = (String) session.getAttribute("userId");
String sessionId = session.getId();
// 로그인 사용자의 세션정보 정리
if (userId != null) {
@@ -34,7 +35,8 @@ public class SessionDestructionListener implements HttpSessionListener {
logger.debug("[SessionListener] 세션만료 감지. 사용자 제거: " + userId);
}
SessionManager.removeUserSession(userId);
//SessionManager.removeUserSession(userId);
SessionManager.removeUserSessionBySessionId(sessionId); //2026.04.29 이중로그인 로그아웃 버그 수정
}
} catch (Exception e) {
@@ -4,6 +4,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.util.CommonConstants;
//craft
@@ -206,23 +207,38 @@ public class SessionManager {
}
}
/**
* 사용자의 로그인 세션을 제거 (이중로그인 버그 수정용)
* @param sessionId 로그인한 session ID
*/
public static void removeUserSessionBySessionId(String sessionId) {
loggedInUsers.entrySet().removeIf(entry -> sessionId.equals(entry.getValue()));
}
/**
* 현재 세션이 유효한 로그인 세션인지 확인
* @param request HTTP 요청
* @param isDualLogin 이중 로그인 허용여부
* @return true: 유효한 세션, false: 유효하지 않은 세션
*/
public static boolean isValidSession(HttpServletRequest request) {
public static boolean isValidSession(HttpServletRequest request, boolean isDualLogin) {
LoginVo loginVo = getLoginVo(request);
if (loginVo == null) {
return false;
}
String userId = loginVo.getUserId();
String currentSessionId = request.getSession().getId();
String registeredSessionId = loggedInUsers.get(userId);
// 등록된 세션이 없거나 현재 세션과 다른 경우
return registeredSessionId != null && registeredSessionId.equals(currentSessionId);
}
if (isDualLogin) {
return true;
} else {
String userId = loginVo.getUserId();
String currentSessionId = request.getSession().getId();
String registeredSessionId = loggedInUsers.get(userId);
// 등록된 세션이 없거나 현재 세션과 다른 경우
return registeredSessionId != null && registeredSessionId.equals(currentSessionId);
}
}
}
@@ -35,6 +35,7 @@ public class MonitoringPropertyManService extends BaseService {
public MonitoringPropertyGroupUI selectDetail(String prptyGroupName) {
MonitoringPropertyGroup propertyGroup = propertyGroupService.getById(prptyGroupName);
propertyGroup.getMonitoringProperties().sort(Comparator.comparing(p -> p.getId().getPrptyName()));
return propertyGroupMapper.toVo(propertyGroup);
}
@@ -15,4 +15,6 @@ public interface EMSScheduler {
void deleteAndAddJob(Scheduler scheduler, String jobName) throws SchedulerException, ClassNotFoundException;
void reloadJob(String jobName) throws SchedulerException, ClassNotFoundException;
}
@@ -120,6 +120,32 @@ public class EMSSchedulerImpl implements EMSScheduler {
}
}
@Override
public void reloadJob(String jobName) throws SchedulerException, ClassNotFoundException {
JobInfo jobInfo = jobInfoService.getById(jobName);
if (CLUSTERED.equals(jobInfo.getInstanceName())) {
reloadClusteredJob(jobInfo);
} else {
deleteAndAddJob(scheduler, jobInfo);
deleteJob(clusteredScheduler, jobName);
}
}
private void reloadClusteredJob(JobInfo jobInfo) throws SchedulerException, ClassNotFoundException {
TriggerKey triggerKey = new TriggerKey(getTriggerName(jobInfo.getId()), Scheduler.DEFAULT_GROUP);
Trigger existing = clusteredScheduler.getTrigger(triggerKey);
if (existing != null) {
CronTrigger newTrigger = newTrigger()
.withIdentity(triggerKey)
.withSchedule(cronSchedule(jobInfo.getCronExp()))
.build();
Date ft = clusteredScheduler.rescheduleJob(triggerKey, newTrigger);
log.info("{} rescheduled at: {} cron: {}", jobInfo.getId(), ft, jobInfo.getCronExp());
} else {
deleteAndAddJob(clusteredScheduler, jobInfo);
}
}
public void deleteJob(Scheduler inScheduler, String jobNm) throws SchedulerException {
JobKey key = new JobKey(jobNm, Scheduler.DEFAULT_GROUP);
JobDetail jobDetail = inScheduler.getJobDetail(key);
@@ -45,6 +45,9 @@ public class SchedulerManController extends BaseAnnotationController {
@Qualifier("monitoringSyncAgent")
private MonitoringSyncAgent monitoringSyncAgent;
@Autowired
private EMSScheduler emsScheduler;
@GetMapping(value = "/common/scheduler/schedulerMan.view")
public String viewList() {
return "/common/scheduler/schedulerMan";
@@ -96,6 +99,11 @@ public class SchedulerManController extends BaseAnnotationController {
@PostMapping(value = "/common/scheduler/schedulerMan.json", params = "cmd=UPDATE")
public ResponseEntity<Void> update(JobInfoUI jobInfoUI) {
service.update(jobInfoUI);
try {
emsScheduler.reloadJob(jobInfoUI.getId());
} catch (Exception e) {
logger.error("reloadJob error on current server", e);
}
syncReloadScheduler(jobInfoUI.getId());
return ResponseEntity.ok().build();
}
@@ -115,7 +123,7 @@ public class SchedulerManController extends BaseAnnotationController {
String url = "/monitoring/schedulerReload.do";
try {
ret = monitoringSyncAgent.sync(url, paramMap);
ret = monitoringSyncAgent.targetSync(url, paramMap);
} catch (Exception e) {
logger.error("syncReloadScheduler error", e);
} finally {
@@ -1,13 +1,11 @@
package com.eactive.eai.rms.common.scheduler;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper;
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;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apache.commons.lang3.StringUtils;
import org.quartz.JobDetail;
import org.quartz.JobKey;
@@ -20,9 +18,14 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.scheduler.ui.JobInfoMapper;
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")
@Transactional(transactionManager = "transactionManagerForEMS")
@@ -122,10 +125,10 @@ public class SchedulerManService extends BaseService {
List<? extends Trigger> triggersOfJob = scheduler.getTriggersOfJob(new JobKey(jobName, Scheduler.DEFAULT_GROUP));
if(triggersOfJob.size() > 0) {
result.setStartTime(String.valueOf(triggersOfJob.get(0).getStartTime()));
result.setEndTime(String.valueOf(triggersOfJob.get(0).getEndTime()));
result.setPreviousFireTime(String.valueOf(triggersOfJob.get(0).getPreviousFireTime()));
result.setNextFireTime(String.valueOf(triggersOfJob.get(0).getNextFireTime()));
result.setStartTime(dateFormat(triggersOfJob.get(0).getStartTime()));
result.setEndTime(dateFormat(triggersOfJob.get(0).getEndTime()));
result.setPreviousFireTime(dateFormat(triggersOfJob.get(0).getPreviousFireTime()));
result.setNextFireTime(dateFormat(triggersOfJob.get(0).getNextFireTime()));
}
} catch (Exception e) {
@@ -150,5 +153,14 @@ public class SchedulerManService extends BaseService {
public void delete(String 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;
import java.math.BigDecimal;
import java.util.List;
import org.apache.log4j.Logger;
@@ -191,6 +192,20 @@ public class StringUtils
}
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 변환한다.
@@ -107,6 +107,17 @@ public class UserInfoService extends
}).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) {
QUserInfo qUserInfo = QUserInfo.userInfo;
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.rms.data.EMSDataSource;
import java.util.List;
@EMSDataSource
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;
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.QInquiry;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
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
@Transactional(transactionManager = "transactionManagerForEMS")
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) {
QInquiry qInquiry = QInquiry.inquiry;
@@ -58,4 +66,8 @@ public class PortalInquiryService extends AbstractEMSDataSerivce<Inquiry, String
repository.deleteAll(inquiries);
}
}
public List<Inquiry> findByInquiryStatus(String inquiryStatus) {
return portalInquiryRepository.findByInquiryStatus(inquiryStatus);
}
}
@@ -10,15 +10,28 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import java.util.List;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, String, PortalNoticeRepository> {
public void deleteByIds(List<String> ids) {
repository.deleteAllById(ids);
}
public Page<PortalNotice> findAll(Pageable pageable, PortalNoticeUISearch portalNoticeUISearch) {
QPortalNotice qPortalNotice = QPortalNotice.portalNotice;
BooleanBuilder predicate = new BooleanBuilder();
if (StringUtils.isNotBlank(portalNoticeUISearch.getSearchNoticeType())) {
predicate.and(qPortalNotice.noticeType.eq(portalNoticeUISearch.getSearchNoticeType()));
}
if (StringUtils.isNotBlank(portalNoticeUISearch.getSearchUseYn())) {
predicate.and(qPortalNotice.useYn.eq(portalNoticeUISearch.getSearchUseYn()));
}
@@ -28,7 +41,12 @@ public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, St
.or(qPortalNotice.noticeDetail.containsIgnoreCase(portalNoticeUISearch.getSearchSubjectDetail())));
}
return repository.findAll(predicate, pageable);
//return repository.findAll(predicate, pageable);
Sort fixYnFirst = Sort.by(Sort.Direction.DESC, "fixYn");
Sort combinedSort = fixYnFirst.and(pageable.getSort());
Pageable fixedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), combinedSort);
return repository.findAll(predicate, fixedPageable);
}
}
@@ -5,6 +5,7 @@ import lombok.Data;
@Data
public class PortalNoticeUISearch {
private String searchNoticeType;
private String searchUseYn;
private String searchSubjectDetail;
@@ -0,0 +1,42 @@
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import com.eactive.eai.data.entity.AbstractEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
@Entity
@Table(name = "API_STATUS")
public class ApiStatus extends AbstractEntity<String> {
@Id
@Column(name = "EAISVCNAME", length = 100)
private String eaisvcname;
@Column(name = "STATUS_CODE", length = 1)
private String statusCode;
@LastModifiedBy // EMSAuditorAware가 자동으로 채움
@Column(name = "MODIFIED_BY", length = 50)
private String modifiedBy = "SCHEDULER";
@LastModifiedDate // save() 자동으로 sysdate 채움
@Column(name = "MODIFIED_DATE")
private LocalDateTime modifiedDate;
@Override
public @NonNull String getId() { return eaisvcname; }
}
@@ -0,0 +1,8 @@
package com.eactive.eai.rms.data.entity.onl.djb.apistatus;
public interface ApiStatusEvent {
String getEaisvcname();
String getEaisvcdesc();
String getPrevStatusCode();
String getEvent();
}
@@ -0,0 +1,7 @@
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
public interface InflowTokenInsufficient {
String getEaisvcname();
String getEaisvcdesc();
Long getCnt();
}
@@ -0,0 +1,47 @@
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import com.eactive.eai.data.entity.AbstractEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
@Entity
@Table(name = "TSEAIFR11")
public class InflowTokenInsufficientLog extends AbstractEntity<InflowTokenInsufficientLogId> implements Serializable {
private static final long serialVersionUID = 1L;
@EmbeddedId
@EqualsAndHashCode.Include
private InflowTokenInsufficientLogId id;
@Column(name = "EAISEVRINSTNCNAME", length = 20)
private String eaisevrinstncname;
@Column(name = "EAISVCNAME", length = 30)
private String eaisvcname;
@Column(name = "ADPTRBZWKGROUPNAME", length = 50)
private String adptrbzwkgroupname;
@Column(name = "THRESHOLDPERSECOND")
private Long thresholdpersecond;
@Column(name = "THRESHOLD")
private Long threshold;
@Column(name = "THRESHOLDTIMEUNIT", length = 12)
private String thresholdtimeunit;
@Override
public @NonNull InflowTokenInsufficientLogId getId() { return id; }
}
@@ -0,0 +1,21 @@
package com.eactive.eai.rms.data.entity.onl.djb.inflow;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import lombok.Data;
@Data
@Embeddable
public class InflowTokenInsufficientLogId implements Serializable {
private static final long serialVersionUID = 1L;
@Column(name = "EAIBZWKDSTCD", length = 4)
private String eaibzwkdstcd;
@Column(name = "MSGDPSTYMS", length = 17)
private String msgdpstyms;
@Column(name = "EAISVCSERNO", length = 41)
private String eaisvcserno;
}
@@ -0,0 +1,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;
}
@@ -0,0 +1,77 @@
package com.eactive.eai.rms.data.entity.onl.djb.webhook;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "PTL_WEBHOOK_SEND_LOG")
@Getter
@Setter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class WebhookSendLog {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "webhook_send_log_seq")
@SequenceGenerator(
name = "webhook_send_log_seq",
sequenceName = "SEQ_PTL_WEBHOOK_SEND_LOG",
allocationSize = 1
)
private Long id;
@Column(name = "TARGET_URL", nullable = false, length = 500)
private String targetUrl;
@Column(name = "EVENT_TYPE", length = 100)
private String eventType;
@Lob
@Column(name = "PAYLOAD")
private String payload;
@Column(name = "SIGNATURE", length = 500)
private String signature;
@Column(name = "STATUS_CODE")
private Integer statusCode;
@Column(name = "ORG_ID")
private String orgId;
@Lob
@Column(name = "RESPONSE_BODY")
private String responseBody;
@Column(name = "SUCCESS", length = 1)
private String success; // 오라클 CHAR(1) : 'Y' / 'N'
@Lob
@Column(name = "ERROR_MESSAGE")
private String errorMessage;
@Column(name = "RETRY_COUNT")
private Integer retryCount = 0;
@CreatedDate
@Column(name = "CREATED_AT", updatable = false)
private LocalDateTime createdAt;
@Column(name = "SENT_AT")
private LocalDateTime sentAt;
/* Boolean 편의 메서드 */
public void setSuccess(Boolean success) {
this.success = (success != null && success) ? "Y" : "N";
}
public Boolean getSuccess() {
return "Y".equalsIgnoreCase(this.success);
}
}
@@ -13,7 +13,9 @@ import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import com.eactive.apim.portal.app.entity.QCredential;
import com.eactive.eai.data.entity.onl.adapter.QAdapterGroup;
import com.eactive.eai.data.entity.onl.authserver.QClientEntity;
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.QInflowControl;
@@ -25,7 +27,6 @@ import com.eactive.eai.rms.onl.manage.inflow.history.InflowControlHistoryManUISe
import com.eactive.eai.rms.onl.manage.inflow.inflow.InflowControlManMapper;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
@@ -33,10 +34,15 @@ import com.querydsl.jpa.impl.JPAQueryFactory;
public class InflowControlService extends AbstractDataService<InflowControl, InflowControlId, InflowControlRepository> {
private static final String ADAPTER_TYPE_INFLOW = "01";
private static final String INTERFACE_TYPE_INFLOW = "02";
private static final String CLIENT_TYPE_INFLOW = "03";
@PersistenceContext
EntityManager entityManager;
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
EntityManager entityManagerForEMS;
@Autowired
InflowControlManMapper mapper;
@@ -124,7 +130,7 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
.select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc)
.from(qEAIMessageEntity)
.leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq("02").and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
.on(qInflowControl.id.type.eq(INTERFACE_TYPE_INFLOW).and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
.where(qEAIMessageEntity.eaisvcname.contains(searchName))
.orderBy(qEAIMessageEntity.eaisvcname.asc())
.offset(pageable.getOffset())
@@ -143,6 +149,8 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
.fetchOne();
return new PageImpl<>(dtoList, pageable, totalCount);
}
private InflowControlServiceDto toDto(QEAIMessageEntity qEAIMessageEntity, QInflowControl qInflowControl,
Tuple tuple) {
@@ -167,13 +175,75 @@ public class InflowControlService extends AbstractDataService<InflowControl, Inf
.select(qInflowControl, qEAIMessageEntity.eaisvcname, qEAIMessageEntity.eaisvcdesc)
.from(qEAIMessageEntity)
.leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq("02").and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
.on(qInflowControl.id.type.eq(INTERFACE_TYPE_INFLOW).and(qInflowControl.id.name.eq(qEAIMessageEntity.eaisvcname)))
.where(qEAIMessageEntity.eaisvcname.eq(eaisvcname))
.fetchOne();
return toDto(qEAIMessageEntity, qInflowControl, tuple);
}
public InflowControlServiceDto findByIdForClient(String clientId) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControl qInflowControl = QInflowControl.inflowControl;
QClientEntity qClientEntity = QClientEntity.clientEntity;
Tuple tuple = jpaQueryFactory
.select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
.from(qClientEntity)
.leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
.where(qClientEntity.clientid.eq(clientId))
.fetchOne();
return toDto(qClientEntity, qInflowControl, tuple);
}
public Page<InflowControlServiceDto> findAllForClient(Pageable pageable, String searchName) {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QInflowControl qInflowControl = QInflowControl.inflowControl;
QClientEntity qClientEntity = QClientEntity.clientEntity;
List<Tuple> tupleList = jpaQueryFactory
.select(qInflowControl, qClientEntity.clientid, qClientEntity.clientname)
.from(qClientEntity)
.leftJoin(qInflowControl)
.on(qInflowControl.id.type.eq(CLIENT_TYPE_INFLOW)
.and(qInflowControl.id.name.eq(qClientEntity.clientid)))
.where(qClientEntity.clientname.contains(searchName))
.orderBy(qClientEntity.clientname.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
List<InflowControlServiceDto> dtoList = tupleList
.stream()
.map(tuple -> toDto(qClientEntity, qInflowControl, tuple))
.collect(Collectors.toList());
long totalCount = jpaQueryFactory
.select(qClientEntity.clientname.count())
.from(qClientEntity)
.where(qClientEntity.clientname.contains(searchName))
.fetchOne();
return new PageImpl<>(dtoList, pageable, totalCount);
}
private InflowControlServiceDto toDto(QClientEntity qClientEntity, QInflowControl qInflowControl,
Tuple tuple) {
InflowControlServiceDto dto = null;
if (tuple.get(qInflowControl) != null)
dto = mapper.toDto(tuple.get(qInflowControl));
else {
dto = new InflowControlServiceDto();
dto.setName(tuple.get(qClientEntity.clientid));
dto.setType(CLIENT_TYPE_INFLOW);
}
dto.setDesc(tuple.get(qClientEntity.clientname));
return dto;
}
public Page<Tuple> selectLogList(Pageable pageable, InflowControlHistoryManUISearch uiSearch) {
QInflowControlLog qlog = QInflowControlLog.inflowControlLog;
QInflowControlGroup qgroup = QInflowControlGroup.inflowControlGroup;
@@ -120,11 +120,11 @@ public class ApiStatsDayService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -211,11 +211,10 @@ public class ApiStatsHourService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -293,9 +292,9 @@ public class ApiStatsHourService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -214,11 +214,11 @@ public class ApiStatsMinuteService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -120,11 +120,11 @@ public class ApiStatsMonthService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -172,9 +172,9 @@ public class ApiStatsMonthService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -117,11 +117,11 @@ public class ApiStatsYearService
q.seq900BizErrCnt.sum().as("seq900BizErrCnt"),
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
// 가중 평균: SUM(avg * successCnt) / SUM(successCnt)
// 가중 평균: SUM(avg * totalCnt) / SUM(totalCnt)
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -169,9 +169,9 @@ public class ApiStatsYearService
q.minRespTime.min().castToNum(BigDecimal.class).as("minRespTime"),
q.maxRespTime.max().castToNum(BigDecimal.class).as("maxRespTime"),
Expressions.cases()
.when(q.successCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.successCnt).sum()
.divide(q.successCnt.sum()).castToNum(BigDecimal.class))
.when(q.totalCnt.sum().gt(0))
.then(q.avgRespTime.multiply(q.totalCnt).sum()
.divide(q.totalCnt.sum()).castToNum(BigDecimal.class))
.otherwise((BigDecimal) null)
.as("avgRespTime")))
.from(q)
@@ -0,0 +1,13 @@
package com.eactive.eai.rms.data.entity.onl.security;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.eactive.eai.data.DataService;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
public interface CryptoModuleConfigDataService extends DataService<CryptoModuleConfig, String> {
Page<CryptoModuleConfig> findAll(Pageable pageable, String searchName, String algType, String keySourceType, String useYn);
}
@@ -0,0 +1,38 @@
package com.eactive.eai.rms.data.entity.onl.security;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
import com.eactive.eai.data.entity.onl.security.QCryptoModuleConfig;
import com.eactive.eai.data.jpa.AbstractDataService;
import com.querydsl.core.types.dsl.BooleanExpression;
@Service
@Transactional
public class CryptoModuleConfigDataServiceImpl
extends AbstractDataService<CryptoModuleConfig, String, CryptoModuleConfigRepository>
implements CryptoModuleConfigDataService {
@Override
public Page<CryptoModuleConfig> findAll(Pageable pageable, String searchName, String algType,
String keySourceType, String useYn) {
QCryptoModuleConfig q = QCryptoModuleConfig.cryptoModuleConfig;
BooleanExpression predicate = q.cryptoName.containsIgnoreCase(searchName != null ? searchName : "");
if (algType != null && !algType.isEmpty()) {
predicate = predicate.and(q.algType.eq(algType));
}
if (keySourceType != null && !keySourceType.isEmpty()) {
predicate = predicate.and(q.keySourceType.eq(keySourceType));
}
if (useYn != null && !useYn.isEmpty()) {
predicate = predicate.and(q.useYn.eq(useYn));
}
return repository.findAll(predicate, pageable);
}
}
@@ -0,0 +1,10 @@
package com.eactive.eai.rms.data.entity.onl.security;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
import com.eactive.eai.data.jpa.BaseRepository;
interface CryptoModuleConfigRepository
extends BaseRepository<CryptoModuleConfig, String>, QuerydslPredicateExecutor<CryptoModuleConfig> {
}
@@ -5,7 +5,9 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -26,7 +28,7 @@ public class ExtendedColumnDefinitionService
public List<ExtendedColumnDefinition> findAll() {
QExtendedColumnDefinition q = QExtendedColumnDefinition.extendedColumnDefinition;
List<ExtendedColumnDefinition> list = new ArrayList<>();
repository.findAll(q.isKey.desc(), q.keySeq.asc(), q.columnName.asc()).iterator().forEachRemaining(list::add);
repository.findAll(q.orderSeq.asc(), q.isKey.desc(), q.keySeq.asc(), q.columnName.asc()).iterator().forEachRemaining(list::add);
return list;
}
@@ -42,7 +44,12 @@ public class ExtendedColumnDefinitionService
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);
}
}
@@ -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) {
//
// }
}
@@ -0,0 +1,76 @@
package com.eactive.eai.rms.ext.djb.apistatus;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.jpa.BaseRepository;
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> {
/**
* API 상태 판단. 정상(N), 점검(C), 지연(D), 장애(E)
*
* 1. 정상/지연/장애 -> 점검중 : CONTROL_START
* 2. 점검 -> 점검X : CONTROL_END
* 3. 정상/지연 -> 장애 : ERROR_START
* 4. 장애 -> 장애X : ERROR_END
* 5. 정상 -> 지연 : DELAY_START
* 6. 지연 -> 지연X : DELAY_END
* 7. 기타 : STAY
*/
@Query(nativeQuery = true, value =
" SELECT EAISVCNAME"
+ " , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC"
+ " , PREV_STATUS_CODE "
+ " , EVENT"
+ " FROM ("
+ " SELECT EAISVCNAME"
+ " , STATUS_CODE AS PREV_STATUS_CODE "
+ " , CASE WHEN STATUS_CODE != 'C' AND CTRL_YN = 'Y' THEN 'CONTROL_START'"
+ " WHEN STATUS_CODE = 'C' AND CTRL_YN = 'N' THEN 'CONTROL_END'"
+ " WHEN STATUS_CODE IN ('N','D') AND ERR_YN = 'Y' THEN 'ERROR_START'"
+ " WHEN STATUS_CODE = 'E' AND ERR_YN = 'N' THEN 'ERROR_END'"
+ " WHEN STATUS_CODE = 'N' AND DELAY_YN = 'Y' THEN 'DELAY_START'"
+ " WHEN STATUS_CODE = 'D' AND DELAY_YN = 'N' THEN 'DELAY_END'"
+ " ELSE 'STAY' END AS EVENT"
+ " FROM ("
+ " SELECT A.EAISVCNAME"
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
+ " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X"
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID "
+ " AND SYSTIMESTAMP BETWEEN STARTED_AT AND END_AT) "
+ " AND A.EAISVCNAME = X.API_ID),'N') AS CTRL_YN "
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
+ " ELSE 'N' END"
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
+ " FROM API_STATS_MINUTE"
+ " WHERE A.EAISVCNAME = API_NAME"
+ " AND TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')"
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:errorRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')"
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS ERR_YN"
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
+ " WHEN RESP_SUM / TOTAL > :delayAvgRespTime THEN 'Y'"
+ " ELSE 'N' END"
+ " FROM (SELECT SUM(SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) AS TOTAL"
+ " , SUM(SUCCESS_CNT) AS SUCCESS"
+ " , SUM((SUCCESS_CNT + TIMEOUT_CNT + SYSTEM_ERR_CNT + BIZ_ERR_CNT) * AVG_RESP_TIME) AS RESP_SUM"
+ " FROM API_STATS_MINUTE"
+ " WHERE A.EAISVCNAME = API_NAME"
+ " AND TO_CHAR(STAT_TIME,'YYYYMMDDHH24MI')"
+ " BETWEEN TO_CHAR(SYSDATE - NUMTODSINTERVAL(:delayRangeMinute,'MINUTE'),'YYYYMMDDHH24MI')"
+ " AND TO_CHAR(SYSDATE,'YYYYMMDDHH24MI'))) AS DELAY_YN"
+ " FROM TSEAIHE01 A LEFT OUTER JOIN API_STATUS B ON A.EAISVCNAME = B.EAISVCNAME"
+ " )"
+ " ) A WHERE EVENT != 'STAY'")
List<ApiStatusEvent> findApiStatusEvents(
@Param("errorRate") int errorRate,
@Param("errorRangeMinute") int errorRangeMinute,
@Param("delayRangeMinute") int delayRangeMinute,
@Param("delayAvgRespTime") int delayAvgRespTime
);
}
@@ -0,0 +1,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);
}
}
@@ -0,0 +1,21 @@
package com.eactive.eai.rms.ext.djb.inflow;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLog;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficientLogId;
public interface InflowTokenRepository extends BaseRepository<InflowTokenInsufficientLog, InflowTokenInsufficientLogId> {
@Query(nativeQuery = true, value =
" SELECT EAISVCNAME" +
" , (SELECT EAISVCDESC FROM TSEAIHE01 WHERE A.EAISVCNAME = EAISVCNAME) AS EAISVCDESC" +
" , COUNT(*) AS CNT" +
" FROM TSEAIFR11 A" +
" WHERE MSGDPSTYMS >= TO_CHAR(SYSTIMESTAMP - NUMTODSINTERVAL(:rangeMinute, 'MINUTE'), 'YYYYMMDDHH24MISSFF3')" +
" GROUP BY EAISVCNAME")
List<InflowTokenInsufficient> countTokenInsufficient(@Param("rangeMinute") long rangeMinute);
}
@@ -0,0 +1,39 @@
package com.eactive.eai.rms.ext.djb.inflow;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
@Service
@Transactional
public class InflowTokenService {
private static final Logger log = LoggerFactory.getLogger(InflowTokenService.class);
@Autowired
private InflowTokenRepository inflowTokenRepository;
@Autowired
private UmsManager ums;
public void checkRecentFails(long rangeMinute) {
List<InflowTokenInsufficient> rows = inflowTokenRepository.countTokenInsufficient(rangeMinute);
for (InflowTokenInsufficient info : rows) {
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
String message = "유량제어 토큰 획득 실패하였습니다 \n" + info.getEaisvcname();
ums.sendMessenger("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, message);
}
}
}
@@ -0,0 +1,143 @@
package com.eactive.eai.rms.ext.djb.job;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.ApiStatsHourService;
import com.eactive.eai.rms.data.entity.onl.kjb.statistics.QApiStatsHour;
/**
* API 로그 테이블을 시간별 통계로 집계하는 배치 작업
* TSEAILGXX API_STATS_HOUR
* 실행 주기: 매시 00:10 (당일 데이터 집계)
*/
@Component
public class ApiStatsHourlyAggregationJob implements Job {
private static final Logger log = LoggerFactory.getLogger(ApiStatsHourlyAggregationJob.class);
@PersistenceContext
private EntityManager entityManager;
private ApiStatsHourService apiStatsHourService;
@Autowired
public void setApiStatsHourService(ApiStatsHourService apiStatsHourService) {
this.apiStatsHourService = apiStatsHourService;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
ApplicationContext appContext = null;
final ApiStatsHourlyAggregationJob selfJob;
try {
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
selfJob = appContext.getBean(ApiStatsHourlyAggregationJob.class);
} catch (SchedulerException e) {
log.error("applicationContext get module error", e);
return;
}
DataSourceType dataType = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW);
DataSourceContextHolder.setDataSourceType(dataType);
try {
LocalDate targetDate = LocalDate.now();
if (LocalTime.now().isBefore(LocalTime.of(1, 0))) {
targetDate = targetDate.minusDays(1);
}
selfJob.executeManual(targetDate);
} catch (Exception e) {
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
/**
* 수동 실행 메서드 (날짜 지정)
*/
@Transactional
public int executeManual(LocalDate targetDate) {
long startTime = System.currentTimeMillis();
log.info("=== 시간별 통계 집계 작업 시작 === 대상: {}", targetDate);
String searchDate = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String logTableName = CommonUtil.getLogTable(searchDate, true);
// 1. 기존 데이터 삭제
QApiStatsHour q = QApiStatsHour.apiStatsHour;
long deletedCount = apiStatsHourService.getJPAQueryFactory()
.delete(q)
.where(q.statTime.goe(targetDate.atStartOfDay())
.and(q.statTime.lt(targetDate.plusDays(1).atStartOfDay())))
.execute();
log.info("기존 데이터 삭제: {} 건", deletedCount);
// 2. 집계 데이터 입력
/**
* EAISVCSERNO로 그룹핑하여 1행으로 집계를 다음 (한거래 안에서 adapter가 여러개 있을 경우 seq=100의 adapter를 기준)
* 총건수 : EAISVCSERNO로 그룹핑한 건수
* 성공건수 : seq = 400 and EAIERRCD = null 건수
* Timeout : EAIERRCD in (공통코드 CODEGROUP = 'ERRCODE_TIMEOUT') 건수
* 시스템오류 : (seq400 = null or EAIERRCD is not null) and 공통코드(ERRCODE_TIMEOUT) 정의되지 않은 errorcode 건수
*/
String sql =
"INSERT INTO API_STATS_HOUR" +
" (STAT_TIME, API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, CLIENT_ID" +
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
", TOTAL_CNT, SUCCESS_CNT, TIMEOUT_CNT, SYSTEM_ERR_CNT, BIZ_ERR_CNT" +
", SEQ900_TIMEOUT_CNT, SEQ900_SYSTEM_ERR_CNT, SEQ900_BIZ_ERR_CNT" +
", AVG_RESP_TIME, MIN_RESP_TIME, MAX_RESP_TIME, P50_RESP_TIME, P95_RESP_TIME)" +
" SELECT TO_TIMESTAMP(SUBSTR(DT,1,10) || '0000', 'YYYYMMDDHH24MISS')" +
" , API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER" +
" , COUNT(EAISVCSERNO)" +
" , SUM(CASE WHEN A.ERROR_CODE IS NULL AND E400 IS NOT NULL THEN 1 ELSE 0 END)" +
" , SUM(CASE WHEN B.CODE IS NOT NULL THEN 1 ELSE 0 END)" +
" , SUM(CASE WHEN (A.E400 IS NULL OR A.ERROR_CODE IS NOT NULL) AND B.CODE IS NULL THEN 1 ELSE 0 END)" +
" , 0, 0, 0, 0" +
" , TRUNC(AVG(RESP_TIME)), MIN(RESP_TIME), MAX(RESP_TIME), 0, 0" +
" FROM (" +
" SELECT EAISVCSERNO" +
" , MAX(EAISVCNAME) AS API_NAME, MAX(EAISEVRINSTNCNAME) AS GW_INSTANCE_ID" +
" , MAX(EAIBZWKDSTCD) AS BIZ_DIV_CODE, MAX(CLIENTID) AS CLIENT_ID" +
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN GSTATSYSADPTRBZWKGROUPNAME ELSE '' END) AS INBOUND_ADAPTER" +
" , MAX(CASE WHEN LOGPRCSSSERNO = '100' THEN PSVSYSADPTRBZWKGROUPNAME ELSE '' END) AS OUTBOUND_ADAPTER" +
" , MIN(MSGDPSTYMS) AS DT" +
" , MAX(MSGPRCSSYMS) - MIN(MSGDPSTYMS) AS RESP_TIME" +
" , MAX(CASE WHEN LOGPRCSSSERNO = '400' THEN LOGPRCSSSERNO ELSE '' END) AS E400" +
" , MAX(EAIERRCD) AS ERROR_CODE" +
" FROM " + logTableName +
" WHERE MSGDPSTYMS LIKE :searchDate || '%'" +
" GROUP BY EAISVCSERNO" +
" ) A LEFT OUTER JOIN TSEAICM20 B ON A.ERROR_CODE = B.CODE AND B.CODEGROUP = 'ERRCODE_TIMEOUT' AND B.USEYN = 'Y' " +
" GROUP BY SUBSTR(DT,1,10), API_NAME, GW_INSTANCE_ID, BIZ_DIV_CODE, NVL(A.CLIENT_ID, 'NONE')" +
" , INBOUND_ADAPTER, OUTBOUND_ADAPTER";
int savedCount = entityManager.createNativeQuery(sql)
.setParameter("searchDate", searchDate)
.executeUpdate();
long elapsedTime = System.currentTimeMillis() - startTime;
log.info("=== 집계 완료 === (처리 건수: {}, 소요 시간: {}ms)", savedCount, elapsedTime);
return savedCount;
}
}
@@ -0,0 +1,192 @@
package com.eactive.eai.rms.ext.djb.job;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.ext.djb.apistatus.ApiStatusService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
/**
* Job - Quartz Job
* API_STATS_MINUTE 데이터를 1분마다 조회하여 Api 상태를 판단하여 API_STATUS 테이블을 insert/update 한다.
* & api 상태가 변경된 경우, 알림 테이블에 저장한다
*
* <p>Cron Schedule 권장:</p>
* <ul>
* <li>기본: 0 * * * * ? (매분 1회 실행)</li>
* </ul>
*
* <p>Job 파라미터 (JobDataMap):</p>
* <ul>
* <li>
* <b>api.status.error.range_minute</b>: API 장애기준 판단 시간간격 (단위: , 기본값: 1)
* <b>api.status.error.rate</b>: API 장애기준 오류건수 비율(%) (단위: 시간, 기본값: 100)
* <b>api.status.delay.range_minute</b>: API 지연기준 판단 시간간격 (단위: , 기본값: 1)
* <b>api.status.delay.avg_resp_time</b>: API 지연기준 평균응답시간 (단위: 밀리세컨드, 기본값: 10000)
* </li>
* </ul>
*
* </pre>
*/
@DisallowConcurrentExecution
public class ApiStatusMonitorJob implements Job {
private static final Logger log = LoggerFactory.getLogger(ApiStatusMonitorJob.class);
/** Job 파라미터 키: API 장애기준 시간간격(분) */
public static final String KEY_API_STATUS_ERROR_RANGE_MINUTE = "api.status.error.range_minute";
/** Job 파라미터 키: API 장애기준 오류건수 비율(%) */
public static final String KEY_API_STATUS_ERROR_RATE = "api.status.error.rate";
/** Job 파라미터 키: API 지연기준 시간간격(분) */
public static final String KEY_API_STATUS_DELAY_RANGE_MINUTE = "api.status.delay.range_minute";
/** Job 파라미터 키: API 지연기준 평균응답시간(ms) */
public static final String KEY_API_STATUS_DELAY_AVG_RESP_TIME = "api.status.delay.avg_resp_time";
public static final String DEFAULT_ERROR_RANGE_MINUTE = "1";
public static final String DEFAULT_ERROR_RATE = "100";
public static final String DEFAULT_DELAY_RANGE_MINUTE = "1";
public static final String DEFAULT_DELAY_AVG_RESP_TIME = "10000";
private transient MonitoringContext monitoringContext;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
// execute.instances 지정된 경우 해당 인스턴스에서만 실행
String allowedInstances = jobDataMap.getString("execute.instances");
if (StringUtils.isNotEmpty(allowedInstances)) {
Set<String> instanceSet = Arrays.stream(allowedInstances.split(","))
.map(s -> s.trim().toLowerCase())
.collect(Collectors.toSet());
String currentInstance = StringUtils.defaultString(System.getProperty("inst.Name")).trim().toLowerCase();
if (!instanceSet.contains(currentInstance)) {
log.debug("Job 실행 인스턴스 제한 - 현재: {}, 허용: {}", currentInstance, allowedInstances);
return;
}
}
log.info("*** START ApiStatusUpdateJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
// Job 파라미터 로깅
logJobParameters(jobDataMap);
HashMap<String, String> param = this.checkParameters(jobDataMap);
ApplicationContext appContext;
try {
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
} catch (SchedulerException e) {
log.error("applicationContext get module error", e);
return;
}
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
ApiStatusService apiStatusUpdateService = appContext.getBean(ApiStatusService.class);
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try {
apiStatusUpdateService.updateApiStatus(param);
} catch (Exception e) {
log.error("ApiStatusUpdateJob execution failed", e);
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
log.info("*** END ApiStatusUpdateJob run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
}
/**
* Job 파라미터 로깅
*/
private void logJobParameters(JobDataMap jobDataMap) {
if (jobDataMap == null || jobDataMap.isEmpty()) {
log.debug("Job 파라미터 없음 (기본값 사용)");
return;
}
StringBuilder sb = new StringBuilder("Job 파라미터: ");
for (String key : jobDataMap.getKeys()) {
sb.append(key).append("=").append(jobDataMap.getString(key)).append(", ");
}
log.info(sb.toString());
}
/**
* 집계 범위 시간 파라미터 파싱
*
* @param jobDataMap Job 파라미터
* @return 집계 범위 시간 (파싱 실패 기본값 반환)
*/
private HashMap<String,String> checkParameters(JobDataMap jobDataMap) {
HashMap<String, String> param = new HashMap<String, String>();
if (jobDataMap == null) {
param.put("errorRangeMinute", DEFAULT_ERROR_RANGE_MINUTE); //1분동안
param.put("errorRate", DEFAULT_ERROR_RATE); //에러가 100% 발생시 '장애' 판단
param.put("delayRangeMinute", DEFAULT_DELAY_RANGE_MINUTE); //1분동안
param.put("delayAvgRespTime", DEFAULT_DELAY_AVG_RESP_TIME); //평균 응답속도가 10초 이상이면 '지연'으로 판단
return param;
}
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_ERROR_RANGE_MINUTE))) {
param.put("errorRangeMinute", DEFAULT_ERROR_RANGE_MINUTE);
} else {
param.put("errorRangeMinute", jobDataMap.getString(KEY_API_STATUS_ERROR_RANGE_MINUTE));
}
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_ERROR_RATE))) {
param.put("errorRate", DEFAULT_ERROR_RATE);
} else {
param.put("errorRate", jobDataMap.getString(KEY_API_STATUS_ERROR_RATE));
}
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_DELAY_RANGE_MINUTE))) {
param.put("delayRangeMinute", DEFAULT_DELAY_RANGE_MINUTE);
} else {
param.put("delayRangeMinute", jobDataMap.getString(KEY_API_STATUS_DELAY_RANGE_MINUTE));
}
if (StringUtils.isEmpty(jobDataMap.getString(KEY_API_STATUS_DELAY_AVG_RESP_TIME))) {
param.put("delayAvgRespTime", DEFAULT_DELAY_AVG_RESP_TIME);
} else {
param.put("delayAvgRespTime", jobDataMap.getString(KEY_API_STATUS_DELAY_AVG_RESP_TIME));
}
return param;
}
/**
* 개발 모드 여부 확인
* @return eai.systemmode=D 이면 true
*/
private boolean isDevMode() {
String systemMode = System.getProperty("eai.systemmode", "");
return "D".equalsIgnoreCase(systemMode);
}
}
@@ -0,0 +1,120 @@
package com.eactive.eai.rms.ext.djb.job;
import java.util.Arrays;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import com.eactive.eai.rms.common.context.MonitoringContext;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.ext.djb.inflow.InflowTokenService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
/**
* Job - Quartz Job
* TSEAIFR11 데이터를 10분마다 조회하여 데이터가 있는 경우(유량제어 토큰 획득 실패), 알림을 발송한다
*
* <p>Cron Schedule 권장:</p>
* <ul>
* <li>기본: 0 0/10 * * * ? (매10분 1회 실행)</li>
* </ul>
*
* <p>Job 파라미터 (JobDataMap):</p>
* <ul>
* <li>
* NONE
* </li>
* </ul>
*
*/
@DisallowConcurrentExecution
public class InflowTokenMonitorJob implements Job {
private static final Logger log = LoggerFactory.getLogger(InflowTokenMonitorJob.class);
private transient MonitoringContext monitoringContext;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
// execute.instances 지정된 경우 해당 인스턴스에서만 실행
String allowedInstances = jobDataMap.getString("execute.instances");
if (StringUtils.isNotEmpty(allowedInstances)) {
Set<String> instanceSet = Arrays.stream(allowedInstances.split(","))
.map(s -> s.trim().toLowerCase())
.collect(Collectors.toSet());
String currentInstance = StringUtils.defaultString(System.getProperty("inst.Name")).trim().toLowerCase();
if (!instanceSet.contains(currentInstance)) {
log.debug("Job 실행 인스턴스 제한 - 현재: {}, 허용: {}", currentInstance, allowedInstances);
return;
}
}
log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
long execMinute = 1; //default 배치 실행주기()
Trigger trigger = context.getTrigger();
Date prevFireTime = trigger.getPreviousFireTime();
Date nextFireTime = trigger.getNextFireTime();
//1회 이상 배치가 실행된 경우, 배치 간격을 구하여 유량제어조회 구간 파라미터로 전달한다
if (prevFireTime != null && nextFireTime != null) {
long diffMillis = nextFireTime.getTime() - prevFireTime.getTime();
execMinute = TimeUnit.MILLISECONDS.toMinutes(diffMillis);
log.info("실행 주기: {}분", execMinute);
}
ApplicationContext appContext;
try {
appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
} catch (SchedulerException e) {
log.error("applicationContext get module error", e);
return;
}
monitoringContext = (MonitoringContext) appContext.getBean("monitoringContext");
InflowTokenService service = appContext.getBean(InflowTokenService.class);
DataSourceContextHolder.setDataSourceType(
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
try {
service.checkRecentFails(execMinute);
} catch (Exception e) {
log.error("InflowTokenFailMonitorJob execution failed", e);
throw new JobExecutionException(e);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
log.info("*** END InflowTokenFailMonitorJob run({})", DateUtil.getDateTime("yyyy-MM-dd HH:mm"));
}
/**
* 개발 모드 여부 확인
* @return eai.systemmode=D 이면 true
*/
private boolean isDevMode() {
String systemMode = System.getProperty("eai.systemmode", "");
return "D".equalsIgnoreCase(systemMode);
}
}

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