Compare commits
247 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a92fc85730 | |||
| 317634b5b7 | |||
| 27fa79ddd9 | |||
| ccc55fd015 | |||
| 3bf8370d0c | |||
| 46bd79bc36 | |||
| cdc1a6f342 | |||
| 42d7fa977e | |||
| cc0ef13d5d | |||
| 6604f7c9e8 | |||
| cd8f826ba5 | |||
| 031796dac4 | |||
| f091b9ab19 | |||
| f47480674d | |||
| cd20aeee2a | |||
| 0365f4342e | |||
| 0576fd4c6e | |||
| 8215b596fe | |||
| fc6b96d7cd | |||
| 5fe247df65 | |||
| d11f7d9f38 | |||
| 889fd1f9f5 | |||
| c0844e8d36 | |||
| db38b8f6e9 | |||
| 8abe0a8585 | |||
| ca8d46c7b8 | |||
| 628ab01c13 | |||
| f10f5ce08e | |||
| 90ba584e4b | |||
| e59f64653f | |||
| 2422c7f01e | |||
| 29025d5f47 | |||
| e91b12f2d7 | |||
| da90a3a002 | |||
| a4614ca7fd | |||
| 2d414dc31b | |||
| 775d69eb53 | |||
| 8f4162569d | |||
| adc5371df8 | |||
| e7297458f6 | |||
| c10bfe8326 | |||
| c224f80cdf | |||
| 6c70d9611b | |||
| 26679cf649 | |||
| 406dd34e28 | |||
| 59dd28c23b | |||
| cd93e34840 | |||
| 2fddffe29d | |||
| 7a73ef370f | |||
| c9be6069ae | |||
| a13c8f154f | |||
| 8c98297634 | |||
| f36bfcc642 | |||
| 2833944fe4 | |||
| 18110641d6 | |||
| 316f787276 | |||
| c3c9f7a2da | |||
| 95e31befbb | |||
| f1f7576b9d | |||
| 545e09891b | |||
| 70c2ac8376 | |||
| 21c493cb0e | |||
| b9c6a0da06 | |||
| a0725b57a6 | |||
| 50bfcacf27 | |||
| f400f0322f | |||
| d0cd8118f3 | |||
| 07e65c055b | |||
| af8a9cca51 | |||
| 39a09d0f5d | |||
| 5b940717c4 | |||
| 8ccf52fd66 | |||
| dedc58a07f | |||
| 4530af9dec | |||
| cbf3066a56 | |||
| 160e28444a | |||
| d7dcf9a81c | |||
| acfe6b2d03 | |||
| 481e82fec7 | |||
| 1b382f5859 | |||
| 608cb4188e | |||
| 534947637d | |||
| 153c68c6ab | |||
| 80ef41f4fc | |||
| 8aebabe005 | |||
| 6d33b69bed | |||
| 49ebdce4c3 | |||
| 979e43e382 | |||
| 53b075967c | |||
| 724117860b | |||
| 0ee52392aa | |||
| f0af06da88 | |||
| b16d03517a | |||
| edf024f0a3 | |||
| a1a1f27498 | |||
| c62f395983 | |||
| c3955cefd8 | |||
| 5c378eac0d | |||
| e51cdbc42a | |||
| 142d402bad | |||
| f23cb02b1c | |||
| eca120812c | |||
| 71b0fc7009 | |||
| 7b12ef2fb5 | |||
| 292e27e66e | |||
| 61d710dd8c | |||
| 8494758fcd | |||
| f2b25d2775 | |||
| 18489e2440 | |||
| 681e4bd014 | |||
| 27abe51b7b | |||
| 55ef71c5e7 | |||
| 235f4b67b3 | |||
| 89c4b89a71 | |||
| 7193070ebd | |||
| 49616cdd84 | |||
| d1683cc319 | |||
| b5ba2c2bee | |||
| baa528a628 | |||
| f49814b2a1 | |||
| 0ea73aa66f | |||
| ed44183de3 | |||
| aee02cc2a6 | |||
| 99b4470ffe | |||
| d9201eb603 | |||
| 185a23d748 | |||
| 99a440094d | |||
| 37380f2975 | |||
| 908b2ebcc8 | |||
| 25a8e13752 | |||
| 7928a0c27b | |||
| 65ba6c1115 | |||
| 2dcf524cd6 | |||
| bb991cea81 | |||
| 702a9a899b | |||
| 7520b96ed5 | |||
| 7935e69871 | |||
| 4cf6dc4da0 | |||
| a5d55f2647 | |||
| af8b925e65 | |||
| 62ae2d93ff | |||
| 124ecabccf | |||
| c521835633 | |||
| fe201b88cd | |||
| 9f81ad697d | |||
| ca94f2e70a | |||
| 97937a8a35 | |||
| a8e124db5d | |||
| a514b9d7a9 | |||
| 2ac61636a0 | |||
| 399bd9c58d | |||
| b8beeb7c3e | |||
| 0bb00b4fb9 | |||
| 655c35b1e1 | |||
| efd1c748ed | |||
| 64a9c7408e | |||
| 8f9411f990 | |||
| ed1f55c534 | |||
| d1392821ef | |||
| 6542b0c0f8 | |||
| f7de1b6885 | |||
| 3e32220061 | |||
| 10db8129eb | |||
| e34829437d | |||
| ef3f0e802a | |||
| b741a7e563 | |||
| e8069ef371 | |||
| c0e218451e | |||
| af63b1a256 | |||
| 02072df973 | |||
| bc478ad4ed | |||
| 161a68a0a2 | |||
| 3b59f7acd7 | |||
| 1e29d1d721 | |||
| 9342b6484c | |||
| dc3842aa9b | |||
| a440ac842d | |||
| df5bbb162a | |||
| 3eec267769 | |||
| 6ab7e1fe16 | |||
| 58fe176715 | |||
| 6b6e2863cd | |||
| cfafccc34d | |||
| fe98ac45c4 | |||
| 5f9de93d35 | |||
| 48dfcf7714 | |||
| 6e3906878a | |||
| fafe7e45cb | |||
| eee7f46aa7 | |||
| b5eb63448a | |||
| 54291e5cbb | |||
| a0743465ab | |||
| d3e05256b7 | |||
| 4d5572f880 | |||
| e21014f992 | |||
| 43fe63964f | |||
| ac0d081a9a | |||
| 164ab92919 | |||
| a6e7d1adb3 | |||
| e51331986d | |||
| 1aa40f2c61 | |||
| c8caeeda8c | |||
| 06ad1230b2 | |||
| 8109f91aa2 | |||
| 62ccebd773 | |||
| ec8d6bfd7b | |||
| 480d7dac5a | |||
| feb4f845a8 | |||
| 7857f5b45c | |||
| 68c8e18e8d | |||
| 35e5564252 | |||
| 0d7d006f92 | |||
| 737b493f7c | |||
| af0a878a39 | |||
| b639e29039 | |||
| 350297aaa2 | |||
| 23ba7993c3 | |||
| 7677e3b26e | |||
| 90c43ea7b0 | |||
| 191857811f | |||
| 88ffc1cc28 | |||
| d3bd4acb62 | |||
| 1f7166502f | |||
| 91f36605e8 | |||
| 0ebd31e256 | |||
| 45c6edbbeb | |||
| 335c7ec53a | |||
| 0b341b85f1 | |||
| e9ac62d3ed | |||
| 133c05f1d7 | |||
| 4a1a4be54d | |||
| dbd36f28d4 | |||
| 4ee50be000 | |||
| 3d916eda53 | |||
| 49fdc3b577 | |||
| 9d55db3281 | |||
| 9a5d5e4815 | |||
| e9bff78cbf | |||
| 6ccd669ec6 | |||
| 5db046eb10 | |||
| 8bd8558038 | |||
| c3f5d6a42e | |||
| 72cdb04261 | |||
| d3601c3991 | |||
| e213c96994 | |||
| 889d1fcfad | |||
| 45f980f638 |
@@ -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
-2
@@ -1,5 +1,3 @@
|
||||
gradle.properties
|
||||
|
||||
# Package Files #
|
||||
*.war
|
||||
|
||||
@@ -248,3 +246,6 @@ eapim-admin-kjb/
|
||||
*.report.md
|
||||
*.claude.md
|
||||
tmpclaude-*-cwd
|
||||
logs
|
||||
rinjae_
|
||||
tomcat-base
|
||||
@@ -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`)은 다음을 포함합니다:
|
||||
|
||||
Vendored
+189
@@ -0,0 +1,189 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
skipDefaultCheckout() // stage별 agent의 자동 SCM checkout 방지 (Deploy 노드는 git 접근 불필요, unstash로만 WAR 수신)
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
|
||||
WEBHOOK_URL = 'http://172.30.1.50:18000/api/hooks/01ba51b01d3c4c98ae8f3183a23a84ed713197857d024b5da4f303458195eb32'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Build (djb-vm)') {
|
||||
agent { label 'djb-vm' }
|
||||
environment {
|
||||
JAVA_HOME = '/apps/opts/jdk8'
|
||||
GRADLE_HOME = '/apps/opts/gradle-8.7'
|
||||
GRADLE_USER_HOME = '/apps/opts/gradle-home'
|
||||
NODE_HOME = '/apps/opts/node-v24'
|
||||
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/node-v24/bin:/apps/opts/bin:${env.PATH}"
|
||||
}
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps { checkout scm }
|
||||
}
|
||||
|
||||
stage('Checkout dependencies') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
cd "$WORKSPACE/.."
|
||||
|
||||
mkdir -p eapim-online
|
||||
|
||||
for REPO in elink-online-core elink-online-core-jpa elink-online-transformer elink-online-common elink-online-emsclient; do
|
||||
TARGET="eapim-online/$REPO"
|
||||
if [ ! -d "$TARGET/.git" ]; then
|
||||
rm -rf "$TARGET"
|
||||
git clone --depth=1 --branch master \
|
||||
"ssh://git@172.30.1.50:2222/djb-eapim/$REPO.git" \
|
||||
"$TARGET"
|
||||
else
|
||||
git -C "$TARGET" fetch --depth=1 origin master
|
||||
git -C "$TARGET" reset --hard origin/master
|
||||
git -C "$TARGET" clean -fdx
|
||||
fi
|
||||
done
|
||||
|
||||
for REPO in elink-portal-common eapim-admin-djb; do
|
||||
if [ ! -d "$REPO/.git" ]; then
|
||||
rm -rf "$REPO"
|
||||
git clone --depth=1 --branch master \
|
||||
"ssh://git@172.30.1.50:2222/djb-eapim/$REPO.git" \
|
||||
"$REPO"
|
||||
else
|
||||
git -C "$REPO" fetch --depth=1 origin master
|
||||
git -C "$REPO" reset --hard origin/master
|
||||
git -C "$REPO" clean -fdx
|
||||
fi
|
||||
done
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Verify toolchain') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
java -version
|
||||
gradle --version
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build WAR') {
|
||||
steps { sh 'gradle clean build -x test --no-daemon -Pprofile=weblogic' }
|
||||
post {
|
||||
success {
|
||||
sh '''
|
||||
set -eu
|
||||
cd build/libs
|
||||
sha256sum eapim-admin.war > eapim-admin.war.sha256
|
||||
'''
|
||||
archiveArtifacts artifacts: 'build/libs/eapim-admin.war,build/libs/eapim-admin.war.sha256', fingerprint: true
|
||||
stash name: 'war', includes: 'build/libs/eapim-admin.war'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SBOM(CycloneDX) -> xlsx. 산출물 eapim-admin-sbom.xlsx 를 아티팩트로 보관.
|
||||
// 실패해도 배포는 진행하도록 UNSTABLE 로만 표시한다.
|
||||
stage('SBOM') {
|
||||
steps {
|
||||
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
|
||||
sh 'gradle sbomXlsx --no-daemon -Pprofile=weblogic'
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/sbom/*.xlsx', fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy (weblogic)') {
|
||||
agent { label 'weblogic' }
|
||||
environment {
|
||||
JENKINS_NODE_COOKIE = 'dontKillMe' // background weblogic를 빌드 종료 시 죽이지 않게
|
||||
WL_HOME = '/app/eapim/adminportal'
|
||||
WL_DEPLOY_DIR = '/app/eapim/adminportal'
|
||||
WL_WAR_NAME = 'eapim-admin.war'
|
||||
WL_HTTP_PORT = '39120'
|
||||
WL_NOHUP = '/logs/weblogic/domains/eapimDomain/nohup.emsSvr11.out'
|
||||
}
|
||||
stages {
|
||||
stage('Stop WebLogic') {
|
||||
steps {
|
||||
sh '"$WL_HOME/stopEms11.sh"' // 동기: 완전 종료까지 블록, 미기동 시에도 안전
|
||||
}
|
||||
}
|
||||
stage('Deploy WAR') {
|
||||
steps {
|
||||
unstash 'war'
|
||||
sh '''
|
||||
set -eu
|
||||
cp build/libs/eapim-admin.war "$WL_DEPLOY_DIR/$WL_WAR_NAME"
|
||||
ls -la "$WL_DEPLOY_DIR"
|
||||
'''
|
||||
}
|
||||
}
|
||||
stage('Start WebLogic and readiness') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
"$WL_HOME/startEms11.sh" # nohup & 로 백그라운드 기동, 즉시 리턴
|
||||
|
||||
DEADLINE=$(($(date +%s) + 300))
|
||||
STATUS=000
|
||||
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||
"http://localhost:$WL_HTTP_PORT/monitoring/loginForm.do" 2>/dev/null || echo "000")
|
||||
case "$STATUS" in
|
||||
200|302|401) echo "Readiness OK (/monitoring/loginForm.do HTTP $STATUS)"; break ;;
|
||||
esac
|
||||
sleep 3
|
||||
done
|
||||
|
||||
case "$STATUS" in
|
||||
200|302|401) ;;
|
||||
*)
|
||||
echo "Readiness failed within 300s, last HTTP=$STATUS"
|
||||
[ -f "$WL_NOHUP" ] && tail -120 "$WL_NOHUP"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
node('weblogic') {
|
||||
sh '''
|
||||
set +e
|
||||
payload=$(printf '{"text":"[%s #%s](%s) SUCCESS"}' "$JOB_NAME" "$BUILD_NUMBER" "$BUILD_URL")
|
||||
curl -sS --fail --max-time 5 -H 'Content-Type: application/json' -X POST --data "$payload" "$WEBHOOK_URL" >/dev/null || echo "Webhook failed"
|
||||
'''
|
||||
}
|
||||
}
|
||||
failure {
|
||||
node('weblogic') {
|
||||
sh '''
|
||||
set +e
|
||||
payload=$(printf '{"text":"[%s #%s](%s) FAILURE"}' "$JOB_NAME" "$BUILD_NUMBER" "$BUILD_URL")
|
||||
curl -sS --fail --max-time 5 -H 'Content-Type: application/json' -X POST --data "$payload" "$WEBHOOK_URL" >/dev/null || echo "Webhook failed"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,19 +37,19 @@
|
||||
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-->
|
||||
<!-- 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>
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ PortalTermsManController_약관관리_APIGW_INSERT,UPDATE,DELETE
|
||||
ProdClientManController_운영Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE
|
||||
ApiSpecController_API스펙관리_APIGW_INSERT,DELETE
|
||||
MessageTemplateManController_메세지템플릿관리_APIGW_INSERT,UPDATE,DELETE
|
||||
PortalPartnershipManController_사업제휴신청관리_APIGW_UNMASK
|
||||
PortalPartnershipManController_피드백/개선요청관리_APIGW_UNMASK,DELETE
|
||||
PortalPropertyController_포탈 프로퍼티 관리_APIGW_UPDATE
|
||||
PortalUserTermsManController_약관동의 이력_APIGW_UNMASK
|
||||
PortalUserTermsManController_약관동의 이력_APIGW_UNMASK
|
||||
MessageRequestManController_메세지발송내역_APIGW_UPDATE_STATUS
|
||||
PortalInquiryManController_Q&A문의관리_APIGW_VISIBILITY
|
||||
PortalMenuManController_포탈메뉴관리_APIGW_INSERT,UPDATE,DELETE,TRANSACTION_PLACEMENT,INITIALIZE,TRANSACTION_RELOAD
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
targetNamespace="http://xmlns.oracle.com/weblogic/weblogic-web-app"
|
||||
xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app"
|
||||
elementFormDefault="qualified">
|
||||
|
||||
<xs:element name="weblogic-web-app">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="context-root" type="xs:string" minOccurs="0"/>
|
||||
<xs:element name="session-descriptor" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="timeout-secs" type="xs:integer" minOccurs="0"/>
|
||||
<xs:element name="persistent-store-type" type="xs:string" minOccurs="0"/>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="container-descriptor" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="prefer-application-packages" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="package-name" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="prefer-application-resources" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="resource-name" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
</xs:schema>
|
||||
@@ -1,12 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<weblogic-web-app
|
||||
xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://xmlns.oracle.com/weblogic/weblogic-web-app weblogic-web-app.xsd">
|
||||
<weblogic-web-app xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app">
|
||||
|
||||
<context-root>monitoring</context-root>
|
||||
<session-descriptor>
|
||||
<timeout-secs>1800</timeout-secs>
|
||||
<cookie-name>JSESSIONID_EMS</cookie-name>
|
||||
<persistent-store-type>replicated_if_clustered</persistent-store-type>
|
||||
</session-descriptor>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@charset "utf-8";
|
||||
|
||||
.gnb .depth1 > li:hover > a{color:#008cc5;} /* theme */
|
||||
.gnb .sitemap-link:hover{color:#008cc5;} /* theme */
|
||||
.gnb .depth1 > li > .red_box{background:#008cc5;}/* theme */
|
||||
.gnb .depth2 > li > a:hover{color:#008cc5;}/* theme */
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@charset "utf-8";
|
||||
|
||||
.gnb .depth1 > li:hover > a{color:#16a085;} /* theme */
|
||||
.gnb .sitemap-link:hover{color:#16a085;} /* theme */
|
||||
.gnb .depth1 > li > .red_box{background:#16a085;}/* theme */
|
||||
.gnb .depth2 > li > a:hover{color:#16a085;}/* theme */
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@charset "utf-8";
|
||||
|
||||
.gnb .depth1 > li:hover > a{color:#e74c3c;} /* theme */
|
||||
.gnb .sitemap-link:hover{color:#e74c3c;} /* theme */
|
||||
.gnb .depth1 > li > .red_box{background:#e74c3c;}/* theme */
|
||||
.gnb .depth2 > li > a:hover{color:#e74c3c;}/* theme */
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@charset "utf-8";
|
||||
|
||||
.gnb .depth1 > li:hover > a{color:#ff80c0;} /* theme */
|
||||
.gnb .sitemap-link:hover{color:#ff80c0;} /* theme */
|
||||
.gnb .depth1 > li > .red_box{background:#ff80c0;}/* theme */
|
||||
.gnb .depth2 > li > a:hover{color:#ff80c0;}/* theme */
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@charset "utf-8";
|
||||
|
||||
.gnb .depth1 > li:hover > a{color:#be8200;} /* theme */
|
||||
.gnb .sitemap-link:hover{color:#be8200;} /* theme */
|
||||
.gnb .depth1 > li > .red_box{background:#be8200;}/* theme */
|
||||
.gnb .depth2 > li > a:hover{color:#be8200;}/* theme */
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@ header.sub{position:relative; padding-top:80px;}
|
||||
100% { color:rgba(255,96,78,0); }
|
||||
} */
|
||||
|
||||
.gnb_bg{position:absolute; top:80px; left:0; display:none; width:100%; height:382px; box-sizing:border-box; background:white; box-shadow: 0px 5px 5px 0px rgba(0,0,0,0.2);}
|
||||
.gnb{position:absolute; top:0px; left:230px; display:inline-block; height:80px; overflow-y:hidden;}
|
||||
.gnb_bg{position:absolute; top:80px; left:0; display:none; width:100%; height:282px; box-sizing:border-box; background:white; box-shadow: 0px 5px 5px 0px rgba(0,0,0,0.2);}
|
||||
.gnb{position:absolute; top:0px; left:180px; display:inline-block; height:80px; overflow-y:hidden;}
|
||||
.gnb:hover{height:auto; overflow-y:visible;}
|
||||
.gnb:hover + .gnb_bg{display:block;}
|
||||
.gnb h1{float:left; display:block; width:auto; height:100%; line-height:80px;}
|
||||
@@ -71,15 +71,15 @@ header.sub{position:relative; padding-top:80px;}
|
||||
|
||||
.gnb .depth1 > li{position:relative;}
|
||||
.gnb .depth1 > li > a{position:relative; display:block; box-sizing:border-box; color:#000; font-size:14px; width:160px; height:80px; text-align:center; line-height:80px; letter-spacing:-0.5px; opacity:0.7; transition:all 0.3s;}
|
||||
.gnb .depth1 > li > a:after{content:''; position:absolute; top:34px; right:0; display:inline-block; width:1px; height:13px; background:#ddd;}
|
||||
.gnb .depth1 > li:first-child > a:before{content:''; position:absolute; top:34px; left:0; display:inline-block; width:1px; height:13px; background:#ddd;}
|
||||
.gnb .depth1 > li > a:after{content:''; position:absolute; top:34px; right:0; display:inline-block; width:1px; height:13px; }
|
||||
.gnb .depth1 > li:first-child > a:before{content:''; position:absolute; top:34px; left:0; display:inline-block; width:1px; height:13px; }
|
||||
/*
|
||||
.gnb .depth1 > li:hover .red_box{transform:scaleX(1);}
|
||||
.gnb .depth1 > li:hover > a{opacity:1;}
|
||||
.gnb .depth1 > li > .red_box{display:block; position:absolute; width:100%; height:2px; top:78px; box-sizing:border-box; background:#ed1c24; transform-origin:center center; transform:scaleX(0); transition:all 0.3s;}
|
||||
*/
|
||||
.gnb .depth2{position:absolute; left:0; top:80px; width:160px; height:410px; box-sizing:border-box; padding:20px 0; border-right:1px solid #eee; text-align:center; z-index:9;}
|
||||
.gnb .depth2.first{border-left:1px solid #eee;}
|
||||
.gnb .depth2{position:absolute; left:0; top:80px; width:160px; height:280px; box-sizing:border-box; padding:20px 0; border-right:1px solid #eee; text-align:center; z-index:9;}
|
||||
.gnb .depth1 > li:first-child .depth2{border-left:1px solid #eee;}
|
||||
.gnb .depth2 > li > a{display:block; color:#000; font-size:13px; line-height:30px;}
|
||||
.gnb .depth2 > li > a:hover{color:#ff0000;}
|
||||
|
||||
@@ -94,9 +94,9 @@ header.sub{position:relative; padding-top:80px;}
|
||||
.left_box .depth3 > li:hover > a, .left_box .depth3 > li.on > a{color:#ed1c24;}
|
||||
|
||||
.content_top{position:fixed; top:0; left:0; padding:0 30px; width:100%; height:50px; line-height:50px; background:#eee; border-top:1px solid #ddd; border-left:1px solid #ddd; z-index:100;}
|
||||
.content_top .path{}
|
||||
.content_top .path > li{display:inline-block;}
|
||||
.content_top .path > li a{display:block; height:50px; line-height:50px; padding-left:10px; font-size:12px; color:#999;}
|
||||
.content_top .path{margin:0; padding:0;}
|
||||
.content_top .path > li{display:inline-block; margin:0; padding:0;}
|
||||
.content_top .path > li a{display:block; height:50px; line-height:50px; padding-left:10px; font-size:12px; color:#999; text-decoration:none;}
|
||||
.content_top .path > li a:hover{color:#666;}
|
||||
.content_top .path > li a:before{content:'>'; margin-right:10px;}
|
||||
.content_top .path > li:first-child a:before{content:'';}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
|
||||
<%@ page import="java.lang.reflect.Method" %>
|
||||
<%@ page import="org.apache.commons.lang3.StringUtils" %>
|
||||
<%@ page import="com.eactive.eai.rms.common.login.SessionManager" %>
|
||||
<%!
|
||||
// 최소 HTML 이스케이프 (진단 페이지 XSS 방지)
|
||||
private String esc(String s) {
|
||||
if (s == null) return "";
|
||||
StringBuilder b = new StringBuilder(s.length() + 16);
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '&': b.append("&"); break;
|
||||
case '<': b.append("<"); break;
|
||||
case '>': b.append(">"); break;
|
||||
case '"': b.append("""); break;
|
||||
case '\'': b.append("'"); break;
|
||||
default: b.append(c);
|
||||
}
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
%>
|
||||
<%
|
||||
// 로그인 사용자만 접근 — real 모드에서 운영 키 암복호화 오라클이 되지 않도록 보호
|
||||
if (StringUtils.isBlank(SessionManager.getUserId(request))
|
||||
&& StringUtils.isBlank(SessionManager.getRoleIdString(request))) {
|
||||
response.sendRedirect(request.getContextPath() + "/loginForm.do");
|
||||
return;
|
||||
}
|
||||
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
request.setCharacterEncoding("utf-8");
|
||||
|
||||
String op = request.getParameter("op");
|
||||
String input = request.getParameter("input");
|
||||
boolean run = request.getParameter("run") != null;
|
||||
if (op == null) op = "encrypt";
|
||||
if (input == null) input = "";
|
||||
|
||||
String mode = "UNKNOWN";
|
||||
String revision = "-";
|
||||
String returnCode = "-";
|
||||
String result = "";
|
||||
String error = "";
|
||||
boolean loaded = false;
|
||||
|
||||
try {
|
||||
Class<?> cls = Class.forName("com.eactive.ext.djb.DamoManager");
|
||||
Object damo = cls.getMethod("getInstance").invoke(null);
|
||||
loaded = true;
|
||||
|
||||
boolean bypass = (Boolean) cls.getMethod("isBypassMode").invoke(damo);
|
||||
boolean fake = (Boolean) cls.getMethod("isFakeMode").invoke(damo);
|
||||
mode = bypass ? "BYPASS" : (fake ? "FAKE" : "REAL");
|
||||
revision = String.valueOf(cls.getMethod("getRevision").invoke(damo));
|
||||
returnCode = String.valueOf(cls.getMethod("getReturnCode").invoke(damo));
|
||||
|
||||
if (run) {
|
||||
if ("encrypt".equals(op)) {
|
||||
result = (String) cls.getMethod("encrypt", String.class).invoke(damo, input);
|
||||
} else if ("decrypt".equals(op)) {
|
||||
result = (String) cls.getMethod("decrypt", String.class).invoke(damo, input);
|
||||
} else if ("sha256".equals(op)) {
|
||||
int t = cls.getField("SHA256").getInt(null);
|
||||
result = (String) cls.getMethod("hash", int.class, String.class).invoke(damo, t, input);
|
||||
} else if ("sha512".equals(op)) {
|
||||
int t = cls.getField("SHA512").getInt(null);
|
||||
result = (String) cls.getMethod("hash", int.class, String.class).invoke(damo, t, input);
|
||||
} else {
|
||||
error = "알 수 없는 op: " + op;
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
error = "com.eactive.ext.djb.DamoManager 클래스를 찾을 수 없습니다. "
|
||||
+ "damo-manager.jar 를 WAS lib (운영) 또는 WEB-INF/lib 에 배포하세요.";
|
||||
} catch (Throwable t) {
|
||||
// require-real fail-fast(ExceptionInInitializerError) 또는 초기화 실패(NoClassDefFoundError) 포함
|
||||
Throwable c = (t.getCause() != null) ? t.getCause() : t;
|
||||
error = c.toString();
|
||||
if (c instanceof NoClassDefFoundError
|
||||
|| error.contains("require-real")
|
||||
|| error.contains("Could not initialize")) {
|
||||
error += " (※ -Ddamo-manager.require-real=true 인데 scpdb 가 없어 기동에 실패했을 수 있습니다. "
|
||||
+ "scpdb 를 갖추거나 옵션을 내리세요.)";
|
||||
}
|
||||
}
|
||||
|
||||
String badgeColor = "REAL".equals(mode) ? "#1a7f37"
|
||||
: "BYPASS".equals(mode) ? "#9a3412"
|
||||
: "FAKE".equals(mode) ? "#9a6700" : "#6e7781";
|
||||
%>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>DamoManager 테스트</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, "Malgun Gothic", sans-serif; margin: 24px; color: #24292f; }
|
||||
h1 { font-size: 20px; margin: 0 0 4px; }
|
||||
.sub { color: #6e7781; font-size: 13px; margin-bottom: 16px; }
|
||||
.badge { display:inline-block; color:#fff; padding:2px 10px; border-radius:12px; font-size:12px; font-weight:600; background:<%= badgeColor %>; }
|
||||
.meta { font-size:12px; color:#57606a; margin-left:8px; }
|
||||
.card { border:1px solid #d0d7de; border-radius:8px; padding:16px; max-width:760px; }
|
||||
label { display:block; font-size:13px; font-weight:600; margin:12px 0 4px; }
|
||||
textarea, select { width:100%; box-sizing:border-box; font-size:14px; padding:8px; border:1px solid #d0d7de; border-radius:6px; font-family:ui-monospace, Menlo, monospace; }
|
||||
textarea { height:90px; resize:vertical; }
|
||||
.row { display:flex; gap:12px; }
|
||||
.row > div { flex:1; }
|
||||
button { margin-top:14px; background:#1f6feb; color:#fff; border:0; border-radius:6px; padding:9px 18px; font-size:14px; font-weight:600; cursor:pointer; }
|
||||
.result { margin-top:16px; }
|
||||
.out { background:#f6f8fa; border:1px solid #d0d7de; border-radius:6px; padding:12px; white-space:pre-wrap; word-break:break-all; font-family:ui-monospace, Menlo, monospace; font-size:13px; min-height:20px; }
|
||||
.err { color:#cf222e; background:#fff5f5; border-color:#ffc1c1; }
|
||||
.hint { font-size:12px; color:#6e7781; margin-top:18px; line-height:1.6; }
|
||||
code { background:#eff1f3; padding:1px 5px; border-radius:4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DamoManager 테스트</h1>
|
||||
<div class="sub">웹에서 encrypt / decrypt / hash 동작을 즉시 확인 (eapim-admin)</div>
|
||||
|
||||
<div class="card">
|
||||
<div>
|
||||
<% if (loaded) { %>
|
||||
<span class="badge"><%= mode %> MODE</span>
|
||||
<span class="meta">revision=<%= esc(revision) %> · returnCode=<%= esc(returnCode) %></span>
|
||||
<% } else { %>
|
||||
<span class="badge" style="background:#cf222e;">NOT LOADED</span>
|
||||
<span class="meta">damo-manager.jar 미배포</span>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<%= request.getContextPath() %>/damo.jsp">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="op">동작</label>
|
||||
<select id="op" name="op">
|
||||
<option value="encrypt" <%= "encrypt".equals(op) ? "selected" : "" %>>encrypt (암호화)</option>
|
||||
<option value="decrypt" <%= "decrypt".equals(op) ? "selected" : "" %>>decrypt (복호화)</option>
|
||||
<option value="sha256" <%= "sha256".equals(op) ? "selected" : "" %>>hash SHA-256</option>
|
||||
<option value="sha512" <%= "sha512".equals(op) ? "selected" : "" %>>hash SHA-512</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="input">입력</label>
|
||||
<textarea id="input" name="input" placeholder="암호화/복호화/해시할 문자열"><%= esc(input) %></textarea>
|
||||
|
||||
<input type="hidden" name="run" value="1">
|
||||
<button type="submit">실행</button>
|
||||
</form>
|
||||
|
||||
<% if (run || error.length() > 0) { %>
|
||||
<div class="result">
|
||||
<label>결과<% if (run && error.length() == 0) { %> <span class="meta">(<%= esc(op) %>)</span><% } %></label>
|
||||
<div class="out <%= error.length() > 0 ? "err" : "" %>"><%= error.length() > 0 ? esc(error) : esc(result) %></div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="hint">
|
||||
모드 의미 — <b>BYPASS</b>: 무변환 원문 통과(<b>기본값</b>, 옵션 없음) · <b>FAKE</b>: Base64/JRE 해시(<code>-Ddamo-manager.enabled=true</code>, scpdb 없음) · <b>REAL</b>: penta scpdb 실 암복호화(<code>enabled=true</code> + scpdb).<br>
|
||||
BYPASS 모드에서는 encrypt/decrypt/hash 모두 입력을 그대로 반환합니다.<br>
|
||||
운영은 <code>-Ddamo-manager.require-real=true</code> 로 real 강제(아니면 기동 실패). CLI 로도 동일 확인: <code>java -jar damo-manager.jar enc <text></code>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,7 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<%@page import="org.apache.commons.lang3.StringUtils"%>
|
||||
<%@page import="com.eactive.eai.rms.common.login.SessionManager"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
@@ -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');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* Editor Content Styles (editor-content.css)
|
||||
*
|
||||
* Summernote 에디터로 작성된 HTML 콘텐츠의 공통 스타일
|
||||
* 관리자포탈/개발자포탈 양쪽에서 동일하게 사용
|
||||
*
|
||||
* 사용법:
|
||||
* - 관리자포탈: Summernote .note-editable에 .editor-content 클래스 추가
|
||||
* - 개발자포탈: 콘텐츠 wrapper에 .editor-content 클래스 추가
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @date 2025-12-30
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
CSS Reset + 기본 설정
|
||||
========================================================================== */
|
||||
|
||||
.editor-content {
|
||||
all: revert;
|
||||
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 400 !important;
|
||||
line-height: 1.8 !important;
|
||||
color: #1A1A2E !important;
|
||||
word-wrap: break-word;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
헤딩 (Headings)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content h1 {
|
||||
font-size: 20px !important;
|
||||
font-weight: 700 !important;
|
||||
margin: 32px 0 16px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content h1:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.editor-content h2 {
|
||||
font-size: 18px !important;
|
||||
font-weight: 700 !important;
|
||||
margin: 24px 0 8px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content h3 {
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
margin: 16px 0 8px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content h4,
|
||||
.editor-content h5,
|
||||
.editor-content h6 {
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
margin: 16px 0 8px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
단락 (Paragraphs)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content p {
|
||||
margin-bottom: 16px !important;
|
||||
line-height: 1.8 !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.editor-content p:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
리스트 (Lists) - 글로벌 reset 대응
|
||||
========================================================================== */
|
||||
|
||||
.editor-content ul {
|
||||
list-style-type: disc !important;
|
||||
padding-left: 32px !important;
|
||||
margin: 16px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content ol {
|
||||
list-style-type: decimal !important;
|
||||
padding-left: 32px !important;
|
||||
margin: 16px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content li {
|
||||
margin-bottom: 8px !important;
|
||||
line-height: 1.8 !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.editor-content li:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* 중첩 리스트 */
|
||||
.editor-content ul ul {
|
||||
list-style-type: circle !important;
|
||||
margin: 8px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content ul ul ul {
|
||||
list-style-type: square !important;
|
||||
}
|
||||
|
||||
.editor-content ol ol {
|
||||
list-style-type: lower-alpha !important;
|
||||
margin: 8px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content ol ol ol {
|
||||
list-style-type: lower-roman !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
테이블 (Tables)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content table {
|
||||
width: 100% !important;
|
||||
border-collapse: collapse !important;
|
||||
margin: 24px 0 !important;
|
||||
border: 1px solid #E2E8F0 !important;
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content th,
|
||||
.editor-content td {
|
||||
border: 1px solid #E2E8F0 !important;
|
||||
padding: 8px 16px !important;
|
||||
text-align: left !important;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
.editor-content th {
|
||||
background: #EFF6FF !important;
|
||||
font-weight: 600 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content tr:hover {
|
||||
background: #F8FAFC !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
링크 (Links)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content a {
|
||||
color: #0049b4 !important;
|
||||
text-decoration: underline !important;
|
||||
transition: color 0.3s ease !important;
|
||||
}
|
||||
|
||||
.editor-content a:hover {
|
||||
color: #003080 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
인용문 (Blockquote)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content blockquote {
|
||||
border-left: 4px solid #0049b4 !important;
|
||||
padding: 16px 24px !important;
|
||||
margin: 24px 0 !important;
|
||||
background: #F8FAFC !important;
|
||||
font-style: italic !important;
|
||||
color: #64748B !important;
|
||||
border-radius: 0 8px 8px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content blockquote p {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
이미지 (Images)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 8px !important;
|
||||
margin: 16px 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
코드 (Code)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content code {
|
||||
background: #F8FAFC !important;
|
||||
padding: 2px 6px !important;
|
||||
border-radius: 4px !important;
|
||||
font-family: 'Fira Code', 'Courier New', monospace !important;
|
||||
font-size: 0.9em !important;
|
||||
color: #0049b4 !important;
|
||||
}
|
||||
|
||||
.editor-content pre {
|
||||
background: #F8FAFC !important;
|
||||
border: 1px solid #E2E8F0 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 16px !important;
|
||||
overflow-x: auto !important;
|
||||
margin: 16px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content pre code {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
구분선 (Horizontal Rule)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content hr {
|
||||
border: none !important;
|
||||
border-top: 1px solid #E2E8F0 !important;
|
||||
margin: 32px 0 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
텍스트 강조 (Text Emphasis)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content strong,
|
||||
.editor-content b {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.editor-content em,
|
||||
.editor-content i {
|
||||
font-style: italic !important;
|
||||
}
|
||||
|
||||
.editor-content u {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.editor-content s,
|
||||
.editor-content strike,
|
||||
.editor-content del {
|
||||
text-decoration: line-through !important;
|
||||
}
|
||||
|
||||
.editor-content mark {
|
||||
background-color: #FEF3C7 !important;
|
||||
padding: 0 2px !important;
|
||||
}
|
||||
|
||||
.editor-content sub {
|
||||
vertical-align: sub !important;
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
.editor-content sup {
|
||||
vertical-align: super !important;
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
반응형 (Responsive)
|
||||
========================================================================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.editor-content {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content h1 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.editor-content h2 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.editor-content h3,
|
||||
.editor-content h4,
|
||||
.editor-content h5,
|
||||
.editor-content h6 {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content p,
|
||||
.editor-content li {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content table {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.editor-content th,
|
||||
.editor-content td {
|
||||
padding: 4px 8px !important;
|
||||
}
|
||||
|
||||
.editor-content ul,
|
||||
.editor-content ol {
|
||||
padding-left: 24px !important;
|
||||
}
|
||||
|
||||
.editor-content blockquote {
|
||||
padding: 8px 16px !important;
|
||||
}
|
||||
|
||||
.editor-content pre {
|
||||
padding: 8px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// OpenAPI 에디터 초기 상태 골격(빈 껍데기).
|
||||
// - specToData() 가 deepClone 하여 서버 spec 값으로 덮어쓰는 구조 기준값.
|
||||
// - 실제 데모/샘플 값은 두지 않는다(팝업은 항상 서버 spec 로드 후 렌더).
|
||||
// - docOptions 만 기능 기본값으로 유지(문서 미리보기 옵션).
|
||||
// locked:true = 게이트웨이 자동 주입(회색+자물쇠) / locked:false = 사용자 편집.
|
||||
|
||||
window.SAMPLE_DATA = (function () {
|
||||
function f() { return { value: '', locked: false }; }
|
||||
return {
|
||||
info: {
|
||||
title: f(),
|
||||
version: f(),
|
||||
summary: f(),
|
||||
description: f(),
|
||||
termsOfService: f(),
|
||||
contact: { name: f(), email: f(), url: f() },
|
||||
license: { name: f(), url: f() }
|
||||
},
|
||||
|
||||
tags: [],
|
||||
|
||||
externalDocs: { description: f(), url: f() },
|
||||
|
||||
servers: [],
|
||||
|
||||
serverVariables: [],
|
||||
|
||||
operation: {
|
||||
method: f(),
|
||||
path: f(),
|
||||
operationId: f(),
|
||||
summary: f(),
|
||||
description: f(),
|
||||
tags: { value: [], locked: false },
|
||||
deprecated: { value: false, locked: false }
|
||||
},
|
||||
|
||||
// 파라미터 (Path/Query/Header/Cookie)
|
||||
parameters: [],
|
||||
|
||||
// Request Body 스키마
|
||||
requestBody: {
|
||||
mediaType: 'application/json',
|
||||
required: { value: false, locked: false },
|
||||
schema: []
|
||||
},
|
||||
|
||||
// 응답 스키마 (상태코드 별)
|
||||
responses: {
|
||||
'200': { description: f(), schema: [] }
|
||||
},
|
||||
|
||||
// 보안 스킴
|
||||
securitySchemes: [],
|
||||
globalSecurity: [],
|
||||
|
||||
// 예제
|
||||
examples: {
|
||||
request: {},
|
||||
response: {}
|
||||
},
|
||||
|
||||
// 문서 옵션
|
||||
docOptions: {
|
||||
theme: 'light',
|
||||
lang: 'ko',
|
||||
includeExamples: true,
|
||||
tryItOut: true,
|
||||
defaultExpandDepth: -1,
|
||||
layout: 'BaseLayout',
|
||||
tagFilter: [],
|
||||
sideToc: true
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,103 @@
|
||||
/* DJB OpenAPI Editor POC — Tailwind 보강 커스텀 */
|
||||
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo",
|
||||
"Malgun Gothic", "Noto Sans KR", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* === 잠금 행/카드 좌측 띠 === */
|
||||
.locked-row td:first-child {
|
||||
position: relative;
|
||||
}
|
||||
.locked-row td:first-child::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
width: 3px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.locked-card {
|
||||
position: relative;
|
||||
background: linear-gradient(to right, #f8fafc 0%, #ffffff 4px) #ffffff;
|
||||
}
|
||||
.locked-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0; top: 8px; bottom: 8px;
|
||||
width: 3px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
/* === 스키마 테이블 — 가독성 보조 === */
|
||||
.schema-table th { white-space: nowrap; }
|
||||
.schema-table tbody tr:hover { background: #fafbfc; }
|
||||
.schema-table input,
|
||||
.schema-table select {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.schema-table input[type="text"] {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
/* === 스텝퍼 항목 hover === */
|
||||
.step-item:hover { background: #f8fafc; }
|
||||
|
||||
/* === 미리보기 패널: Swagger UI 크기 미세조정 === */
|
||||
#preview-swagger .swagger-ui {
|
||||
font-size: 13px;
|
||||
}
|
||||
#preview-swagger .swagger-ui .info { margin: 18px 0; }
|
||||
#preview-swagger .swagger-ui .info .title { font-size: 22px; }
|
||||
#preview-swagger .swagger-ui .scheme-container { padding: 12px 0; box-shadow: none; }
|
||||
#preview-swagger .swagger-ui .opblock { margin: 0 0 12px 0; }
|
||||
|
||||
/* === 다크 테마 (POC 시뮬레이션 — Swagger 자체 다크는 미지원이라 컨테이너만) === */
|
||||
body.theme-dark #preview-swagger { background: #1e293b; }
|
||||
|
||||
/* === 토스트 애니메이션 === */
|
||||
#toast { transition: opacity 0.2s; }
|
||||
|
||||
/* === Monaco 컨테이너가 hidden 일 때 layout 깨짐 방지 === */
|
||||
#preview-monaco {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* === Stepper 6단 grid 셀 좌우 라인 (마지막 셀 제외) === */
|
||||
#stepper li {
|
||||
position: relative;
|
||||
}
|
||||
#stepper li:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -8px;
|
||||
width: 16px;
|
||||
height: 1px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
/* === 보안 스킴 카드 — 잠금 표시 === */
|
||||
.locked-card {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
/* === 칩(태그) 미세 조정 === */
|
||||
.tag-chip { transition: background 0.15s; }
|
||||
|
||||
/* === Export menu fade === */
|
||||
#export-menu { animation: fadeIn 0.12s ease-out; }
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* === 1600px 기준 폼 패널 내부 가로 여유 확보 === */
|
||||
#form-content { max-width: 100%; }
|
||||
|
||||
/* === readonly input 위에서 텍스트 커서 안 보이게 === */
|
||||
input[readonly] { user-select: text; }
|
||||
@@ -184,8 +184,9 @@ function getEndTime(){
|
||||
</td>
|
||||
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.remoteAddress") %></th>
|
||||
<td><input type="text" name="searchRemoteAddress" value="${param.searchRemoteAddress}" style="width:100%"></td>
|
||||
<th style="width:180px;"></th>
|
||||
<th style="width:180px;"><%= localeMessage.getString("auditLogMan.parameters") %></th>
|
||||
<td>
|
||||
<input type="text" name="searchParameters" value="${param.searchParameters}" style="width:100%">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -54,10 +54,18 @@ function detail(key){
|
||||
$('#systemCode').val(data.systemCode);
|
||||
$('#logTypeText').val(data.logTypeText);
|
||||
$('#remoteAddress').val(data.remoteAddress);
|
||||
$('#logMsg').val(data.message);
|
||||
$('#logSubMsg').val(data.command);
|
||||
$('#parameters').val(data.parameters);
|
||||
|
||||
// 사유(message)는 값이 있을 때만 노출
|
||||
if (data.message) {
|
||||
$('#logMsg').val(data.message);
|
||||
$('#messageRow').show();
|
||||
} else {
|
||||
$('#logMsg').val('');
|
||||
$('#messageRow').hide();
|
||||
}
|
||||
|
||||
},
|
||||
error:function(e){
|
||||
alert(e.responseText);
|
||||
@@ -121,6 +129,9 @@ $(document).ready(function() {
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("auditLogMan.command") %></th><td><input type="text" id="logSubMsg" name="logSubMsg" readonly="readonly"/></td>
|
||||
</tr>
|
||||
<tr id="messageRow" style="display:none;">
|
||||
<th><%= localeMessage.getString("auditLogMan.message") %></th><td><textarea id="logMsg" name="logMsg" style="width:100%;height:80px" readonly="readonly"></textarea></td>
|
||||
</tr>
|
||||
<tr height="100px">
|
||||
<th><%= localeMessage.getString("auditLogMan.parameters") %></th><td><textarea id="parameters" name="parameters" style="width:100%;height:200px" readonly="readonly"></textarea></td>
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<%@ page import="java.util.List"%>
|
||||
<%@ page import="com.eactive.eai.rms.common.acl.sitemap.ui.SitemapNode"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<%!
|
||||
private void printNodeLabel(javax.servlet.jsp.JspWriter out, SitemapNode node) throws java.io.IOException {
|
||||
String url = node.getMenuUrl();
|
||||
boolean hasLink = url != null && !url.trim().isEmpty() && !"NAN".equalsIgnoreCase(url.trim());
|
||||
if (hasLink) {
|
||||
out.print("<a href=\"javascript:void(0);\" class=\"sitemap-name\" onclick=\"goPage('"
|
||||
+ escapeJs(url) + "','" + escapeJs(node.getMenuId()) + "');return false;\">"
|
||||
+ escapeHtml(node.getMenuName()) + "</a>");
|
||||
} else {
|
||||
out.print("<span class=\"sitemap-name\">" + escapeHtml(node.getMenuName()) + "</span>");
|
||||
}
|
||||
}
|
||||
|
||||
private void printSitemapNode(javax.servlet.jsp.JspWriter out, SitemapNode node) throws java.io.IOException {
|
||||
out.print("<li>");
|
||||
printNodeLabel(out, node);
|
||||
|
||||
List<SitemapNode> children = node.getChildren();
|
||||
if (children != null && !children.isEmpty()) {
|
||||
out.print("<ul>");
|
||||
for (SitemapNode child : children) {
|
||||
printSitemapNode(out, child);
|
||||
}
|
||||
out.print("</ul>");
|
||||
}
|
||||
out.print("</li>");
|
||||
}
|
||||
|
||||
private String escapeHtml(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||||
}
|
||||
|
||||
private String escapeJs(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return value.replace("\\", "\\\\").replace("'", "\\'");
|
||||
}
|
||||
%>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
|
||||
List<SitemapNode> sitemapTree = (List<SitemapNode>) request.getAttribute("sitemapTree");
|
||||
%>
|
||||
<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"/>
|
||||
<style>
|
||||
.sitemap-columns { display: flex; flex-wrap: wrap; gap: 40px; }
|
||||
.sitemap-column { min-width: 180px; }
|
||||
.sitemap-category { font-size: 1.1em; font-weight: bold; padding-bottom: 6px; margin-bottom: 8px; border-bottom: 2px solid #ccc; }
|
||||
.sitemap-tree, .sitemap-tree ul { list-style-type: disc; margin: 0; padding-left: 18px; }
|
||||
.sitemap-tree { padding-left: 0; list-style-type: none; }
|
||||
.sitemap-tree li { line-height: 1.8; }
|
||||
a.sitemap-name { text-decoration: none; cursor: pointer; }
|
||||
a.sitemap-name:hover { text-decoration: underline; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
function goPage(url, menuId) {
|
||||
var serviceType = sessionStorage["serviceType"];
|
||||
|
||||
var pageUrl = url;
|
||||
pageUrl += (pageUrl.indexOf("?") > -1 ? "&" : "?") + "menuId=" + menuId;
|
||||
pageUrl += "&serviceType=" + serviceType;
|
||||
|
||||
parent.leftFrame.location.href = '<c:url value="/leftMenu.do"/>?menuId=' + menuId + '&serviceType=' + serviceType;
|
||||
location.href = pageUrl;
|
||||
}
|
||||
</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="title">사이트맵</div> -->
|
||||
<div class="sitemap-columns">
|
||||
<%
|
||||
for (SitemapNode category : sitemapTree) {
|
||||
%>
|
||||
<div class="sitemap-column">
|
||||
<div class="sitemap-category">
|
||||
<%
|
||||
printNodeLabel(out, category);
|
||||
%>
|
||||
</div>
|
||||
<ul class="sitemap-tree">
|
||||
<%
|
||||
List<SitemapNode> children = category.getChildren();
|
||||
if (children != null) {
|
||||
for (SitemapNode child : children) {
|
||||
printSitemapNode(out, child);
|
||||
}
|
||||
}
|
||||
%>
|
||||
</ul>
|
||||
</div>
|
||||
<%
|
||||
}
|
||||
%>
|
||||
</div>
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -18,7 +18,17 @@
|
||||
<script language="javascript" >
|
||||
var url = '<c:url value="/common/acl/user/userMan.json" />';
|
||||
var url_view = '<c:url value="/common/acl/user/userMan.view" />';
|
||||
$(document).ready(function() {
|
||||
|
||||
function userStatusFormat (cellvalue){
|
||||
if ( cellvalue == '1' ){
|
||||
return '<%= localeMessage.getString("code.normal") %>';
|
||||
}else if ( cellvalue == '2' ) {
|
||||
return '<span style="color:red"><%= localeMessage.getString("code.lock") %></span>';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
$('#grid').jqGrid({
|
||||
datatype:"json",
|
||||
@@ -28,16 +38,20 @@ $(document).ready(function() {
|
||||
colNames:['<%= localeMessage.getString("login.userId") %>',
|
||||
'<%= localeMessage.getString("user.name") %>',
|
||||
'<%= localeMessage.getString("user.roleName") %>',
|
||||
'<%= localeMessage.getString("user.status") %>',
|
||||
'<%= localeMessage.getString("user.createOn") %>',
|
||||
'<%= localeMessage.getString("user.lastLoginYms") %>',
|
||||
'<%= localeMessage.getString("user.depart") %>',
|
||||
'<%= localeMessage.getString("user.teamName") %>',
|
||||
'<%= localeMessage.getString("user.position") %>'
|
||||
],
|
||||
colModel:[
|
||||
{ name : 'USERID' , align:'center' ,sortable:false },
|
||||
{ name : 'USERNAME' , align:'center' },
|
||||
{ name : 'ROLEIDNFINAME' , align:'center' },
|
||||
{ name : 'LASTAMNDYMS' , align:'center', formatter: timeStampFormat },
|
||||
{ name : 'USERID' , align:'center', width: 100, sortable:false },
|
||||
{ name : 'USERNAME' , align:'center', width: 100 },
|
||||
{ name : 'ROLEIDNFINAME' , align:'center', width: 300 },
|
||||
{ name : 'STATUS' , align:'center', width: 80, formatter: userStatusFormat },
|
||||
{ name : 'REGDYMS' , align:'center', width: 120, formatter: timeStampFormat },
|
||||
{ name : 'LASTLOGINYMS' , align:'center', width: 120, formatter: timeStampFormat },
|
||||
{ name : 'DVSNNAME' , align:'center', hidden: true },
|
||||
{ name : 'TEAMNAME' , align:'center', hidden: true },
|
||||
{ name : 'JOBTLNAME' , align:'center', hidden: true }
|
||||
|
||||
@@ -88,7 +88,8 @@ function detail(key){
|
||||
$(tag+"[name="+name+"]").val(data[name.toUpperCase()] ? data[name.toUpperCase()].trim():"");
|
||||
});
|
||||
|
||||
$("input[name=LASTAMNDYMS]").inputmask("9999-99-99 99:99:99",{'autoUnmask':true});
|
||||
$("input[name=REGDYMS]").inputmask("9999-99-99 99:99:99",{'autoUnmask':true});
|
||||
$("input[name=LASTLOGINYMS]").inputmask("9999-99-99 99:99:99",{'autoUnmask':true});
|
||||
/* $("input[name=SECONDMENTSTDT]").inputmask("9999-99-99",{'autoUnmask':true});
|
||||
$("input[name=SECONDMENTENDT]").inputmask("9999-99-99",{'autoUnmask':true}); */
|
||||
|
||||
@@ -216,6 +217,7 @@ $(document).ready(function() {
|
||||
dataType:"json",
|
||||
data:{cmd: 'UPDATE_PASSWORD', userId : $("input[name=userId]").val()},
|
||||
success:function(json){
|
||||
$('select[name="status"]').val("1")
|
||||
alert(json.result);
|
||||
},
|
||||
error:function(e){
|
||||
@@ -277,10 +279,10 @@ $(document).ready(function() {
|
||||
<table class="table_row" cellspacing="0">
|
||||
<input type="hidden" name="jobclcd">
|
||||
<tr>
|
||||
<th style="width:180px;"><%= localeMessage.getString("login.userId") %></th><td ><input type="text" name="userId"/> </td>
|
||||
<th style="width:180px;"><%= localeMessage.getString("login.userId") %> <span class=\"required-mark\">*</span></th><td ><input type="text" name="userId"/> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("user.name") %> </th><td><input type="text" name="userName" /></td>
|
||||
<th><%= localeMessage.getString("user.name") %> <span class=\"required-mark\">*</span></th><td><input type="text" name="userName" /></td>
|
||||
</tr>
|
||||
<tr class="datailT">
|
||||
<th><%= localeMessage.getString("user.auth") %> </th><td><input type="text" name="roleidnfiname" /></td>
|
||||
@@ -317,7 +319,6 @@ $(document).ready(function() {
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="status">
|
||||
<option value=""><%= localeMessage.getString("code.normal") %></option>
|
||||
<option value="1"><%= localeMessage.getString("code.normal") %></option>
|
||||
<option value="2"><%= localeMessage.getString("code.lock") %></option>
|
||||
</select>
|
||||
@@ -334,7 +335,10 @@ $(document).ready(function() {
|
||||
<th><%= localeMessage.getString("user.fieldAgentName") %> </th><td><input type="text" name="spotprxyname" /></td>
|
||||
</tr> --%>
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("user.createOn") %> </th><td><input type="text" name="LASTAMNDYMS" readonly/></td>
|
||||
<th><%= localeMessage.getString("user.createOn") %> </th><td><input type="text" name="REGDYMS" readonly/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("user.lastLoginYms") %> </th><td><input type="text" name="LASTLOGINYMS" readonly/></td>
|
||||
</tr>
|
||||
<%-- <tr>
|
||||
<th><%= localeMessage.getString("user.secondmentbrncd") %> </th><td><input type="text" name="SECONDMENTBRNCD" readonly/></td>
|
||||
|
||||
@@ -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> --%>
|
||||
@@ -527,6 +531,88 @@
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사유 입력 모달 - 확인 시 입력한 사유 문자열을 onConfirm(reason)으로 전달
|
||||
* @param {string} message - 안내 메시지
|
||||
* @param {object} options - (title, confirmText, cancelText, placeholder, required, onConfirm, onCancel)
|
||||
* required 기본 true (빈 사유 차단)
|
||||
*/
|
||||
function showReasonPrompt(message, options) {
|
||||
options = options || {};
|
||||
var title = options.title || '사유 입력';
|
||||
var confirmText = options.confirmText || '확인';
|
||||
var cancelText = options.cancelText || '취소';
|
||||
var placeholder = options.placeholder || '사유를 입력하세요.';
|
||||
var required = options.required !== false;
|
||||
var onConfirm = options.onConfirm || function(){};
|
||||
var onCancel = options.onCancel || function(){};
|
||||
|
||||
// 기존 모달 제거
|
||||
$('#commonReasonModal').remove();
|
||||
|
||||
var iconHtml = getAlertIcon('warning');
|
||||
|
||||
var modalHtml =
|
||||
'<div id="commonReasonModal" class="common-alert-overlay">' +
|
||||
'<div class="common-alert-modal alert-warning">' +
|
||||
'<div class="common-alert-header">' +
|
||||
'<span class="common-alert-icon">' + iconHtml + '</span>' +
|
||||
'<span class="common-alert-title">' + title + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="common-alert-body">' +
|
||||
'<p class="common-alert-message">' + message + '</p>' +
|
||||
'<textarea id="commonReasonInput" maxlength="200" placeholder="' + placeholder + '" style="width:100%;height:80px;margin-top:10px;box-sizing:border-box;font-size:13px;padding:8px;border:1px solid #ddd;border-radius:4px;resize:vertical;"></textarea>' +
|
||||
'<p id="commonReasonError" style="display:none;color:#f44336;font-size:12px;margin:6px 0 0;">사유를 입력해 주세요.</p>' +
|
||||
'</div>' +
|
||||
'<div class="common-alert-footer">' +
|
||||
'<button type="button" class="common-confirm-cancel-btn" id="commonReasonCancelBtn">' + cancelText + '</button>' +
|
||||
'<button type="button" class="common-alert-btn" id="commonReasonOkBtn">' + confirmText + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
$('body').append(modalHtml);
|
||||
|
||||
setTimeout(function() {
|
||||
$('#commonReasonModal').addClass('show');
|
||||
$('#commonReasonInput').focus();
|
||||
}, 10);
|
||||
|
||||
// 확인 버튼
|
||||
$('#commonReasonOkBtn').on('click', function() {
|
||||
var reason = $.trim($('#commonReasonInput').val());
|
||||
if (required && reason === '') {
|
||||
$('#commonReasonError').show();
|
||||
$('#commonReasonInput').focus();
|
||||
return;
|
||||
}
|
||||
closeCommonReason(function() { onConfirm(reason); });
|
||||
});
|
||||
|
||||
// 취소 버튼
|
||||
$('#commonReasonCancelBtn').on('click', function() {
|
||||
closeCommonReason(onCancel);
|
||||
});
|
||||
|
||||
// ESC 키로 취소
|
||||
$(document).on('keydown.commonReason', function(e) {
|
||||
if (e.keyCode === 27) {
|
||||
closeCommonReason(onCancel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeCommonReason(callback) {
|
||||
$('#commonReasonModal').removeClass('show');
|
||||
setTimeout(function() {
|
||||
$('#commonReasonModal').remove();
|
||||
$(document).off('keydown.commonReason');
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 공용 Alert 모달 스타일 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -91,7 +91,10 @@
|
||||
data: $('#loginForm').serialize(),
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.smsAuthRequired) {
|
||||
if (response.changePassword) {
|
||||
showChangeInitPassword(response);
|
||||
}
|
||||
else if (response.smsAuthRequired) {
|
||||
// SMS 인증 필요
|
||||
showSmsAuthModal(response);
|
||||
} else if (response.success) {
|
||||
@@ -108,6 +111,13 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showChangeInitPassword(data) {
|
||||
$('input[name="resetUserId"]').val($('input[name="userId"]').val());
|
||||
$('input[name="resetPassword"]').val($('input[name="password"]').val());
|
||||
$('#changeInitPassword').hide();
|
||||
$('#pwdChgModal').modal('show');
|
||||
}
|
||||
|
||||
// SMS 인증 모달 표시
|
||||
function showSmsAuthModal(data) {
|
||||
@@ -218,44 +228,39 @@
|
||||
clearInterval(resendTimer);
|
||||
$('#smsAuthModal').modal('hide');
|
||||
}
|
||||
function changePwd(){
|
||||
|
||||
function checkPwd(){
|
||||
if ($("input[name=resetUserId]").val() == null || $("input[name=resetUserId]").val().length == 0 ){
|
||||
alert("<%= localeMessage.getString("login.checkid") %>");
|
||||
$("input[name=resetUserId]").trigger("focus");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if ($("input[name=resetPassword]").val() == null || $("input[name=resetPassword]").val().trim().length == 0 ){
|
||||
alert("<%= localeMessage.getString("login.checkpwd1") %>");
|
||||
$("input[name=resetPassword]").trigger("focus");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if ($("input[name=changePassword]").val() == null || $("input[name=changePassword]").val().trim().length == 0 ){
|
||||
alert("<%= localeMessage.getString("login.checkpwd2") %>");
|
||||
$("input[name=changePassword]").trigger("focus");
|
||||
return ;
|
||||
return false;
|
||||
}
|
||||
if ($("input[name=confirmPassword]").val() == null || $("input[name=confirmPassword]").val().trim().length == 0 ){
|
||||
alert("<%= localeMessage.getString("login.checkpwd3") %>");
|
||||
return ;
|
||||
return false;
|
||||
}
|
||||
if ($("input[name=confirmPassword]").val() != $("input[name=confirmPassword]").val()){
|
||||
if ($("input[name=changePassword]").val() != $("input[name=confirmPassword]").val()){
|
||||
alert("<%= localeMessage.getString("login.checkpwd4") %>");
|
||||
$("input[name=confirmPassword]").trigger("focus");
|
||||
return ;
|
||||
return false;
|
||||
}
|
||||
if ($("input[name=resetUserId]").val() == $("input[name=changePassword]").val()){
|
||||
alert("<%= localeMessage.getString("login.checkpwd5") %>");
|
||||
$("input[name=changePassword]").trigger("focus");
|
||||
return ;
|
||||
return false;
|
||||
}
|
||||
|
||||
//if (idCheck.checked){
|
||||
// setCookie("key",id);
|
||||
//}
|
||||
<%-- $("form").attr("action","<c:url value="/changePassword.do"/>");
|
||||
//$("form").action = "<%=request.getContextPath()%>/changePassword.do";
|
||||
--%>
|
||||
$("form").submit();
|
||||
return true;
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
@@ -271,14 +276,22 @@
|
||||
});
|
||||
$("input[name=changePassword]").keydown(function(event){
|
||||
if ( event.which == 13 ) {
|
||||
changePwd();
|
||||
$('#modalLoginForm').submit();
|
||||
}
|
||||
});
|
||||
$("input[name=confirmPassword]").keydown(function(event){
|
||||
if ( event.which == 13 ) {
|
||||
changePwd();
|
||||
$('#modalLoginForm').submit();
|
||||
}
|
||||
});
|
||||
// Password Change 링크로 직접 열 때만 초기화 (JS에서 modal('show') 호출 시 relatedTarget 없음)
|
||||
$('#pwdChgModal').on('show.bs.modal', function(event) {
|
||||
if (event.relatedTarget) {
|
||||
$('#modalLoginForm')[0].reset();
|
||||
$('#changeInitPassword').show();
|
||||
}
|
||||
});
|
||||
|
||||
$("select[name=serviceType]").change(function(){
|
||||
fncPreMain();
|
||||
});
|
||||
@@ -286,9 +299,7 @@
|
||||
$("#btn_login").click(function(){
|
||||
fncPreMain();
|
||||
});
|
||||
$("#changePwd").click(function(){
|
||||
changePwd();
|
||||
});
|
||||
|
||||
$("#btn_sso_login").click(function(){
|
||||
fncSsoLogin();
|
||||
});
|
||||
@@ -353,10 +364,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>
|
||||
@@ -366,7 +377,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 비밀번호 변경 모달 -->
|
||||
<div class="modal fade" id="pwdChgModal" tabindex="-1" role="dialog" aria-labelledby="pwdChgModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
@@ -376,13 +387,15 @@
|
||||
<span aria-hidden="true">X</span>
|
||||
</button>
|
||||
</div>
|
||||
<form name="form" id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post">
|
||||
<form id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post" onsubmit="return checkPwd()" novalidate>
|
||||
<div class="modal-body">
|
||||
<div class="form-group d-flex">
|
||||
<input type="text" name="resetUserId" class="form-control rounded-left" placeholder="User Id" required>
|
||||
</div>
|
||||
<div class="form-group d-flex">
|
||||
<input type="password" name="resetPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderCurrentPassword") %>" autocomplete="off" required>
|
||||
<div id="changeInitPassword">
|
||||
<div class="form-group d-flex">
|
||||
<input type="text" name="resetUserId" class="form-control rounded-left" placeholder="User Id" required>
|
||||
</div>
|
||||
<div class="form-group d-flex">
|
||||
<input type="password" name="resetPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderCurrentPassword") %>" autocomplete="off" required>
|
||||
</div>
|
||||
</div>
|
||||
<p><%= localeMessage.getString("login.toChangePasswordMessage") %></p>
|
||||
<div class="form-group d-flex">
|
||||
@@ -391,9 +404,12 @@
|
||||
<div class="form-group d-flex">
|
||||
<input type="password" name="confirmPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderConfirmationPassword") %>" autocomplete="off" required>
|
||||
</div>
|
||||
<span style="font-size:0.8em; color:red">
|
||||
※ 7글자 이상 & 영문/숫자/특수문자 2종류 이상
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<input type="button" id ="changePwd" class="form-control btn btn-primary rounded submit px-3" value="변경">
|
||||
<button type="submit" id="changePwd" class="form-control btn btn-primary rounded submit px-3">변경</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -52,6 +52,13 @@
|
||||
var urlParam = window.document.location.search;
|
||||
var mod = "";
|
||||
var strServiceType = "<%=request.getParameter("serviceType")%>";
|
||||
function getServiceType() {
|
||||
var serviceType = sessionStorage.getItem("serviceType");
|
||||
if (!serviceType || serviceType == "undefined" || serviceType == "null") {
|
||||
serviceType = localStorage.getItem("serviceType") || "";
|
||||
}
|
||||
return serviceType;
|
||||
}
|
||||
if(urlParam != ""){
|
||||
if(urlParam.indexOf("mod=") > -1){
|
||||
mod = urlParam.substring((urlParam.indexOf("mod=") + 4), urlParam.lastIndexOf("&"));
|
||||
@@ -67,15 +74,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$(document).ready(function() {
|
||||
var url = '<%=topPage%>';
|
||||
if (url.indexOf("?")>=0){
|
||||
url = url + "&serviceType="+ sessionStorage["serviceType"];
|
||||
}else{
|
||||
url = url + "?serviceType="+ sessionStorage["serviceType"];
|
||||
var serviceType = getServiceType();
|
||||
if (serviceType) {
|
||||
if (url.indexOf("?")>=0){
|
||||
url = url + "&serviceType="+ serviceType;
|
||||
}else{
|
||||
url = url + "?serviceType="+ serviceType;
|
||||
}
|
||||
}
|
||||
|
||||
$("#topFrame").attr('src',url);
|
||||
|
||||
$("#topFrame").attr('src',url);
|
||||
|
||||
$(".topMenu").load(function(){ // iframe이 모두 load된후 제어
|
||||
$(".topMenu").contents().find('.gnb').on("mouseenter", function(e){
|
||||
@@ -99,6 +109,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>
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
<%@page import="com.eactive.eai.rms.common.util.StringUtils" %>
|
||||
<%@page import="com.eactive.eai.rms.common.datasource.DataSourceType" %>
|
||||
<%@page import="com.eactive.eai.rms.common.datasource.DataSourceTypeManager" %>
|
||||
<%@page import="com.eactive.eai.common.util.ApplicationContextProvider" %>
|
||||
<%@page import="com.eactive.eai.rms.data.entity.man.role.RoleMenuAuthService" %>
|
||||
<%@ 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" %>
|
||||
@@ -16,6 +18,12 @@
|
||||
<%
|
||||
String serviceKey = SessionManager.getServiceTypeKey(request);
|
||||
String serviceText = DataSourceTypeManager.getDataSourceType(serviceKey).getText();
|
||||
|
||||
final String SITEMAP_MENU_ID = "0510013";
|
||||
RoleMenuAuthService roleMenuAuthService = ApplicationContextProvider.getContext().getBean(RoleMenuAuthService.class);
|
||||
String sitemapMenuAuth = roleMenuAuthService.getRoleList(SessionManager.getUserId(request), SITEMAP_MENU_ID, serviceKey);
|
||||
boolean showSitemapLink = "R".equals(sitemapMenuAuth) || "W".equals(sitemapMenuAuth);
|
||||
|
||||
String roleScreenId = (String) request.getAttribute("roleScreenId");
|
||||
String mainPage = (String) request.getAttribute("mainPage");
|
||||
String menuId = (String) request.getAttribute("menuId");
|
||||
@@ -240,6 +248,12 @@
|
||||
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="<c:url value="/css/web_ui.css"/>"/>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="<c:url value="/css/theme_${themeColor}.css"/>"/>
|
||||
<style>
|
||||
.topmenu_box .sitemap-link { display: inline-block; width: 80px; height: 80px; line-height: 80px; box-sizing: border-box; font-size: 14px; color: #666; text-align: center; opacity: 0.7; transition: all 0.3s; margin-top: 0px; }
|
||||
.topmenu_box .sitemap-link:hover { opacity: 1; }
|
||||
.topmenu_box .logout-link { line-height: 80px; margin-top:0; }
|
||||
.topmenu_box > ul li:last-child > a:before { display: none; }
|
||||
</style>
|
||||
<script language="javascript" src="<c:url value="/js/jquery-1.12.1.min.js"/>"></script>
|
||||
<script language="javascript" src="<c:url value="/js/prefixfree.min.js"/>"></script>
|
||||
<script language="javascript" src="<c:url value="/js/jquery.cookie.js"/>"></script>
|
||||
@@ -303,11 +317,11 @@
|
||||
}
|
||||
|
||||
//왼쪽메뉴, 메인 페이지 이동
|
||||
function goPage2(page1, page2, page2_id) {
|
||||
function goPage2(page1, page2, page2_id) {
|
||||
parent.leftFrame.location.href = goNavUrl(page1);
|
||||
parent.mainFrame.location.href = goNavUrl(page2);
|
||||
}
|
||||
|
||||
|
||||
var sessionAjax = createAjaxRequest();
|
||||
|
||||
function getAjaxData() {
|
||||
@@ -529,6 +543,7 @@
|
||||
console.log("selectLocaleValue : " + selectLocaleValue);
|
||||
parent.changeLocale($(this).val());
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
$(function() {
|
||||
@@ -551,21 +566,28 @@
|
||||
</h1><!-- /monitoring/images/top_logo.png -->
|
||||
<div class="topmenu_box">
|
||||
<ul>
|
||||
<li style="width:240px;" id="newDashboardShow">
|
||||
<a style="width:240px;"
|
||||
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>)</span>
|
||||
<li style="width:240px;">
|
||||
<a style="width:240px; cursor: default"
|
||||
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>
|
||||
<% } %>
|
||||
</a></li>
|
||||
<li>
|
||||
<select id="selectLocale" name="locale" style="margin-top: 20px; width:80px">
|
||||
<li style="width:100px; line-height:80px; text-align:center;">
|
||||
<select id="selectLocale" name="locale" style="width:60px; padding: 2px; text-align:center;">
|
||||
<option value="en">English</option>
|
||||
<option value="ko">한글</option>
|
||||
</select>
|
||||
</li>
|
||||
<li onclick="logout()"><a href="#" class=""><img src="<c:url value="/img/icon_logout.png"/>"
|
||||
<% if (showSitemapLink) { %>
|
||||
<li>
|
||||
<a href="<c:url value="/common/acl/sitemap/sitemapMan.view"><c:param name="menuId" value="<%=SITEMAP_MENU_ID%>"/><c:param name="serviceType" value="<%=serviceKey%>"/></c:url>" target="mainFrame" class="sitemap-link">
|
||||
<%= localeMessage.getString("screen.sitemap") %>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
<li onclick="logout()"><a href="#" class="logout-link"><img src="<c:url value="/img/icon_logout.png"/>"
|
||||
alt=""/><%= localeMessage.getString("screen.logout") %>
|
||||
</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/**
|
||||
* 문자열 마스킹 처리를 위한 유틸리티
|
||||
*
|
||||
* 보안아키텍처 표준 마스킹 규칙(Java MaskingUtils)과 동일한 결과를 내도록 구현한다.
|
||||
* 이름 2자: 뒤 1자리(가나→가*) / 3자 이상: 앞뒤 제외 가운데 전체(가나다라→가**라)
|
||||
* 이메일 아이디 앞2·뒤2 마스킹, 4자 이하 전체(test→****, test123→**st1**)
|
||||
* 전화 2·3번째 세그먼트 각각 뒤 2자리(010-1234-1234→010-12**-12**)
|
||||
*/
|
||||
const StringMaskingUtil = {
|
||||
|
||||
@@ -8,41 +13,51 @@ const StringMaskingUtil = {
|
||||
return str != null && str !== '';
|
||||
},
|
||||
|
||||
// 이름 마스킹 처리
|
||||
// '*' 반복 생성
|
||||
_stars: function(count) {
|
||||
return count > 0 ? '*'.repeat(count) : '';
|
||||
},
|
||||
|
||||
// 세그먼트 뒤 count 자리 마스킹
|
||||
_maskSegmentTail: function(segment, count) {
|
||||
if (segment.length <= count) {
|
||||
return this._stars(segment.length);
|
||||
}
|
||||
return segment.substring(0, segment.length - count) + this._stars(count);
|
||||
},
|
||||
|
||||
// 이름 마스킹 처리 (2자: 뒤 1자리, 3자 이상: 앞뒤 제외 가운데 전체)
|
||||
maskName: function(name) {
|
||||
if (!this.isValidString(name)) {
|
||||
return name;
|
||||
}
|
||||
return name.length > 1 ?
|
||||
name.substring(0, name.length - 1) + "*" :
|
||||
name;
|
||||
const len = name.length;
|
||||
if (len === 1) {
|
||||
return name;
|
||||
}
|
||||
if (len === 2) {
|
||||
return name.charAt(0) + '*';
|
||||
}
|
||||
return name.charAt(0) + this._stars(len - 2) + name.charAt(len - 1);
|
||||
},
|
||||
|
||||
// 이메일 마스킹 처리
|
||||
// 이메일 마스킹 처리 (아이디 앞2·뒤2, 4자 이하 전체)
|
||||
maskEmail: function(email) {
|
||||
if (!this.isValidString(email) || !email.includes('@')) {
|
||||
return email;
|
||||
}
|
||||
|
||||
const parts = email.split('@');
|
||||
if (parts.length !== 2) {
|
||||
return email;
|
||||
const atIdx = email.indexOf('@');
|
||||
const local = email.substring(0, atIdx);
|
||||
const domain = email.substring(atIdx);
|
||||
|
||||
if (local.length <= 4) {
|
||||
return this._stars(local.length) + domain;
|
||||
}
|
||||
|
||||
const localPart = parts[0];
|
||||
let maskedLocal;
|
||||
|
||||
if (localPart.length <= 3) {
|
||||
maskedLocal = localPart.substring(0, 1) +
|
||||
'*'.repeat(localPart.length - 1);
|
||||
} else {
|
||||
maskedLocal = localPart.substring(0, localPart.length - 3) + '***';
|
||||
}
|
||||
|
||||
return maskedLocal + '@' + parts[1];
|
||||
return this._stars(2) + local.substring(2, local.length - 2) + this._stars(2) + domain;
|
||||
},
|
||||
|
||||
// 휴대폰 번호 마스킹 처리
|
||||
// 휴대폰/전화 번호 마스킹 처리 (2·3번째 세그먼트 각각 뒤 2자리)
|
||||
maskMobileNumber: function(number) {
|
||||
if (!this.isValidString(number)) {
|
||||
return number;
|
||||
@@ -50,8 +65,10 @@ const StringMaskingUtil = {
|
||||
|
||||
const parts = number.split('-');
|
||||
if (parts.length === 3) {
|
||||
return parts[0] + '-****-' + parts[2];
|
||||
return parts[0] + '-' +
|
||||
this._maskSegmentTail(parts[1], 2) + '-' +
|
||||
this._maskSegmentTail(parts[2], 2);
|
||||
}
|
||||
return number;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,8 +82,10 @@
|
||||
apiDesc: api.eaiSvcDesc
|
||||
};
|
||||
});
|
||||
// addJSONData는 datatype:"local" 그리드의 p.data를 채우지 않아, 헤더 클릭 정렬 시 p.data(빈 배열) 기준으로 다시 그려지며 행이 사라짐.
|
||||
// setGridParam({data:...}).trigger('reloadGrid')로 채워야 정렬이 정상 동작함.
|
||||
$("#selectedApiGrid").jqGrid("clearGridData");
|
||||
$("#selectedApiGrid")[0].addJSONData(gridData);
|
||||
$("#selectedApiGrid").jqGrid('setGridParam', {data: gridData}).trigger('reloadGrid');
|
||||
|
||||
var data = json;
|
||||
$("#btn_gen_id").hide();
|
||||
@@ -133,15 +135,14 @@
|
||||
var selectedApiGrid = $("#selectedApiGrid");
|
||||
selectedApiGrid.jqGrid({
|
||||
datatype: "local",
|
||||
gridview: true, // 성능 향상
|
||||
rownumbers: true, // 가상 인덱스
|
||||
rownumbers: true, // 가상 인덱스
|
||||
rownumWidth: 40,
|
||||
colNames: ['API ID', 'API 이름'],
|
||||
colModel: [
|
||||
{name: 'apiId', index: 'apiId', width: 100, align: 'center'},
|
||||
{name: 'apiDesc', index: 'apiDesc', width: 100, align: 'center'}
|
||||
],
|
||||
rowNum: -1,
|
||||
rowNum: 9999,
|
||||
// rowList: [10, 20, 30],
|
||||
// pager: '#selectedApiGridPager',
|
||||
// sortname: 'apiId',
|
||||
@@ -156,8 +157,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
selectedApiGrid.jqGrid('setLabel', 'rn', 'No'); // 가상 인덱스 ColNames
|
||||
|
||||
selectedApiGrid.jqGrid('setLabel', 'rn', 'No'); // 가상 인덱스 ColNames
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
@@ -450,8 +451,8 @@
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영
|
||||
</button>
|
||||
<!-- <button type="button" class="cssbtn" id="btn_sync_portal" level="W" status="DETAIL"><i class="material-icons">sync</i> 개발자포탈 정보 반영
|
||||
</button> -->
|
||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %>
|
||||
</button>
|
||||
<button type="button" class="cssbtn" id="btn_modify" level="W" status="DETAIL,NEW"><i
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
var url = '<c:url value="/onl/admin/authserver/scopeMan.json" />';
|
||||
var url_view = '<c:url value="/onl/admin/authserver/scopeMan.view" />';
|
||||
var api_url_view = '<c:url value="/onl/apim/apigroup/apiGroupMan.view" />';
|
||||
var mapping_url = '<c:url value="/onl/admin/authserver/apiScopeMan.json" />';
|
||||
|
||||
var isDetail = false;
|
||||
function isValid(){
|
||||
@@ -65,9 +64,9 @@ function mappingInfo(key){
|
||||
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url: mapping_url,
|
||||
url: url,
|
||||
dataType: "json",
|
||||
data: {cmd: 'LIST', searchScopeId : key},
|
||||
data: {cmd: 'API_LIST', searchScopeId : key},
|
||||
success: function(json){
|
||||
var mappingData = json.rows;
|
||||
|
||||
@@ -131,45 +130,6 @@ function init(key, callback){
|
||||
}
|
||||
}
|
||||
|
||||
function saveApiGridData(scopeId, returnUrl) {
|
||||
var postData = [];
|
||||
var rows = $("#selectedApiGrid").jqGrid('getRowData');
|
||||
|
||||
$.each(rows, function(index, item) {
|
||||
item.scopeId = scopeId;
|
||||
});
|
||||
|
||||
postData.push({
|
||||
name: "apiList",
|
||||
value: JSON.stringify(rows)
|
||||
});
|
||||
|
||||
postData.push({ name: "cmd" , value:"INSERT_APILIST" });
|
||||
postData.push({ name: "scopeId" , value:scopeId });
|
||||
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url:mapping_url,
|
||||
data: postData,
|
||||
beforeSend: function() {
|
||||
$("[id^='btn_']").prop("disabled", true);
|
||||
$("#btn_modify").text("처리중 . . . . .");
|
||||
},
|
||||
success:function(args){
|
||||
alert("저장 되었습니다.");
|
||||
|
||||
goNav(returnUrl);//LIST로 이동
|
||||
},
|
||||
error:function(e){
|
||||
alert(e.responseText);
|
||||
},
|
||||
complete: function() {
|
||||
$("[id^='btn_']").prop("disabled", false);
|
||||
$("#btn_modify").text("수정");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
var key ="${param.scopeId}";
|
||||
@@ -182,28 +142,39 @@ $(document).ready(function() {
|
||||
if (!isValid()){
|
||||
return;
|
||||
}
|
||||
var scopeId = $('input[name=scopeId]').val();
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
|
||||
var rows = $("#selectedApiGrid").jqGrid('getRowData');
|
||||
$.each(rows, function(index, item) {
|
||||
item.scopeId = scopeId;
|
||||
});
|
||||
postData.push({ name: "apiList" , value: JSON.stringify(rows) });
|
||||
|
||||
if (isDetail){
|
||||
postData.push({ name: "cmd" , value:"UPDATE"});
|
||||
}else{
|
||||
postData.push({ name: "cmd" , value:"INSERT"});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url:url,
|
||||
data:postData,
|
||||
beforeSend: function() {
|
||||
$("[id^='btn_']").prop("disabled", true);
|
||||
$("#btn_modify").text("처리중 . . . . .");
|
||||
},
|
||||
success:function(args){
|
||||
alert("SCOPE 정보가 저장 되었습니다. 이어서 API 리스트를 저장합니다.");
|
||||
|
||||
if(!isDetail) { // INSERT 일 경우 ScopeId를 업데이트
|
||||
key = $('input[name=scopeId]').val();
|
||||
}
|
||||
saveApiGridData(key, returnUrl); // API LIST 저장.
|
||||
|
||||
|
||||
alert("저장 되었습니다.");
|
||||
goNav(returnUrl);//LIST로 이동
|
||||
},
|
||||
error:function(e){
|
||||
alert(e.responseText);
|
||||
},
|
||||
complete: function() {
|
||||
$("[id^='btn_']").prop("disabled", false);
|
||||
$("#btn_modify").text("수정");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -250,6 +250,29 @@ var url_view = '<c:url value="/onl/admin/common/propertyMan.view" />';
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
$("#btn_pop_encrypt").click(function() {
|
||||
var prpty2Val = $("input[name=prpty2Val]").val();
|
||||
if (prpty2Val == '') {
|
||||
alert("프라퍼티 값을 입력하세요");
|
||||
return;
|
||||
}
|
||||
var postData = {
|
||||
cmd : "ENCRYPT",
|
||||
prpty2Val : prpty2Val,
|
||||
};
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url : url,
|
||||
data : postData,
|
||||
success : function(args) {
|
||||
$("input[name=prpty2Val]").val(args);
|
||||
},
|
||||
error : function(e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
buttonControl(key);
|
||||
titleControl(key);
|
||||
@@ -289,12 +312,17 @@ var url_view = '<c:url value="/onl/admin/common/propertyMan.view" />';
|
||||
<table class="table_row" cellspacing="0">
|
||||
<tr>
|
||||
<th style="width:20%;"><%= localeMessage.getString("propertyDetail.propertyKey") %></th>
|
||||
<td><input type="text" name="prptyName" style="width:calc(100% - 85px);" /> <!-- <img src="<c:url value="/img/btn_pop_input.png"/>" id="btn_pop_input" /></td> -->
|
||||
<button type="button" class="cssbtn smallBtn2" id="btn_pop_input" style="vertical-align:middle; font-weight:bold;"><i class="material-icons">input</i><%= localeMessage.getString("button.input") %></button>
|
||||
<td>
|
||||
<input type="text" name="prptyName" style="width:calc(100% - 85px);" /> <!-- <img src="<c:url value="/img/btn_pop_input.png"/>" id="btn_pop_input" /></td> -->
|
||||
<button type="button" class="cssbtn smallBtn2" id="btn_pop_input" style="vertical-align:middle; font-weight:bold;"><i class="material-icons">input</i><%= localeMessage.getString("button.input") %></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("propertyDetail.propertyValue") %></th>
|
||||
<td><input type="text" name="prpty2Val"/></td>
|
||||
<th style="width:20%;"><%= localeMessage.getString("propertyDetail.propertyValue") %></th>
|
||||
<td>
|
||||
<input type="text" name="prpty2Val" style="width:calc(100% - 85px);"/>
|
||||
<button type="button" class="cssbtn smallBtn2" id="btn_pop_encrypt" style="vertical-align:middle; font-weight:bold;"><i class="material-icons">lock</i><%= localeMessage.getString("button.encode") %></button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- grid -->
|
||||
|
||||
@@ -25,7 +25,7 @@ $(document).ready(function() {
|
||||
datatype:"json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val()},
|
||||
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()},
|
||||
colNames:['<%= localeMessage.getString("infAdpConMan.adaNm") %>',
|
||||
'<%= localeMessage.getString("infAdpConMan.adaDes") %>',
|
||||
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
|
||||
@@ -62,6 +62,7 @@ $(document).ready(function() {
|
||||
url2 += '&menuId='+'${param.menuId}';
|
||||
//검색값
|
||||
url2 += '&searchName='+$("input[name=searchName]").val();
|
||||
url2 += '&searchUseYn='+$("select[name=searchUseYn]").val();
|
||||
//key값
|
||||
url2 += '&name='+name;
|
||||
goNav(url2);
|
||||
@@ -80,7 +81,7 @@ $(document).ready(function() {
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
|
||||
$("#btn_search").click(function(){
|
||||
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val()}, page:1 }).trigger("reloadGrid");
|
||||
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()}, page:1 }).trigger("reloadGrid");
|
||||
});
|
||||
$("#btn_new").click(function(){
|
||||
var url2 = url_view;
|
||||
@@ -126,6 +127,16 @@ $(document).ready(function() {
|
||||
<tr>
|
||||
<th style="width:180px;"><%= localeMessage.getString("infAdpConMan.adaNm") %></th>
|
||||
<td><input type="text" name="searchName" value="${param.searchName}"></td>
|
||||
<th style="width:180px;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="searchUseYn">
|
||||
<option value="">전체</option>
|
||||
<option value="1">사용함</option>
|
||||
<option value="0">사용안함</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -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,150 @@
|
||||
<%@ 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(), searchUseYn: $('select[name=searchUseYn]').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();
|
||||
url2 += '&searchUseYn='+$("select[name=searchUseYn]").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(), searchUseYn: $('select[name=searchUseYn]').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=';
|
||||
url2 += '&searchUseYn='+$('select[name=searchUseYn]').val();
|
||||
|
||||
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>
|
||||
<th style="width:180px;">사용여부</th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="searchUseYn">
|
||||
<option value="">전체</option>
|
||||
<option value="1">사용함</option>
|
||||
<option value="0">사용안함</option>
|
||||
</select>
|
||||
</div>
|
||||
</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,512 @@
|
||||
<%@ 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) {
|
||||
console.log('mod', 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>
|
||||
</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>
|
||||
@@ -47,27 +47,40 @@ function groupNameFormat(cellvalue, options, rowObject) {
|
||||
return strVal;
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$(document).ready(function() {
|
||||
$("input[name=searchStartYYYYMMDD],input[name=searchEndYYYYMMDD]").inputmask("yyyy-mm-dd",{'autoUnmask':true});
|
||||
|
||||
$("input[name=searchStartTime],input[name=searchEndTime]").inputmask("hh:mm:ss",{'autoUnmask':true});
|
||||
|
||||
$("input[name=searchStartYYYYMMDD]").each(function(){
|
||||
if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
|
||||
$(this).val(getToday());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$("input[name=searchEndYYYYMMDD]").each(function(){
|
||||
if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
|
||||
$(this).val(getToday());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
|
||||
|
||||
$("input[name=searchStartTime]").each(function(){
|
||||
if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
|
||||
$(this).val("000000");
|
||||
}
|
||||
});
|
||||
|
||||
$("input[name=searchEndTime]").each(function(){
|
||||
if ($(this).val() == undefined || $(this).val() == null || $(this).val() == ""){
|
||||
$(this).val("235959");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
|
||||
var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi,"");
|
||||
var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi,"");
|
||||
|
||||
|
||||
gridPostData["searchStartDate"] = start;
|
||||
gridPostData["searchEndDate"] = end;
|
||||
|
||||
@@ -132,18 +145,20 @@ $(document).ready(function() {
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
|
||||
$("#btn_search").click(function(){
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
|
||||
var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi,"");
|
||||
var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi,"");
|
||||
|
||||
if(start > end){
|
||||
var startTime = $("input[name=searchStartTime]").val();
|
||||
var endTime = $("input[name=searchEndTime]").val();
|
||||
|
||||
if((start + startTime) > (end + endTime)){
|
||||
alert("조회기간을 확인해주세요.");
|
||||
return false;
|
||||
}
|
||||
$("input[name=searchStartDate]").val(start);
|
||||
$("input[name=searchEndDate]").val(end);
|
||||
|
||||
|
||||
postData["searchStartDate"] = start;
|
||||
postData["searchEndDate"] = end;
|
||||
|
||||
@@ -197,14 +212,24 @@ $(document).ready(function() {
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.date") %></th>
|
||||
<td>
|
||||
<td colspan="5">
|
||||
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:80px; border:1px solid #ebebec;">
|
||||
~
|
||||
<input type="text" name="searchStartTime" value="" size="8" style="width:70px; border:1px solid #ebebec;">
|
||||
~
|
||||
<input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:80px; border:1px solid #ebebec;">
|
||||
<input type="text" name="searchEndTime" value="" size="8" style="width:70px; border:1px solid #ebebec;">
|
||||
<input type="hidden" name="searchStartDate" value="" style="width:0px;">
|
||||
<input type="hidden" name="searchEndDate" value="" style="width:0px;">
|
||||
</td>
|
||||
</tr>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.instNm") %></th>
|
||||
<td><input type="text" name="searchInstanceName" value="${param.searchInstanceName}"></td>
|
||||
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.eaiSvcCd") %></th>
|
||||
<td><input type="text" name="searchApiName" value="${param.searchApiName}"></td>
|
||||
<th style="width:180px;"><%= localeMessage.getString("infConHstMan.adpGrpNm") %></th>
|
||||
<td><input type="text" name="searchAdapterGroupName" value="${param.searchAdapterGroupName}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
@@ -88,7 +88,7 @@ $(document).ready(function() {
|
||||
datatype:"json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData : { cmd : 'LIST', searchGroupName: $('input[name=searchGroupName]').val()},
|
||||
postData : { cmd : 'LIST', searchGroupName: $('input[name=searchGroupName]').val(), searchUseYn: $('select[name=searchUseYn]').val()},
|
||||
colNames:['그룹 ID',
|
||||
'그룹명',
|
||||
'초당 임계치',
|
||||
@@ -125,6 +125,7 @@ $(document).ready(function() {
|
||||
url2 += '&menuId='+'${param.menuId}';
|
||||
//검색값
|
||||
url2 += '&searchGroupName='+$("input[name=searchGroupName]").val();
|
||||
url2 += '&searchUseYn='+$("select[name=searchUseYn]").val();
|
||||
//key값
|
||||
url2 += '&groupId='+groupId;
|
||||
goNav(url2);
|
||||
@@ -143,7 +144,7 @@ $(document).ready(function() {
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
|
||||
$("#btn_search").click(function(){
|
||||
$("#grid").setGridParam({ postData: { searchGroupName: $('input[name=searchGroupName]').val()}, page:1 }).trigger("reloadGrid");
|
||||
$("#grid").setGridParam({ postData: { searchGroupName: $('input[name=searchGroupName]').val(), searchUseYn: $('select[name=searchUseYn]').val()}, page:1 }).trigger("reloadGrid");
|
||||
});
|
||||
$("#btn_new").click(function(){
|
||||
var url2 = url_view;
|
||||
@@ -188,6 +189,16 @@ $(document).ready(function() {
|
||||
<tr>
|
||||
<th style="width:180px;">그룹명</th>
|
||||
<td><input type="text" name="searchGroupName" value="${param.searchGroupName}"></td>
|
||||
<th style="width:180px;">사용여부</th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="searchUseYn">
|
||||
<option value="">전체</option>
|
||||
<option value="1">사용함</option>
|
||||
<option value="0">사용안함</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -25,7 +25,7 @@ $(document).ready(function() {
|
||||
datatype:"json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val()},
|
||||
postData : { cmd : 'LIST', searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()},
|
||||
colNames:['<%= localeMessage.getString("infIfConMan.ifNm") %>',
|
||||
'<%= localeMessage.getString("infIfConMan.ifDesc") %>',
|
||||
'<%= localeMessage.getString("infAdpConMan.thrPerSecond") %>',
|
||||
@@ -62,6 +62,7 @@ $(document).ready(function() {
|
||||
url2 += '&menuId='+'${param.menuId}';
|
||||
//검색값
|
||||
url2 += '&searchName='+$("input[name=searchName]").val();
|
||||
url2 += '&searchUseYn='+$("select[name=searchUseYn]").val();
|
||||
//key값
|
||||
url2 += '&name='+name;
|
||||
goNav(url2);
|
||||
@@ -80,7 +81,7 @@ $(document).ready(function() {
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
|
||||
$("#btn_search").click(function(){
|
||||
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val()}, page:1 }).trigger("reloadGrid");
|
||||
$("#grid").setGridParam({ postData: { searchName: $('input[name=searchName]').val(), searchUseYn: $('select[name=searchUseYn]').val()}, page:1 }).trigger("reloadGrid");
|
||||
});
|
||||
$("#btn_new").click(function(){
|
||||
var url2 = url_view;
|
||||
@@ -126,6 +127,16 @@ $(document).ready(function() {
|
||||
<tr>
|
||||
<th style="width:180px;"><%= localeMessage.getString("infIfConMan.ifNm") %></th>
|
||||
<td><input type="text" name="searchName" value="${param.searchName}"></td>
|
||||
<th style="width:180px;"><%= localeMessage.getString("infAdpConMan.useYn") %></th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="searchUseYn">
|
||||
<option value="">전체</option>
|
||||
<option value="1">사용함</option>
|
||||
<option value="0">사용안함</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -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({
|
||||
@@ -71,18 +71,18 @@
|
||||
|
||||
$('#grid').children().remove();
|
||||
|
||||
const itemTypes = ['Field', 'Grid', 'Group', 'Attr'];
|
||||
const itemTypes = ['Field', 'Grid', 'Group', 'Attr', 'Array'];
|
||||
|
||||
columns = [ // for columnData prop
|
||||
{ title: '<%= localeMessage.getString("layoutMan.itmeEn") %>', name: 'loutItemName', type: 'text', width:280, resize:true, minWidth:120 },
|
||||
{ title: '<%= localeMessage.getString("layoutMan.itemDesc") %>', name: 'loutItemDesc', type: 'text', width:280, resize:true, minWidth:120 },
|
||||
{ title: '<%=localeMessage.getString("standardLayout.itemLevel")%>', name: 'loutItemDepth', type: 'text', width:50 },
|
||||
{ title: '<%= localeMessage.getString("layoutMan.itmeEn") %>', name: 'loutItemName', type: 'text', width:280, resize:true, minWidth:120 },
|
||||
{ title: '<%=localeMessage.getString("standardLayout.itemLevel")%>', name: 'loutItemDepth', type: 'numeric', width:50 },
|
||||
{ title: '<%= localeMessage.getString("standardLayout.itemType") %>', name: 'loutItemType', type: 'autocomplete', source: itemTypes, width:70 },
|
||||
{ title: '<%= localeMessage.getString("standardLayout.arraySize") %>', name: 'loutItemOccCnt', type: 'text', width:90 },
|
||||
{ title: '<%= localeMessage.getString("standardLayout.refField") %>', name: 'loutItemOccRef', type: 'text', width:90 },
|
||||
{ title: '<%=localeMessage.getString("layoutMan.dataType")%>', name: 'loutItemDataType', type: 'autocomplete', source: basicDataTypes, width:160 },
|
||||
{ title: '<%=localeMessage.getString("layoutMan.dataLen")%>', name: 'loutItemLength', type: 'numeric', width: 70, align: 'center' },
|
||||
{ title: '<%=localeMessage.getString("layoutMan.decpLen")%>', name: 'loutItemDecimal', type: 'text', width: 70 },
|
||||
{ title: '<%=localeMessage.getString("layoutMan.decpLen")%>', name: 'loutItemDecimal', type: 'numeric', width: 70 },
|
||||
{ title: '<%=localeMessage.getString("layoutMan.defaultVal")%>', name: 'loutItemDefault', type: 'text', width: 70 },
|
||||
{ title: '<%=localeMessage.getString("layoutMan.mask")%>', name: 'loutItemMaskYn', type: 'checkbox', width: 70 },
|
||||
{ title: '<%=localeMessage.getString("layoutMan.moffset")%>', name: 'loutItemMaskOffset', type: 'text', width: 50 },
|
||||
@@ -182,6 +182,41 @@
|
||||
$('#dataTotalLen').text(totalLen);
|
||||
}
|
||||
|
||||
function focusGridCell(colIndex, rowIndex) {
|
||||
var cellName = jexcel.getColumnNameFromId([colIndex, rowIndex]);
|
||||
var cell = grid.getCell(cellName);
|
||||
grid.updateSelection(cell);
|
||||
cell.scrollIntoView({block: 'center', inline: 'center'});
|
||||
grid.openEditor(cell, false);
|
||||
}
|
||||
|
||||
var INTEGER_REGEXP = /^(0|[1-9][0-9]*)$/;
|
||||
|
||||
function validateNumberColumns() {
|
||||
var data = grid.getData();
|
||||
var gridColumns = grid.options.columns;
|
||||
|
||||
for (var col = 0; col < gridColumns.length; col++) {
|
||||
if (gridColumns[col].type !== 'numeric') {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var row = 0; row < data.length; row++) {
|
||||
var value = data[row][col];
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
value = $.trim(String(value));
|
||||
if (value !== '' && !INTEGER_REGEXP.test(value)) {
|
||||
alert("숫자만 입력할 수 있습니다.");
|
||||
focusGridCell(col, row);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function detail(key) {
|
||||
if (!isDetail) {
|
||||
return
|
||||
@@ -321,6 +356,10 @@
|
||||
});
|
||||
|
||||
$("#btn_modify").click(function(){
|
||||
if (!validateNumberColumns()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
if (isDetail){
|
||||
postData.push({ name: "cmd" , value:"UPDATE"});
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
<%@ 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");
|
||||
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
|
||||
<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/transaction/apim/apiInterfaceMan.json"/>';
|
||||
var url_view = '<c:url value="/onl/transaction/apim/apiInterfaceMan.view"/>';
|
||||
|
||||
var selectName = "searchEaiBzwkDstcd"; // selectBox Name
|
||||
function processSelectedData() {
|
||||
var grid = $("#grid");
|
||||
var selectedRowIds = grid.jqGrid('getGridParam', 'selarrrow');
|
||||
var selectedData = [];
|
||||
|
||||
for(var i = 0; i < selectedRowIds.length; i++) {
|
||||
var rowData = grid.jqGrid('getRowData', selectedRowIds[i]);
|
||||
selectedData.push({
|
||||
bizCode: rowData.eaiBzwkDstcd, // CLINET
|
||||
apiId: rowData.eaiSvcName,
|
||||
apiDesc: rowData.eaiSvcDesc, // SCOPE-API
|
||||
APIFULLPATH: rowData.apiFullPath,
|
||||
BZWKSVCKEYNAME: rowData.bzwksvckeyname
|
||||
});
|
||||
}
|
||||
|
||||
if(selectedData.length === 0) {
|
||||
alert("선택된 항목이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
alert("선택한 " + selectedData.length + "건의 API를 추가합니다.");
|
||||
window.returnValue = selectedData.length === 1 ? selectedData[0] : selectedData;
|
||||
window.close();
|
||||
}
|
||||
|
||||
function updateSelectionInfo(gridId) {
|
||||
var grid = $(gridId || "#grid");
|
||||
var selectedRows = grid.jqGrid('getGridParam', 'selarrrow');
|
||||
$("#selected-count").text("총: " + selectedRows.length + " 건");
|
||||
}
|
||||
|
||||
function init( callback) {
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url:url,
|
||||
dataType:"json",
|
||||
data:{cmd: 'LIST_COMBO'},
|
||||
success:function(json){
|
||||
new makeOptions("BIZCODE","BIZNAME").setObj($("select[name=searchEaiBzwkDstcd]")).setNoValueInclude(true).setNoValue("","전체").setData(json.bizList).setFormat(codeName3OptionFormat).rendering();
|
||||
|
||||
setSearchable(selectName); // 콤보에 searchable 설정
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
error:function(e){
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
function detail(){
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
$("#grid").setGridParam({ url:url,postData: postData }).trigger("reloadGrid");
|
||||
}
|
||||
|
||||
function search(){
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
$("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid");
|
||||
}
|
||||
function ioFormatter(cellvalue,options,rowObject){
|
||||
var serviceType = sessionStorage["serviceType"];
|
||||
if(cellvalue == "I"){
|
||||
return "타발";
|
||||
}else if(cellvalue =="O"){
|
||||
return "당발";
|
||||
}
|
||||
}
|
||||
function adapterNameShortFormatter(cellvalue,options,rowObject){
|
||||
if(cellvalue == null || cellvalue == '')
|
||||
return ''
|
||||
return cellvalue.substring(1, 4);
|
||||
}
|
||||
|
||||
function list(){
|
||||
detail()
|
||||
var gridPostData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
// gridPostData.searchApiEnableYn = 'Y';
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var selectedApiIds = urlParams.get("selectedApiIds");
|
||||
$('#grid').jqGrid({
|
||||
datatype : "json",
|
||||
mtype : 'POST',
|
||||
url : url,
|
||||
postData : gridPostData,
|
||||
colNames : [ '업무구분',
|
||||
'<%= localeMessage.getString("eaiMessage.eaiSvcName")%>',
|
||||
'<%= localeMessage.getString("eaiMessage.eaiSvcDesc")%>',
|
||||
'API FULL PATH',
|
||||
'업무서비스명',
|
||||
'요청',
|
||||
'응답',
|
||||
'작성자',
|
||||
'가상응답여부',
|
||||
'SyncAsyncType',
|
||||
],
|
||||
colModel : [ { name : 'eaiBzwkDstcd' , align : 'center' , width:'40', sortable:false},
|
||||
{ name : 'eaiSvcName' , align : 'left' , width:'100'},
|
||||
{ name : 'eaiSvcDesc' , align : 'left' },
|
||||
{ name : 'apiFullPath' , align : 'left' , width:'100'},
|
||||
{ name : 'bzwksvckeyname' , align : 'left' , width:'100', hidden: true},
|
||||
{ name : 'fromAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter },
|
||||
{ name : 'toAdapter' , align : 'center' , width:'40', formatter:adapterNameShortFormatter },
|
||||
{ name : 'author' , align : 'center' , width:'60' },
|
||||
{ name : 'simYn' , align : 'center' , width:'40' },
|
||||
{ name : 'syncAsyncType' , align : 'center' , width:'40', hidden: true},
|
||||
],
|
||||
jsonReader : {
|
||||
repeatitems : false
|
||||
},
|
||||
pager : $('#pager'),
|
||||
page : '${param.page}',
|
||||
rowNum : '${rmsDefaultRowNum}',
|
||||
autoheight : true,
|
||||
height : $("#container").height(),
|
||||
autowidth : true,
|
||||
viewrecords : true,
|
||||
multiselect: false,
|
||||
multiboxonly: false,
|
||||
rowList : eval('[${rmsDefaultRowList}]'),
|
||||
loadComplete:function (d){
|
||||
var $grid = $(this);
|
||||
if(selectedApiIds) {
|
||||
const selectedIds = selectedApiIds.split(',');
|
||||
$.each($grid.getDataIDs(), function(_, id) {
|
||||
var rowData = $grid.getRowData(id);
|
||||
if(selectedIds.includes(rowData.eaiSvcName)) {
|
||||
$grid.jqGrid('setSelection', id, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
for(var i = 0 ; i< colModel.length; i++){
|
||||
$(this).setColProp(colModel[i].name, {sortable : false});
|
||||
}
|
||||
updateSelectionInfo("#grid");
|
||||
},
|
||||
onSelectRow: function(rowid, status, e) {
|
||||
updateSelectionInfo("#grid");
|
||||
},
|
||||
onSelectAll: function(rowids, status) {
|
||||
updateSelectionInfo("#grid");
|
||||
},
|
||||
ondblClickRow : function(rowId) {
|
||||
const rowData = $(this).getRowData(rowId);
|
||||
const returnValue = {
|
||||
bizCode: rowData.eaiBzwkDstcd,
|
||||
apiId: rowData.eaiSvcName,
|
||||
apiDesc: rowData.eaiSvcDesc
|
||||
};
|
||||
//alert("선택한 API를 추가합니다.\nAPI ID: " + rowData.eaiSvcName);
|
||||
window.returnValue = returnValue;
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
init(list);
|
||||
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
|
||||
// 업무구분명 선택 시 자동 검색
|
||||
$("select[name=searchEaiBzwkDstcd]").change(function() {
|
||||
search();
|
||||
});
|
||||
$("#btn_search").click(function(){
|
||||
search();
|
||||
});
|
||||
$("input[name^=search]").keydown(function(key){
|
||||
if (key.keyCode == 13){
|
||||
$("#btn_search").click();
|
||||
}
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
|
||||
$("#btn_select").click(function(){
|
||||
processSelectedData();
|
||||
});
|
||||
|
||||
$("#btn_close").click(function () {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<div class="content_middle" id="content_middle" style="margin-top: 0px">
|
||||
<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>
|
||||
<button type="button" class="cssbtn" id="btn_select" level="R" ><i class="material-icons">check</i> <%= localeMessage.getString("button.check") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_close" level="R" status="DETAIL"><i class="material-icons">close</i> <%= localeMessage.getString("button.close") %></button>
|
||||
</div>
|
||||
<div class="title" id="title" >${rmsMenuName}</div>
|
||||
<!-- 선택 정보 표시 영역 추가 -->
|
||||
<div style="margin-bottom: 5px;">
|
||||
<span id="selected-count" style="color: #666; font-size: 12px;"></span>
|
||||
</div>
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:180px;">업무구분명</th>
|
||||
<td>
|
||||
<select name="searchEaiBzwkDstcd" value="${param.searchEaiBzwkDstcd}">
|
||||
</select>
|
||||
</td>
|
||||
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcName")%></th>
|
||||
<td>
|
||||
<input type="text" name="searchEaiSvcName" value="${param.searchEaiSvcName}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:180px;"><%= localeMessage.getString("eaiMessage.eaiSvcDesc")%></th>
|
||||
<td>
|
||||
<input type="text" name="searchEaiSvcDesc" value="${param.searchEaiSvcDesc}">
|
||||
</td>
|
||||
<th style="width:180px;">API FULL PATH</th>
|
||||
<td>
|
||||
<input type="text" name="searchApiFullPath" value="${param.searchApiFullPath}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table id="grid" ></table>
|
||||
<div id="pager"></div>
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
const LAYOUT_ITEM_TYPE_GROUP = "Group";
|
||||
const LAYOUT_ITEM_TYPE_GRID = "Grid";
|
||||
const LAYOUT_ITEM_TYPE_FIELD = "Field";
|
||||
const LAYOUT_ITEM_TYPE_ARRAY = "Array";
|
||||
|
||||
var isDetail = false;
|
||||
|
||||
@@ -550,6 +551,14 @@
|
||||
return data;
|
||||
}
|
||||
|
||||
// 항목 자신이 Array인 경우, 경로/변환명령 끝에 반복 표시 [*]를 추가
|
||||
function addArrayTag(loutItemType, data) {
|
||||
if (loutItemType == LAYOUT_ITEM_TYPE_ARRAY && !data.endsWith("[*]")) {
|
||||
data = data + "[*]";
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
//변환명에 있는 서비스코드와 if서비스 코드 매칭
|
||||
function validation(){
|
||||
if ($('input[name=cnvsnName]').val() == '') {
|
||||
@@ -889,10 +898,10 @@
|
||||
let selectValue = "";
|
||||
let targetValue = "";
|
||||
for ( var i = 0; i < sourceData.length; i++) {
|
||||
if (sourceData[i]['loutItemType'] != "Field" && sourceData[i]['loutItemType'] != "Attr") continue;
|
||||
if (sourceData[i]['loutItemType'] != "Field" && sourceData[i]['loutItemType'] != "Attr" && sourceData[i]['loutItemType'] != "Array") continue;
|
||||
selectValue = (sourceData[i]['LOUTITEMPATH']).substr(sourceData[i]['loutName'].length+1);
|
||||
for (var j = 0; j <targetData.length; j++) {
|
||||
if (targetData[j]['loutItemType'] != "Field" && targetData[j]['loutItemType'] != "Attr") continue;
|
||||
if (targetData[j]['loutItemType'] != "Field" && targetData[j]['loutItemType'] != "Attr" && targetData[j]['loutItemType'] != "Array") continue;
|
||||
targetValue = (targetData[j]['LOUTITEMPATH']).substr(targetData[j]['loutName'].length+1);
|
||||
if (targetValue == selectValue ) {
|
||||
var data = sourceData[i]['LOUTITEMPATH'];
|
||||
@@ -916,8 +925,8 @@
|
||||
//field, ATTR 여부 판단
|
||||
var sourceRow = sourceGrid.getRowData( srcRowId );
|
||||
var targetRow = targetGrid.getRowData( tgtRowId );
|
||||
if ( (sourceRow["loutItemType"] == 'Field' || sourceRow["loutItemType"] == 'Attr') &&
|
||||
(targetRow["loutItemType"] == 'Field' || targetRow["loutItemType"] == 'Attr') ){
|
||||
if ( (sourceRow["loutItemType"] == 'Field' || sourceRow["loutItemType"] == 'Attr' || sourceRow["loutItemType"] == 'Array') &&
|
||||
(targetRow["loutItemType"] == 'Field' || targetRow["loutItemType"] == 'Attr' || targetRow["loutItemType"] == 'Array') ){
|
||||
;
|
||||
}else{
|
||||
alert('<%=localeMessage.getString("transformManDetail.alert3")%>');
|
||||
@@ -936,15 +945,15 @@
|
||||
var targetValue = "";
|
||||
var j=tgtIndex-1-1;
|
||||
for ( var i = srcIndex-1; i < sourceData.length; i++) {
|
||||
if (sourceData[i]['loutItemType'] != LAYOUT_ITEM_TYPE_FIELD) continue;
|
||||
if (sourceData[i]['loutItemType'] != LAYOUT_ITEM_TYPE_FIELD && sourceData[i]['loutItemType'] != LAYOUT_ITEM_TYPE_ARRAY) continue;
|
||||
j++;
|
||||
while (targetData.length -1 >= j &&targetData[j]["loutItemType"] != LAYOUT_ITEM_TYPE_FIELD){
|
||||
while (targetData.length -1 >= j && targetData[j]["loutItemType"] != LAYOUT_ITEM_TYPE_FIELD && targetData[j]["loutItemType"] != LAYOUT_ITEM_TYPE_ARRAY){
|
||||
j++;
|
||||
}
|
||||
if (targetData.length -1 < j) break;
|
||||
var targetRowId = targetData[j]['id'];
|
||||
targetGrid.jqGrid('setCell',targetRowId,'CNVSNCMDNAME',addGroupTag(srcGroup,sourceData[i]['LOUTITEMPATH']));
|
||||
targetGrid.jqGrid('setCell',targetRowId,'CNVSNRSULTITEMPATHNAME',addGroupTag(tgtGroup,targetData[j]['LOUTITEMPATH']));
|
||||
targetGrid.jqGrid('setCell',targetRowId,'CNVSNCMDNAME',addArrayTag(sourceData[i]['loutItemType'], addGroupTag(srcGroup,sourceData[i]['LOUTITEMPATH'])));
|
||||
targetGrid.jqGrid('setCell',targetRowId,'CNVSNRSULTITEMPATHNAME',addArrayTag(targetData[j]['loutItemType'], addGroupTag(tgtGroup,targetData[j]['LOUTITEMPATH'])));
|
||||
targetGrid.jqGrid('setCell',targetRowId,'CNVSNITEMSERNO',targetData[j]['loutItemSerno']);
|
||||
|
||||
const sourceEndpointElementId = sourceGrid.getEndpointElementId(sourceData[i]['id']);
|
||||
@@ -1015,7 +1024,7 @@
|
||||
var targetData = targetGrid.getRowData();
|
||||
var gridData = new Array();
|
||||
for (var i = 0; i <targetData.length; i++) {
|
||||
if ((targetData[i]['loutItemType'] != "Field") && (targetData[i]['loutItemType'] != "Attr")) {
|
||||
if ((targetData[i]['loutItemType'] != "Field") && (targetData[i]['loutItemType'] != "Attr") && (targetData[i]['loutItemType'] != "Array")) {
|
||||
continue;
|
||||
}
|
||||
if (targetData[i]['CNVSNCMDNAME']==null
|
||||
@@ -1158,7 +1167,7 @@
|
||||
function srcformatterFunction(cellvalue,options,rowObject){
|
||||
var rowId = options["rowId"];
|
||||
var loutItemType = rowObject["loutItemType"];
|
||||
if (loutItemType == 'Field' || loutItemType == 'Attr'){
|
||||
if (loutItemType == 'Field' || loutItemType == 'Attr' || loutItemType == 'Array'){
|
||||
return "<div id='div_src_"+rowId+"' name='src_"+rowId+"' style='width:28px;height:17px;background-color: #D12F35; border-color: #ab1717; border-style: solid; border-width: 1px; border-radius: 3px;' />";
|
||||
}else{
|
||||
return "<div id='div_srcgroup_"+rowId+"' name='srcgroup_"+rowId+"' style='width:28px;height:17px;' />";
|
||||
@@ -1170,7 +1179,7 @@
|
||||
function tgtformatterFunction(cellvalue,options,rowObject){
|
||||
var rowId = options["rowId"];
|
||||
var loutItemType = rowObject["loutItemType"];
|
||||
if (loutItemType == 'Field' || loutItemType == 'Attr'){
|
||||
if (loutItemType == 'Field' || loutItemType == 'Attr' || loutItemType == 'Array'){
|
||||
return "<div id='div_tgt_"+rowId+"_base' name='div_tgt_"+rowId+"_base' style='width:30px;height:20px; background-color:gray;' >"
|
||||
+ "<div id='div_tgt_"+rowId+"' name='tgt_"+rowId+"' style='width:28px;height:17px; background-color: #3E7E9C; border-color: #217ca7; border-style: solid; border-width: 1px; border-radius: 3px;' onclick='jqGridOnTargetGridEndpointClick(this)'>"
|
||||
+ "</div></div>";
|
||||
@@ -1669,8 +1678,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if ((sourceRow["loutItemType"] != 'Field' && sourceRow["loutItemType"] != 'Attr')
|
||||
|| (targetRow["loutItemType"] != 'Field' && targetRow["loutItemType"] != 'Attr')) {
|
||||
if ((sourceRow["loutItemType"] != 'Field' && sourceRow["loutItemType"] != 'Attr' && sourceRow["loutItemType"] != 'Array')
|
||||
|| (targetRow["loutItemType"] != 'Field' && targetRow["loutItemType"] != 'Attr' && targetRow["loutItemType"] != 'Array')) {
|
||||
alert('<%=localeMessage.getString("transformManDetail.alert3")%>');
|
||||
return ;
|
||||
}
|
||||
@@ -1684,7 +1693,7 @@
|
||||
|
||||
if (targetRow['CNVSNCMDNAME'] == '') {
|
||||
// 변환 명령 값이 한번 설정 되면 변경 되지 않게 함.
|
||||
var cnvsnCmdName = addGroupTag(srcGroup,sourceRow["LOUTITEMPATH"]);
|
||||
var cnvsnCmdName = addArrayTag(sourceRow["loutItemType"], addGroupTag(srcGroup,sourceRow["LOUTITEMPATH"]));
|
||||
// function까지 처리되도록 수정했으나, parameter가 하나인 경우에만 정상처리됨
|
||||
if($('#functionCombo').val() != "") {
|
||||
cnvsnCmdName = $('#functionCombo').val() + "(" + cnvsnCmdName + ")";
|
||||
@@ -1693,7 +1702,7 @@
|
||||
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNCMDNAME', cnvsnCmdName);
|
||||
}
|
||||
|
||||
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNRSULTITEMPATHNAME',addGroupTag(tgtGroup,targetRow["LOUTITEMPATH"]));
|
||||
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNRSULTITEMPATHNAME',addArrayTag(targetRow["loutItemType"], addGroupTag(tgtGroup,targetRow["LOUTITEMPATH"])));
|
||||
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNITEMSERNO',targetRow['loutItemSerno']);
|
||||
targetGrid.jqGrid('setCell',tgtRowId,'CNVSNSRCITEMS', cnvsnSrcItems);
|
||||
targetGrid.jqGrid('setCell',tgtRowId,'ISVALIDMAPPING', true);
|
||||
@@ -1885,8 +1894,8 @@
|
||||
if (loutItemType == LAYOUT_ITEM_TYPE_GRID || loutItemType == LAYOUT_ITEM_TYPE_GROUP) {
|
||||
// group 인 경우
|
||||
items = createGridGroupContextMenuItems(grid, rowId);
|
||||
} else if(loutItemType == LAYOUT_ITEM_TYPE_FIELD) {
|
||||
// field 인 경우
|
||||
} else if(loutItemType == LAYOUT_ITEM_TYPE_FIELD || loutItemType == LAYOUT_ITEM_TYPE_ARRAY) {
|
||||
// field, array 인 경우
|
||||
items = createFieldContextMenuItems(grid, positionElement.attr('id'));
|
||||
} else {
|
||||
return false;
|
||||
@@ -1938,8 +1947,8 @@
|
||||
var key = "";
|
||||
var args = new Object();
|
||||
args['eaiSvcName'] = $('input[name=eaiSvcName]').val();
|
||||
var url='<c:url value="/onl/transaction/extnl/interfaceMan.view"/>';
|
||||
url = url + "?cmd=POPUP";
|
||||
var url=url_view; //'<c:url value="/onl/transaction/extnl/interfaceMan.view"/>';
|
||||
url = url + "?cmd=API_POPUP";
|
||||
var ret = showModal(url,args,1020,630, function(arg){
|
||||
var args = null;
|
||||
if(arg == null || arg == undefined ) {//chrome
|
||||
@@ -1953,11 +1962,11 @@
|
||||
|
||||
var ret = args.returnValue;
|
||||
console.log("ret",ret);
|
||||
key = ret['key'];
|
||||
key = ret['apiId'];
|
||||
|
||||
$("input[name=eaiSvcName]").val(key);
|
||||
$("input[name=eaiSvcDesc]").val(ret['eaiSvcDesc']);
|
||||
bzwkDstCd = ret['eaiBzwkDstCd'];
|
||||
$("input[name=eaiSvcDesc]").val(ret['apiDesc']);
|
||||
bzwkDstCd = ret['bizCode'];
|
||||
|
||||
$("input[name=eaiSvcName]").change();
|
||||
});
|
||||
@@ -1989,7 +1998,7 @@
|
||||
break;
|
||||
}
|
||||
|
||||
if (loutItemType == LAYOUT_ITEM_TYPE_FIELD) {
|
||||
if (loutItemType == LAYOUT_ITEM_TYPE_FIELD || loutItemType == LAYOUT_ITEM_TYPE_ARRAY) {
|
||||
loutItemPath = loutItemPath.replace(new RegExp("^" + prefix), "");
|
||||
console.debug(grid.getId() + " : loutItemPath : " + loutItemPath);
|
||||
rowData['GROUP_LOUTITEMPATH'] = loutItemPath;
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
|
||||
<script language="javascript" >
|
||||
<script language="javascript" >
|
||||
var url_view = '<c:url value="/onl/admin/rule/transform2/transform2Man.view" />';
|
||||
var $ = jQuery.noConflict();
|
||||
var bzwkDstCd = "";
|
||||
|
||||
@@ -81,8 +82,9 @@ $(document).ready(function() {
|
||||
var key = "";
|
||||
var args = new Object();
|
||||
args['eaiSvcName'] = $('input[name=eaiSvcName]').val();
|
||||
var url='<c:url value="/onl/transaction/extnl/interfaceMan.view"/>';
|
||||
url = url + "?cmd=POPUP";
|
||||
var url=url_view; //'<c:url value="/onl/transaction/extnl/interfaceMan.view"/>';
|
||||
url = url + "?cmd=API_POPUP";
|
||||
console.log('[new]', url);
|
||||
var ret = showModal(url,args,1020,630, function(arg){
|
||||
var args = null;
|
||||
if(arg == null || arg == undefined ) {//chrome
|
||||
@@ -96,11 +98,11 @@ $(document).ready(function() {
|
||||
|
||||
var ret = args.returnValue;
|
||||
console.log("ret",ret);
|
||||
key = ret['key'];
|
||||
key = ret['apiId'];
|
||||
|
||||
$("input[name=eaiSvcName]").val(key);
|
||||
$("input[name=eaiSvcDesc]").val(ret['eaiSvcDesc']);
|
||||
bzwkDstCd = ret['eaiBzwkDstCd'];
|
||||
$("input[name=eaiSvcDesc]").val(ret['apiDesc']);
|
||||
bzwkDstCd = ret['bizCode'];
|
||||
|
||||
$("input[name=eaiSvcName]").change();
|
||||
//$("select[name=eaiSevrDstcd]").val(ret['eaiSevrDstcd']);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<script language="javascript" >
|
||||
var url = '<c:url value="/onl/apim/apigroup/apiGroupMan.json"/>';
|
||||
var url_view = '<c:url value="/onl/apim/apigroup/apiGroupMan.view"/>';
|
||||
var url_popup_view = '<c:url value="/onl/transaction/apim/apiSpecManPopup.view"/>';
|
||||
var url_popup_view = '<c:url value="/onl/transaction/apim/apiSpecMan.view"/>';
|
||||
|
||||
var isDetail = false;
|
||||
let dialog;
|
||||
@@ -415,6 +415,12 @@
|
||||
<input type="text" name="groupName" data-required data-warning="API 그룹 명을 입력하여 주십시오.">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:150px;">그룹 설명</th>
|
||||
<td colspan="7">
|
||||
<input type="text" name="groupDesc">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:150px;">공개 여부</th>
|
||||
<td style="width:100px;">
|
||||
@@ -461,14 +467,6 @@
|
||||
<img id="mainIconPreview" src="#" alt="Main Icon Preview" style="display:none;height:20px">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<%--<tr>
|
||||
<th>컨텐츠 내용</th>
|
||||
<td colspan="5">
|
||||
<textarea name="contentDetail" id="contentDetail" rows="10" cols="50"></textarea>
|
||||
</td>
|
||||
</tr>--%>
|
||||
</table>
|
||||
<div style="margin-top:15px;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px ">
|
||||
|
||||
@@ -38,19 +38,34 @@
|
||||
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("승인하시겠습니까?")) {
|
||||
var isDeleteReq = (currentRequestType === 'DELETE');
|
||||
var gwAction = (isDeleteReq && $('#chkGwDelete').is(':checked')) ? 'DELETE' : 'BLOCK';
|
||||
var confirmMsg = isDeleteReq
|
||||
? "해지 승인 시 포털 인증키(Credential)는 삭제되며,\n게이트웨이 클라이언트는 [" + (gwAction === 'DELETE' ? "완전 삭제" : "차단(비활성화)") + "] 처리됩니다.\n승인하시겠습니까?"
|
||||
: "승인하시겠습니까?";
|
||||
if (confirm(confirmMsg)) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {cmd: "APPROVE", id: key},
|
||||
data: {cmd: "APPROVE", id: key, gwAction: gwAction},
|
||||
success: function (args) {
|
||||
<%--alert("<%= localeMessage.getString("approval.approveSuccess") %>");--%>
|
||||
alert("승인되었습니다.");
|
||||
@@ -91,6 +106,32 @@
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_app_force_reject").click(function () {
|
||||
if (!confirm("승인자 목록에 삭제된 사용자가 있어 승인 진행이 불가능한 건입니다.\n강제 반려하시겠습니까?")) {
|
||||
return;
|
||||
}
|
||||
var rejectReason = prompt("강제 반려 사유를 입력해 주세요");
|
||||
if (rejectReason != null) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {cmd: "FORCE_REJECT", id: key, rejectReason: rejectReason},
|
||||
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_app_redeploy').click(function () {
|
||||
if (confirm("재배포 하시겠습니까?")) {
|
||||
$.ajax({
|
||||
@@ -112,12 +153,36 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$("#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();
|
||||
});
|
||||
});
|
||||
|
||||
var currentRequestType = null; // 현재 신청 유형 (DELETE 승인 시 GW 처리방식 선택 노출용)
|
||||
|
||||
function detail(key) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
@@ -126,17 +191,31 @@
|
||||
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 =>
|
||||
var isCurrentApprover = data.approvers.some(approver =>
|
||||
approver.approverStatus === 'CURRENT' &&
|
||||
approver.user.USERID === loginUser
|
||||
)) {
|
||||
approver.user && approver.user.USERID === loginUser
|
||||
);
|
||||
if (isCurrentApprover) {
|
||||
document.getElementById("btn_app_approve").style.display = "";
|
||||
document.getElementById("btn_app_reject").style.display = "";
|
||||
} else {
|
||||
@@ -144,7 +223,30 @@
|
||||
document.getElementById("btn_app_reject").style.display = "none";
|
||||
}
|
||||
|
||||
if (data.approvalStatus.description === '배포실패') {
|
||||
// API 이용해지(DELETE) 건: 승인 가능 상태의 현재 승인자에게만 GW 처리방식 선택 노출
|
||||
currentRequestType = data.requestType;
|
||||
var inProgress = data.approvalStatus.code === 'REQUESTED' || data.approvalStatus.code === 'PROCESSING';
|
||||
if (data.requestType === 'DELETE' && isCurrentApprover && inProgress) {
|
||||
$("#gwActionSection").show();
|
||||
} else {
|
||||
$("#gwActionSection").hide();
|
||||
}
|
||||
// 처리 완료된 해지 건은 어떤 방식으로 실행됐는지 표시
|
||||
if (data.requestType === 'DELETE' && data.gwAction) {
|
||||
$("#gwActionResult").text("GW 처리방식: " + (data.gwAction === 'DELETE' ? "완전 삭제" : "차단(비활성화)")).show();
|
||||
} else {
|
||||
$("#gwActionResult").hide();
|
||||
}
|
||||
|
||||
// 삭제된 승인자(USERID 없음)로 진행 불가한 건: 요청됨/진행중 상태면 승인자가 아니어도 강제 반려 허용
|
||||
var hasDeletedApprover = data.approvers.some(approver =>
|
||||
!approver.user || !approver.user.USERID
|
||||
);
|
||||
var isProgressStatus = data.approvalStatus.code === 'REQUESTED' || data.approvalStatus.code === 'PROCESSING';
|
||||
document.getElementById("btn_app_force_reject").style.display =
|
||||
(hasDeletedApprover && isProgressStatus && !isCurrentApprover) ? "" : "none";
|
||||
|
||||
if (data.approvalStatus.code === 'FAILED') {
|
||||
document.getElementById("btn_app_redeploy").style.display = "";
|
||||
}
|
||||
|
||||
@@ -242,14 +344,23 @@
|
||||
</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>
|
||||
|
||||
<div class="title">Client 정보</div>
|
||||
<table id="client" class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col style="width:20%"/>
|
||||
<col style="width:80%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th style="width:20%;">이름</th>
|
||||
<td><span id="clientName"></span></td>
|
||||
@@ -281,13 +392,26 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="gwActionSection" style="display:none; margin: 12px 0 0 4px; color: #c0392b;">
|
||||
<label style="cursor:pointer;">
|
||||
<input type="checkbox" id="chkGwDelete"/>
|
||||
GW 인증정보 완전 삭제 (미체크 시 차단 처리 — 인증정보 보존, 복구 가능)
|
||||
</label>
|
||||
</div>
|
||||
<div id="gwActionResult" style="display:none; margin: 12px 0 0 4px; font-weight: bold;"></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>
|
||||
<button type="button" class="cssbtn" style="display: none;" id="btn_app_reject" level="W"><i
|
||||
class="material-icons">close</i>반려
|
||||
</button>
|
||||
<button type="button" class="cssbtn" style="display: none;" id="btn_app_force_reject" level="W"><i
|
||||
class="material-icons">block</i>강제 반려
|
||||
</button>
|
||||
<button type="button" class="cssbtn" style="display: none;" id="btn_app_redeploy" level="W"><i
|
||||
class="material-icons">close</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'];
|
||||
@@ -79,6 +131,24 @@
|
||||
// var reverseNumber = records - ((page - 1) * rowNum + i);
|
||||
$(this).setCell(rows[i], 'rowNum', number);
|
||||
}
|
||||
|
||||
// 승인 상태별 행 배경색: 요청됨(#fff3cd) / 진행중(#d1ecf1) 강조
|
||||
if (d && d.rows) {
|
||||
for (var j = 0; j < d.rows.length; j++) {
|
||||
var st = d.rows[j].approvalStatus || {};
|
||||
var code = st.code || '';
|
||||
var desc = st.description || '';
|
||||
var bg = '';
|
||||
if (code === 'REQUESTED' || desc === '요청됨') {
|
||||
bg = '#fff3cd';
|
||||
} else if (code === 'PROCESSING' || desc === '진행중') {
|
||||
bg = '#d1ecf1';
|
||||
}
|
||||
if (bg) {
|
||||
$('#grid tr#' + $.jgrid.jqID(d.rows[j].id)).css('background-color', bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
loadError: function(jqXHR, textStatus, errorThrown) {
|
||||
var location = '<%=request.getContextPath()%>/';
|
||||
@@ -93,6 +163,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 +186,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> 삭제 기능은 <strong>개발(dev) 환경에서만</strong> 동작합니다. 선택 삭제 시 승인 요청과 승인자 이력이 DB에서 영구 삭제됩니다.
|
||||
</div>
|
||||
</c:if>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing="0">
|
||||
<tbody>
|
||||
@@ -134,8 +216,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>
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.cssbtn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
<script language="javascript">
|
||||
<%
|
||||
@@ -150,6 +154,55 @@
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_user_force_reject").click(function() {
|
||||
if (!confirm("승인자 목록에 삭제된 사용자가 있어 승인 진행이 불가능한 건입니다.\n강제 반려하시겠습니까?")) {
|
||||
return;
|
||||
}
|
||||
var rejectReason = prompt("강제 반려 사유를 입력해 주세요");
|
||||
if (rejectReason != null) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {cmd: "FORCE_REJECT", id: key, rejectReason: rejectReason},
|
||||
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_user_delete").click(function() {
|
||||
if (!confirm("삭제된 사용자 또는 법인이 포함되어 승인·반려할 수 없는 요청입니다.\n이 승인 요청을 삭제하시겠습니까?")) {
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {cmd: "DELETE", id: key},
|
||||
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_user_redeploy').click(function() {
|
||||
if (confirm("재배포 하시겠습니까?")) {
|
||||
$.ajax({
|
||||
@@ -217,20 +270,55 @@
|
||||
|
||||
// Display Portal User details
|
||||
var portalUser = data.portalUser;
|
||||
$("#loginId").text(portalUser.loginId);
|
||||
$("#userName").text(portalUser.userName);
|
||||
$("#mobileNumber").text(portalUser.mobileNumber);
|
||||
$("#userStatusDescription").text(portalUser.userStatusDescription);
|
||||
$("#roleCodeDescription").text(portalUser.roleCodeDescription);
|
||||
if (data.approvers.some(approver =>
|
||||
approver.approverStatus === 'CURRENT' &&
|
||||
approver.user.USERID === loginUser
|
||||
)) {
|
||||
document.getElementById("btn_user_approve").style.display = "";
|
||||
document.getElementById("btn_user_reject").style.display = "";
|
||||
var portalOrg = portalUser ? portalUser.portalOrgUIs : null;
|
||||
|
||||
// 삭제된 대상 사용자/법인 판별
|
||||
var userDeleted = !portalUser
|
||||
|| portalUser.userName === '반려된 사용자'
|
||||
|| portalUser.userName === '삭제된 사용자';
|
||||
var orgDeleted = !portalOrg
|
||||
|| portalOrg.orgStatus === 'REMOVED'
|
||||
|| portalOrg.orgName === '삭제된법인';
|
||||
var deletedTarget = userDeleted || orgDeleted;
|
||||
|
||||
if (portalUser) {
|
||||
$("#loginId").text(portalUser.loginId);
|
||||
$("#userName").text(portalUser.userName);
|
||||
$("#mobileNumber").text(portalUser.mobileNumber);
|
||||
$("#userStatusDescription").text(portalUser.userStatusDescription);
|
||||
$("#roleCodeDescription").text(portalUser.roleCodeDescription);
|
||||
} else {
|
||||
document.getElementById("btn_user_approve").style.display = "none";
|
||||
document.getElementById("btn_user_reject").style.display = "none";
|
||||
$("#loginId, #userName, #mobileNumber, #userStatusDescription, #roleCodeDescription").text("삭제된 사용자");
|
||||
}
|
||||
|
||||
var isCurrentApprover = data.approvers.some(approver =>
|
||||
approver.approverStatus === 'CURRENT' &&
|
||||
approver.user && approver.user.USERID === loginUser
|
||||
);
|
||||
|
||||
if (deletedTarget) {
|
||||
// 삭제된 사용자/법인 포함 → 승인·반려 불가. 버튼은 비활성(disable) 표기, 삭제 버튼 노출
|
||||
$("#btn_user_approve, #btn_user_reject").show().prop("disabled", true);
|
||||
document.getElementById("btn_user_force_reject").style.display = "none";
|
||||
document.getElementById("btn_user_delete").style.display = "";
|
||||
} else {
|
||||
$("#btn_user_approve, #btn_user_reject").prop("disabled", false);
|
||||
document.getElementById("btn_user_delete").style.display = "none";
|
||||
if (isCurrentApprover) {
|
||||
document.getElementById("btn_user_approve").style.display = "";
|
||||
document.getElementById("btn_user_reject").style.display = "";
|
||||
} else {
|
||||
document.getElementById("btn_user_approve").style.display = "none";
|
||||
document.getElementById("btn_user_reject").style.display = "none";
|
||||
}
|
||||
|
||||
// 삭제된 승인자(USERID 없음)로 진행 불가한 건: 요청됨/진행중 상태면 승인자가 아니어도 강제 반려 허용
|
||||
var hasDeletedApprover = data.approvers.some(approver =>
|
||||
!approver.user || !approver.user.USERID
|
||||
);
|
||||
var isProgressStatus = data.approvalStatus.code === 'REQUESTED' || data.approvalStatus.code === 'PROCESSING';
|
||||
document.getElementById("btn_user_force_reject").style.display =
|
||||
(hasDeletedApprover && isProgressStatus && !isCurrentApprover) ? "" : "none";
|
||||
}
|
||||
|
||||
if (data.approvalStatus.description === '배포실패'){
|
||||
@@ -238,23 +326,27 @@
|
||||
}
|
||||
|
||||
// Display Portal Org details
|
||||
var portalOrg = portalUser.portalOrgUIs;
|
||||
$("#orgName").text(portalOrg.orgName);
|
||||
$("#orgStatusDescription").text(portalOrg.orgStatusDescription);
|
||||
$("#serviceName").text(portalOrg.serviceName);
|
||||
$("#corpRegNo").text(formatCorpRegNo(portalOrg.corpRegNo));
|
||||
$("#compRegNo").text(formatCompanyRegNo(portalOrg.compRegNo));
|
||||
if (portalOrg) {
|
||||
$("#orgName").text(portalOrg.orgName);
|
||||
$("#orgStatusDescription").text(portalOrg.orgStatusDescription);
|
||||
$("#serviceName").text(portalOrg.serviceName);
|
||||
$("#corpRegNo").text(formatCorpRegNo(portalOrg.corpRegNo));
|
||||
$("#compRegNo").text(formatCompanyRegNo(portalOrg.compRegNo));
|
||||
|
||||
$("#scPhoneNumber").text(portalOrg.scPhoneNumber); // 고객센터 전화번호
|
||||
$("#orgPhoneNumber").text(portalOrg.orgPhoneNumber); // 회사 전화번호
|
||||
$("#ceoName").text(portalOrg.ceoName); // 대표자 성명
|
||||
$("#orgIndustryType").text(portalOrg.orgIndustryType || ""); // 업종
|
||||
$("#orgAddr").text(portalOrg.orgAddr); // 소재지
|
||||
$("#orgSectors").text(portalOrg.orgSectors || ""); // 업태
|
||||
$("#orgCode").text(portalOrg.orgCode);
|
||||
$("#ipWhitelist").text(portalOrg.ipWhitelist || "");
|
||||
$("#scPhoneNumber").text(portalOrg.scPhoneNumber); // 고객센터 전화번호
|
||||
$("#orgPhoneNumber").text(portalOrg.orgPhoneNumber); // 회사 전화번호
|
||||
$("#ceoName").text(portalOrg.ceoName); // 대표자 성명
|
||||
$("#orgIndustryType").text(portalOrg.orgIndustryType || ""); // 업종
|
||||
$("#orgAddr").text(portalOrg.orgAddr); // 소재지
|
||||
$("#orgSectors").text(portalOrg.orgSectors || ""); // 업태
|
||||
$("#orgCode").text(portalOrg.orgCode);
|
||||
$("#ipWhitelist").text(portalOrg.ipWhitelist || "");
|
||||
} else {
|
||||
$("#orgName").text("삭제된법인");
|
||||
$("#orgStatusDescription, #serviceName, #corpRegNo, #compRegNo, #scPhoneNumber, #orgPhoneNumber, #ceoName, #orgIndustryType, #orgAddr, #orgSectors, #orgCode, #ipWhitelist").text("");
|
||||
}
|
||||
|
||||
if (portalOrg.compRegFile) {
|
||||
if (portalOrg && portalOrg.compRegFile) {
|
||||
$("#fileName").text(portalOrg.fileName);
|
||||
if (portalOrg.fileName) {
|
||||
$("#fileWrapper").show().off('click').on('click', function() {
|
||||
@@ -294,15 +386,18 @@
|
||||
|
||||
// 사용자 정보 업데이트
|
||||
var portalUser = data.portalUser;
|
||||
$("#loginId").text(portalUser.loginId);
|
||||
$("#userName").text(portalUser.userName);
|
||||
$("#mobileNumber").text(portalUser.mobileNumber);
|
||||
$("#userStatusDescription").text(portalUser.userStatusDescription);
|
||||
$("#roleCodeDescription").text(portalUser.roleCodeDescription);
|
||||
if (portalUser) {
|
||||
$("#loginId").text(portalUser.loginId);
|
||||
$("#userName").text(portalUser.userName);
|
||||
$("#mobileNumber").text(portalUser.mobileNumber);
|
||||
$("#userStatusDescription").text(portalUser.userStatusDescription);
|
||||
$("#roleCodeDescription").text(portalUser.roleCodeDescription);
|
||||
} else {
|
||||
$("#loginId, #userName, #mobileNumber, #userStatusDescription, #roleCodeDescription").text("삭제된 사용자");
|
||||
}
|
||||
|
||||
// 법인 정보 업데이트
|
||||
var portalOrg = portalUser.portalOrgUIs;
|
||||
updateOrgInfo(portalOrg);
|
||||
updateOrgInfo(portalUser ? portalUser.portalOrgUIs : null);
|
||||
|
||||
// 마스킹 상태에 따라 데이터를 다르게 표시
|
||||
var isMasked = $("#btn_unmask").attr("status") === "DETAIL";
|
||||
@@ -344,6 +439,11 @@
|
||||
|
||||
// 법인 정보 업데이트 함수
|
||||
function updateOrgInfo(portalOrg) {
|
||||
if (!portalOrg) {
|
||||
$("#orgName").text("삭제된법인");
|
||||
$("#orgStatusDescription, #serviceName, #corpRegNo, #compRegNo, #scPhoneNumber, #orgPhoneNumber, #ceoName, #orgIndustryType, #orgAddr, #orgSectors, #orgCode, #ipWhitelist").text("");
|
||||
return;
|
||||
}
|
||||
$("#orgName").text(portalOrg.orgName);
|
||||
$("#orgStatusDescription").text(portalOrg.orgStatusDescription);
|
||||
$("#serviceName").text(portalOrg.serviceName);
|
||||
@@ -392,15 +492,21 @@
|
||||
|
||||
<div class="title">사용자 정보</div>
|
||||
<table class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col style="width:20%"/>
|
||||
<col style="width:30%"/>
|
||||
<col style="width:20%"/>
|
||||
<col style="width:30%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th style="width:20%;">이메일 아이디</th>
|
||||
<td style="width:30%;" colspan="3"><span id="loginId"></span></td>
|
||||
<th>이메일 아이디</th>
|
||||
<td colspan="3"><span id="loginId"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:20%;">이름</th>
|
||||
<td style="width:30%;"><span id="userName"></span></td>
|
||||
<th style="width:20%;">권한</th>
|
||||
<td style="width:30%;"><span id="roleCodeDescription"></span></td>
|
||||
<th>이름</th>
|
||||
<td><span id="userName"></span></td>
|
||||
<th>권한</th>
|
||||
<td><span id="roleCodeDescription"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>핸드폰 번호</th>
|
||||
@@ -480,6 +586,8 @@
|
||||
<div class="popup_button">
|
||||
<button type="button" class="cssbtn" style="display: none;" id="btn_user_approve" level="W"><i class="material-icons">check</i>승인</button>
|
||||
<button type="button" class="cssbtn" style="display: none;" id="btn_user_reject" level="W"><i class="material-icons">close</i>반려</button>
|
||||
<button type="button" class="cssbtn" style="display: none;" id="btn_user_force_reject" level="W"><i class="material-icons">block</i>강제 반려</button>
|
||||
<button type="button" class="cssbtn" style="display: none;" id="btn_user_delete" level="W"><i class="material-icons">delete</i>삭제</button>
|
||||
<button type="button" class="cssbtn" style="display: none;" id="btn_user_redeploy" level="W"><i class="material-icons">close</i>재처리</button>
|
||||
<button type="button" class="cssbtn" id="btn_close" level="R"><i class="material-icons">cancel</i> <%= localeMessage.getString("button.close") %>
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
<%@ 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"/>
|
||||
<style>
|
||||
.mask-email { color:#1a73e8; font-weight:bold; }
|
||||
.mask-phone { color:#d93025; font-weight:bold; }
|
||||
.mask-digits { color:#9334e6; font-weight:bold; }
|
||||
.msg-cell { display:inline-block; max-width:380px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; vertical-align:middle; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/messagerequest/messageRequestMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/messagerequest/messageRequestMan.view" />';
|
||||
|
||||
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
|
||||
function escapeAttr(s) { return escapeHtmlJs(s).replace(/"/g, '"'); }
|
||||
|
||||
// 제목: 없으면 messageCode, 마우스오버에 코드명(코드) 표기
|
||||
function formatSubject(cellvalue, options, rowObject) {
|
||||
var code = rowObject.messageCode || '';
|
||||
var subject = (cellvalue && cellvalue !== '') ? cellvalue : '';
|
||||
var title = rowObject.messageCodeName ? (rowObject.messageCodeName + ' (' + code + ')') : code;
|
||||
if (subject) {
|
||||
// 제목이 있으면 2줄: 제목 + 하단에 메세지코드
|
||||
var codeLine = code ? '<br><span style="color:#888;font-size:0.85em;">' + escapeHtmlJs(code) + '</span>' : '';
|
||||
return '<span title="' + escapeAttr(title) + '">' + escapeHtmlJs(subject) + codeLine + '</span>';
|
||||
}
|
||||
// 제목이 없으면 메세지코드만
|
||||
return '<span title="' + escapeAttr(title) + '">' + escapeHtmlJs(code) + '</span>';
|
||||
}
|
||||
|
||||
// 본문: 서버에서 마스킹+escape 된 안전 HTML 그대로 렌더
|
||||
function formatMessage(cellvalue, options, rowObject) {
|
||||
return '<span class="msg-cell">' + (rowObject.messageHtml || '') + '</span>';
|
||||
}
|
||||
|
||||
// 수신자 정보: 휴대폰 / 이메일 / 메신저ID (모두 마스킹됨)
|
||||
function formatReceiverInfo(cellvalue, options, rowObject) {
|
||||
var parts = [];
|
||||
if (rowObject.phone) parts.push(escapeHtmlJs(rowObject.phone));
|
||||
if (rowObject.email) parts.push(escapeHtmlJs(rowObject.email));
|
||||
if (rowObject.messengerId) parts.push(escapeHtmlJs(rowObject.messengerId));
|
||||
return parts.join('<br>');
|
||||
}
|
||||
|
||||
function formatRequestDate(cellvalue, options, rowObject) {
|
||||
return '<span title="' + escapeAttr(rowObject.requestDateFull || '') + '">' + escapeHtmlJs(rowObject.requestDateShort || '') + '</span>';
|
||||
}
|
||||
function formatSentDate(cellvalue, options, rowObject) {
|
||||
return '<span title="' + escapeAttr(rowObject.sentDateFull || '') + '">' + escapeHtmlJs(rowObject.sentDateShort || '') + '</span>';
|
||||
}
|
||||
|
||||
function init() {
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'LIST_INIT_COMBO'},
|
||||
success: function (json) {
|
||||
new makeOptions("CODE", "NAME").setObj($("select[name=searchRequestStatus]"))
|
||||
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
|
||||
.setData(json.statusList).rendering();
|
||||
new makeOptions("CODE", "NAME").setObj($("select[name=searchMessageCode]"))
|
||||
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
|
||||
.setData(json.messageCodeList).rendering();
|
||||
|
||||
putSelectFromParam();
|
||||
|
||||
// 콤보 로드 후 그리드 생성 (경합 방지)
|
||||
buildGrid();
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
function buildGrid() {
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json", mtype: 'POST', url: url,
|
||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||
colNames: ['id', 'No.', '제목', '메세지 내용', '수신자명', '수신자 정보', '유형', '상태', '요청시간', '발송시간'],
|
||||
colModel: [
|
||||
{name: 'id', align: 'center', key: true, hidden: true},
|
||||
{name: 'rowNum', align: 'center', width: 45, sortable: false},
|
||||
{name: 'subject', align: 'left', width: 160, formatter: formatSubject},
|
||||
{name: 'messageHtml', align: 'left', width: 300, formatter: formatMessage},
|
||||
{name: 'username', align: 'center', width: 80},
|
||||
{name: 'receiverInfo', align: 'left', width: 150, formatter: formatReceiverInfo},
|
||||
{name: 'messageType', align: 'center', width: 60},
|
||||
{name: 'requestStatus', align: 'center', width: 70},
|
||||
{name: 'requestDateShort', align: 'center', width: 110, formatter: formatRequestDate},
|
||||
{name: 'sentDateShort', align: 'center', width: 110, formatter: formatSentDate}
|
||||
],
|
||||
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 id = rowData['id'];
|
||||
var url2 = url_view + '?cmd=DETAIL';
|
||||
url2 += '&page=' + $(this).getGridParam("page");
|
||||
url2 += '&returnUrl=' + getReturnUrl();
|
||||
url2 += '&menuId=' + '${param.menuId}';
|
||||
url2 += '&id=' + id;
|
||||
goNav(url2);
|
||||
},
|
||||
loadComplete: function () {
|
||||
var page = $(this).getGridParam('page');
|
||||
var rowNum = $(this).getGridParam('rowNum');
|
||||
var rows = $(this).getDataIDs();
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var number = ((page - 1) * rowNum + i) + 1;
|
||||
$(this).setCell(rows[i], 'rowNum', number);
|
||||
}
|
||||
// 서버 고정 정렬(요청일 역순) — 컬럼 정렬 비활성
|
||||
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');
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function () {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("select[name^=search]").change(function () { $("#btn_search").click(); });
|
||||
$("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">발송 요청 이력을 조회합니다. 개인정보는 마스킹되어 표시됩니다.</span></div>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<colgroup>
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>상태</th>
|
||||
<td><div class="select-style"><select name="searchRequestStatus"></select></div></td>
|
||||
<th>메세지 코드</th>
|
||||
<td><div class="select-style"><select name="searchMessageCode"></select></div></td>
|
||||
<th>수신자명</th>
|
||||
<td><input type="text" name="searchRecipient" autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>유형</th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="searchMessageType">
|
||||
<option value="">전체</option>
|
||||
<option value="EMAIL">EMAIL</option>
|
||||
<option value="SMS">SMS</option>
|
||||
<option value="MESSENGER">MESSENGER</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<th>요청기간</th>
|
||||
<td colspan="3">
|
||||
<input type="date" name="searchFromDate"> ~ <input type="date" name="searchToDate">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,149 @@
|
||||
<%@ 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"/>
|
||||
<style>
|
||||
.mask-email { color:#1a73e8; font-weight:bold; }
|
||||
.mask-phone { color:#d93025; font-weight:bold; }
|
||||
.mask-digits { color:#9334e6; font-weight:bold; }
|
||||
.msg-detail { white-space:pre-wrap; word-break:break-all; min-height:80px; line-height:1.6; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/messagerequest/messageRequestMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/messagerequest/messageRequestMan.view" />';
|
||||
var key = '${param.id}';
|
||||
|
||||
function detail() {
|
||||
if (!key) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'DETAIL', id: key},
|
||||
success: function (data) {
|
||||
var codeText = (data.messageCodeName || '') + (data.messageCode ? ' (' + data.messageCode + ')' : '');
|
||||
$('#messageCodeName').text(codeText);
|
||||
$('#subject').text((data.subject && data.subject !== '') ? data.subject : (data.messageCode || ''));
|
||||
$('#messageType').text(data.messageType || '');
|
||||
$('#requestStatus').text(data.requestStatus || '');
|
||||
$('#username').text(data.username || '');
|
||||
$('#email').text(data.email || '');
|
||||
$('#phone').text(data.phone || '');
|
||||
$('#messengerId').text(data.messengerId || '');
|
||||
$('#umsUid').text(data.umsUid || '');
|
||||
$('#eaiInterfaceId').text(data.eaiInterfaceId || '');
|
||||
$('#serviceId').text(data.serviceId || '');
|
||||
$('#requestDate').text(data.requestDateFull || '');
|
||||
$('#sentDate').text(data.sentDateFull || '');
|
||||
// 서버에서 마스킹+escape 된 안전 HTML
|
||||
$('#messageHtml').html(data.messageHtml || '');
|
||||
|
||||
// PENDING 건만 '실패 처리' 노출 (권한 제어는 buttonControl 이 담당)
|
||||
if (data.requestStatus !== 'PENDING') {
|
||||
$('#btn_fail').hide();
|
||||
}
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
|
||||
buttonControl();
|
||||
detail();
|
||||
|
||||
$('#btn_fail').click(function () {
|
||||
if (!confirm('이 건을 FAILED(발송 실패) 상태로 변경하시겠습니까?')) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'UPDATE_STATUS', id: key},
|
||||
success: function () {
|
||||
alert("변경되었습니다.");
|
||||
detail();
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
});
|
||||
|
||||
$('#btn_previous').click(function () { goNav(returnUrl); });
|
||||
});
|
||||
</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_fail" level="W" status="DETAIL" style="background-color:#dc3545;border-color:#dc3545;color:#fff;"><i class="material-icons">report</i> 실패 처리</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" id="title">메세지 발송 내역 상세<span class="tooltip">개인정보는 마스킹되어 표시됩니다.</span></div>
|
||||
<table class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>메세지 코드</th>
|
||||
<td colspan="3"><span id="messageCodeName"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>제목</th>
|
||||
<td colspan="3"><span id="subject"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>유형</th>
|
||||
<td><span id="messageType"></span></td>
|
||||
<th>상태</th>
|
||||
<td><span id="requestStatus"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>수신자명</th>
|
||||
<td><span id="username"></span></td>
|
||||
<th>메신저 ID</th>
|
||||
<td><span id="messengerId"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>이메일</th>
|
||||
<td><span id="email"></span></td>
|
||||
<th>휴대폰</th>
|
||||
<td><span id="phone"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>요청 시간</th>
|
||||
<td><span id="requestDate"></span></td>
|
||||
<th>발송 시간</th>
|
||||
<td><span id="sentDate"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>EAI 인터페이스 ID</th>
|
||||
<td><span id="eaiInterfaceId"></span></td>
|
||||
<th>서비스 ID</th>
|
||||
<td><span id="serviceId"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>UMS UID</th>
|
||||
<td colspan="3"><span id="umsUid"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>메세지 내용</th>
|
||||
<td colspan="3"><div id="messageHtml" class="msg-detail"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -20,15 +20,35 @@
|
||||
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 == 'REVIEWING') {
|
||||
return '<span >문의검토중</span>'
|
||||
} else if (cellvalue == 'RESPONDED') {
|
||||
return '<span >답변완료</span>'
|
||||
} else if (cellvalue == 'CLOSED') {
|
||||
return '<span >답변종료</span>'
|
||||
} else {
|
||||
return cellvalue;
|
||||
}
|
||||
}
|
||||
|
||||
function formatVisibility(cellvalue, options, rowObject) {
|
||||
if (cellvalue == 'PRIVATE') {
|
||||
return '<span style="color:#c0392b; font-weight:bold;">비공개</span>';
|
||||
} else if (cellvalue == 'ALL') {
|
||||
return '전체공개';
|
||||
} else if (cellvalue == 'ORG') {
|
||||
return '법인공개';
|
||||
}
|
||||
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() {
|
||||
@@ -92,13 +112,14 @@
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData : { cmd : 'LIST' },
|
||||
colNames:['No.', 'id', '제목', '법인명', '상태', '작성자', '등록일', '답변자','답변일'],
|
||||
colNames:['No.', 'id', '제목', '법인명', '상태', '공개범위', '작성자', '등록일', '답변자','답변일'],
|
||||
colModel:[
|
||||
{ name : 'rowNum' , align:'center', width:50, sortable:false },
|
||||
{ name : 'id' , align:'center', key:true, hidden:true},
|
||||
{ name : 'inquirySubject' , align:'left' , width:200 , formatter: formatFile},
|
||||
{ name : 'orgName' , align:'center' , width:100 , },
|
||||
{ name : 'inquiryStatus' , align:'center', width:70, formatter: formatInquiryStatus },
|
||||
{ name : 'visibility' , align:'center', width:70, formatter: formatVisibility },
|
||||
{ name : 'inquirer.userName', align:'center', width:70 },
|
||||
{ name : 'createdDate' , align:'center', width:100, formatter: timeStampFormat },
|
||||
{ name : 'responderName' , align:'center', width:70 },
|
||||
@@ -200,7 +221,9 @@
|
||||
<select name="searchInquiryStatus">
|
||||
<option value="">전체</option>
|
||||
<option value="PENDING">문의중</option>
|
||||
<option value="REVIEWING">문의검토중</option>
|
||||
<option value="RESPONDED">답변완료</option>
|
||||
<option value="CLOSED">답변종료</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -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,17 +64,61 @@
|
||||
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;
|
||||
margin-left: 2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.reason-modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
}
|
||||
.reason-modal-box {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
width: 400px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.reason-modal-box h3 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.reason-modal-box textarea {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.reason-modal-actions {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
<script language="javascript">
|
||||
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';
|
||||
var originalVisibility = 'ORG';
|
||||
var reasonModalCallback = null;
|
||||
|
||||
function decodeHTMLEntities(text) {
|
||||
var textArea = document.createElement('textarea');
|
||||
@@ -82,10 +133,23 @@
|
||||
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 == 'REVIEWING') {
|
||||
$("#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 +158,26 @@
|
||||
var decodedContent = decodeHTMLEntities(data.responseDetail);
|
||||
$("#responseDetail").summernote('code', decodedContent || '');
|
||||
|
||||
// 공개범위 / 조회수
|
||||
$('#visibilitySelect').val(data.visibility || 'ORG');
|
||||
originalVisibility = data.visibility || 'ORG';
|
||||
$('#viewCount').text(data.viewCount || 0);
|
||||
|
||||
// 조회수 증가 (sessionStorage dedup — 새로고침 시 중복 증가 억제)
|
||||
var viewedKey = 'inquiry_viewed_' + key;
|
||||
if (!sessionStorage.getItem(viewedKey)) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {cmd: 'INCREMENT_VIEW', id: key},
|
||||
success: function () {
|
||||
sessionStorage.setItem(viewedKey, 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 댓글 표시
|
||||
listComment(key);
|
||||
|
||||
// 첨부 파일 표시
|
||||
displayAttachFiles(data.fileInfo);
|
||||
@@ -104,6 +188,159 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 isPrivate = comment.visibility === 'PRIVATE';
|
||||
var row = $('<div>').addClass('info-row');
|
||||
if (isPrivate) {
|
||||
// 비공개 댓글은 색상으로 구별
|
||||
row.css({'background-color': '#FFF5F5', 'border-radius': '4px', 'padding': '4px'});
|
||||
}
|
||||
var labelDiv = $('<div>').addClass('info-label').css('color', '#555').text(comment.writerName || '');
|
||||
if (isPrivate) {
|
||||
$('<span>').text(' 비공개').css({'color': '#c0392b', 'font-size': '0.85em', 'font-weight': 'bold'}).appendTo(labelDiv);
|
||||
}
|
||||
labelDiv.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);
|
||||
|
||||
// 댓글 공개범위 변경 컨트롤 (사유 필수)
|
||||
var visCtrl = $('<div>').css({'margin-top': '4px', 'font-size': '0.85em'});
|
||||
var sel = $('<select>').append('<option value="ALL">공개</option>').append('<option value="PRIVATE">비공개</option>');
|
||||
sel.val(comment.visibility || 'ALL');
|
||||
var reasonInput = $('<input>').attr({'type': 'text', 'placeholder': '변경 사유'}).css({'margin-left': '4px'});
|
||||
var applyBtn = $('<button>').attr('type', 'button').addClass('cssbtn smallBtn2').css({'margin-left': '4px', 'font-size': '0.85em', 'padding': '1px 6px'})
|
||||
.text('공개범위변경').on('click', (function(id, inquiryId, selEl, reasonEl) {
|
||||
return function() { changeCommentVisibility(id, inquiryId, selEl.val(), reasonEl.val()); };
|
||||
})(comment.id, comment.inquiryId, sel, reasonInput));
|
||||
visCtrl.append(sel).append(reasonInput).append(applyBtn);
|
||||
visCtrl.appendTo(content);
|
||||
|
||||
content.appendTo(row);
|
||||
row.appendTo($commentList);
|
||||
});
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openReasonModal(callback) {
|
||||
reasonModalCallback = callback;
|
||||
$('#reasonModalInput').val('');
|
||||
$('#reasonModal').show();
|
||||
$('#reasonModalInput').focus();
|
||||
}
|
||||
|
||||
function closeReasonModal() {
|
||||
$('#reasonModal').hide();
|
||||
reasonModalCallback = null;
|
||||
}
|
||||
|
||||
// 답변(summernote) 내용이 실제로 입력됐는지 확인
|
||||
function hasAnswerContent() {
|
||||
var v = $('#responseDetail').summernote('code');
|
||||
var text = $('<div>').html(v || '').text().replace(/ /g, ' ').trim();
|
||||
return text.length > 0;
|
||||
}
|
||||
|
||||
function saveInquiryAnswer(returnUrl) {
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
postData.push({name: "cmd", value: "INSERT"});
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: postData,
|
||||
success: function (json) {
|
||||
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 게시물 공개범위 변경(사유 필수). 성공 후 답변 내용이 있으면 이어서 저장.
|
||||
function changeInquiryVisibility(newVis, reason, returnUrl, thenSaveAnswer) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {cmd: 'VISIBILITY', serviceType: 'APIGW', targetType: 'INQUIRY', id: $('#id').val(),
|
||||
visibility: newVis, auditReason: reason},
|
||||
success: function () {
|
||||
originalVisibility = newVis;
|
||||
if (thenSaveAnswer) {
|
||||
saveInquiryAnswer(returnUrl);
|
||||
} else {
|
||||
alert('변경되었습니다.');
|
||||
goNav(returnUrl);
|
||||
}
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function changeCommentVisibility(commentId, inquiryId, visibility, reason) {
|
||||
if (!reason || !reason.trim()) {
|
||||
alert('공개범위 변경 사유를 입력하세요.');
|
||||
return;
|
||||
}
|
||||
if (!confirm('댓글 공개범위를 변경하시겠습니까?')) return;
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {cmd: 'VISIBILITY', serviceType: 'APIGW', targetType: 'COMMENT', id: commentId, visibility: visibility, auditReason: reason},
|
||||
success: function () {
|
||||
alert('변경되었습니다.');
|
||||
listComment(inquiryId);
|
||||
},
|
||||
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,25 +351,70 @@
|
||||
placeholder: '여기에 답변을 입력하세요.',
|
||||
height: 200
|
||||
});
|
||||
|
||||
|
||||
if (key) {
|
||||
detail(key);
|
||||
}
|
||||
|
||||
$("#btn_modify").click(function () {
|
||||
if (inquiryStatus == 'CLOSED') {
|
||||
alert('답변이 종료되어 수정할 수 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
var newVis = $('#visibilitySelect').val();
|
||||
var visChanged = (newVis !== originalVisibility);
|
||||
|
||||
if (visChanged) {
|
||||
// 공개범위 변경 감지 → 사유 입력 모달 (답변 내용이 있으면 함께 저장)
|
||||
var saveAnswerToo = hasAnswerContent();
|
||||
openReasonModal(function (reason) {
|
||||
changeInquiryVisibility(newVis, reason, returnUrl, saveAnswerToo);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 공개범위 변경 없음 → 기존 답변 저장
|
||||
if (!checkRequired("ajaxForm")) return;
|
||||
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
|
||||
saveInquiryAnswer(returnUrl);
|
||||
});
|
||||
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
postData.push({name: "cmd", value: "INSERT"});
|
||||
$("#reasonModalOk").click(function () {
|
||||
var reason = ($('#reasonModalInput').val() || '').trim();
|
||||
if (!reason) {
|
||||
alert('변경 사유를 입력하세요.');
|
||||
return;
|
||||
}
|
||||
var cb = reasonModalCallback;
|
||||
closeReasonModal();
|
||||
if (cb) cb(reason);
|
||||
});
|
||||
|
||||
$("#reasonModalCancel").click(function () {
|
||||
closeReasonModal();
|
||||
});
|
||||
|
||||
$("#btn_comment").click(function () {
|
||||
if (inquiryStatus == 'CLOSED') {
|
||||
alert('답변이 종료되어 댓글을 등록할 수 없습니다');
|
||||
return;
|
||||
}
|
||||
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: postData,
|
||||
data: {
|
||||
cmd: "INSERT_COMMENT",
|
||||
inquiryId: $('#id').val(),
|
||||
commentDetail: $('#commentDetail').val(),
|
||||
},
|
||||
success: function (json) {
|
||||
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
||||
goNav(returnUrl);
|
||||
$('#commentDetail').val('');
|
||||
listComment(key);
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
@@ -194,7 +476,22 @@
|
||||
<div class="info-label">작성일</div>
|
||||
<div id="createdDate" class="info-content"></div>
|
||||
</div>
|
||||
<div class="info-row" id="attachFileRow" style="display: none;">
|
||||
<div class="info-row">
|
||||
<div class="info-label">조회수</div>
|
||||
<div id="viewCount" class="info-content">0</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">공개범위</div>
|
||||
<div class="info-content" style="display:flex; gap:8px; align-items:center;">
|
||||
<select id="visibilitySelect">
|
||||
<option value="ALL">전체공개</option>
|
||||
<option value="ORG">법인공개</option>
|
||||
<option value="PRIVATE">비공개</option>
|
||||
</select>
|
||||
<span style="font-size:0.85em; color:#999;">변경 후 상단 [수정] 버튼을 누르면 사유 입력창이 표시됩니다.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-row" id="attachFileRow" style="display: none;">
|
||||
<div class="info-label">첨부파일</div>
|
||||
<div id="attachFiles" class="info-content"></div>
|
||||
</div>
|
||||
@@ -206,6 +503,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,9 +522,34 @@
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- 공개범위 변경 사유 입력 모달 -->
|
||||
<div id="reasonModal" class="reason-modal-overlay">
|
||||
<div class="reason-modal-box">
|
||||
<h3>공개범위 변경 사유</h3>
|
||||
<textarea id="reasonModalInput" placeholder="변경 사유를 입력하세요."></textarea>
|
||||
<div class="reason-modal-actions">
|
||||
<button type="button" class="cssbtn" id="reasonModalOk">확인</button>
|
||||
<button type="button" class="cssbtn" id="reasonModalCancel">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,550 @@
|
||||
<%@ 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" %>
|
||||
<%
|
||||
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"/>
|
||||
<jsp:include page="/jsp/common/include/portal.jsp"/>
|
||||
|
||||
<style>
|
||||
/* ── 스윔레인 보드 ── */
|
||||
.menu-board { display: flex; gap: 12px; align-items: flex-start; }
|
||||
.menu-lane-strip { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 10px; flex: 1 1 auto; }
|
||||
.menu-lane { background: #f1f3f5; border-radius: 10px; min-width: 230px; max-width: 230px; flex: 0 0 230px; }
|
||||
.menu-lane.unplaced { background: #fff4e6; border: 1px dashed #fd7e14; }
|
||||
.lane-header { display: flex; align-items: center; gap: 6px; padding: 10px 12px;
|
||||
border-bottom: 1px solid #dee2e6; cursor: grab; }
|
||||
.menu-lane.unplaced .lane-header { cursor: default; }
|
||||
.lane-title { font-weight: 700; font-size: 14px; flex: 1 1 auto; overflow: hidden;
|
||||
text-overflow: ellipsis; white-space: nowrap; }
|
||||
.lane-body { list-style: none; margin: 0; padding: 8px; min-height: 60px; }
|
||||
.menu-card { background: #fff; border: 1px solid #dee2e6; border-radius: 8px;
|
||||
padding: 8px 10px; margin-bottom: 8px; cursor: grab;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.06); }
|
||||
.menu-card.hidden-item, .menu-lane.hidden-item > .lane-header { opacity: 0.45; }
|
||||
.menu-card .card-name { font-size: 13px; font-weight: 600; }
|
||||
.menu-card .card-id { font-size: 11px; color: #868e96; }
|
||||
.menu-card .card-path { font-size: 11px; color: #495057; word-break: break-all; }
|
||||
.card-actions, .lane-actions { display: flex; gap: 4px; }
|
||||
.icon-btn { border: none; background: transparent; padding: 1px 3px; cursor: pointer;
|
||||
font-size: 14px; color: #495057; }
|
||||
.icon-btn:hover { color: #0d6efd; }
|
||||
.icon-btn.danger:hover { color: #dc3545; }
|
||||
.badge-src { font-size: 10px; }
|
||||
.role-check-group { display: flex; flex-wrap: wrap; gap: 4px 12px; }
|
||||
.role-check-group .form-check { min-width: 45%; }
|
||||
.dflt-hint { font-size: 11px; color: #868e96; }
|
||||
.board-toolbar { display: flex; gap: 6px; margin-bottom: 10px; align-items: center; }
|
||||
.board-toolbar .spacer { flex: 1 1 auto; }
|
||||
.sortable-placeholder { border: 2px dashed #adb5bd; border-radius: 8px; height: 48px;
|
||||
margin-bottom: 8px; background: #e9ecef; }
|
||||
.lane-placeholder { border: 2px dashed #adb5bd; border-radius: 10px; min-width: 230px; }
|
||||
</style>
|
||||
|
||||
<script language="javascript">
|
||||
// AuthorizeInterceptor 는 serviceType 을 "요청 파라미터"에서 읽으므로
|
||||
// 모든 .json 호출 URL 에 serviceType 을 부착한다 (누락 시 권한 조회 실패 → Access denied)
|
||||
var url = urlAddServiceType('<c:url value="/onl/apim/portalmenu/portalMenuMan.json" />');
|
||||
|
||||
// 화면 상태 (저장 버튼 클릭 시에만 서버 반영 — 드래그/토글은 클라이언트 상태만 변경)
|
||||
var state = { itemsById: {}, lanes: [], unplaced: [], roles: [] };
|
||||
var dirty = false;
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function loadAll(afterFn) {
|
||||
$.post(url, { cmd: 'LIST_ALL' }, function (data) {
|
||||
buildState(data);
|
||||
render();
|
||||
dirty = false;
|
||||
if (afterFn) afterFn();
|
||||
}, 'json').fail(ajaxFail);
|
||||
}
|
||||
|
||||
function buildState(data) {
|
||||
state.itemsById = {};
|
||||
state.roles = data.roles || [];
|
||||
(data.items || []).forEach(function (it) { state.itemsById[it.menuId] = it; });
|
||||
|
||||
var placedTop = [], childrenByParent = {}, unplaced = [];
|
||||
(data.items || []).forEach(function (it) {
|
||||
if (!it.placed) { unplaced.push(it.menuId); return; }
|
||||
if (!it.parentId) {
|
||||
placedTop.push(it);
|
||||
} else {
|
||||
(childrenByParent[it.parentId] = childrenByParent[it.parentId] || []).push(it);
|
||||
}
|
||||
});
|
||||
placedTop.sort(function (a, b) { return (a.sortOrder || 0) - (b.sortOrder || 0); });
|
||||
|
||||
state.lanes = placedTop.map(function (top) {
|
||||
var kids = (childrenByParent[top.menuId] || [])
|
||||
.sort(function (a, b) { return (a.sortOrder || 0) - (b.sortOrder || 0); })
|
||||
.map(function (c) { return { menuId: c.menuId, visibleYn: c.visibleYn || 'Y' }; });
|
||||
delete childrenByParent[top.menuId];
|
||||
return { menuId: top.menuId, visibleYn: top.visibleYn || 'Y', children: kids };
|
||||
});
|
||||
// 부모가 미배치/부재인 고아 배치는 미배치로 강등
|
||||
Object.keys(childrenByParent).forEach(function (pid) {
|
||||
childrenByParent[pid].forEach(function (c) { unplaced.push(c.menuId); });
|
||||
});
|
||||
state.unplaced = unplaced;
|
||||
}
|
||||
|
||||
// ── 렌더 ──
|
||||
function cardHtml(menuId, visibleYn, inLane) {
|
||||
var it = state.itemsById[menuId];
|
||||
if (!it) return '';
|
||||
var hidden = visibleYn === 'N';
|
||||
var srcBadge = it.sourceType === 'PORTAL'
|
||||
? '<span class="badge text-bg-secondary badge-src">기본</span>'
|
||||
: '<span class="badge text-bg-warning badge-src">커스텀</span>';
|
||||
var grpBadge = it.groupYn === 'Y' ? ' <span class="badge text-bg-info badge-src">그룹</span>' : '';
|
||||
var placeBtn = (!inLane && it.groupYn === 'Y')
|
||||
? '<button type="button" class="icon-btn" title="최상위 레인으로 배치" onclick="placeAsLane(\'' + menuId + '\')"><i class="bi bi-box-arrow-in-up"></i></button>'
|
||||
: '';
|
||||
var eyeBtn = inLane
|
||||
? '<button type="button" class="icon-btn" title="노출/숨김" onclick="toggleVisible(\'' + menuId + '\')"><i class="bi ' + (hidden ? 'bi-eye-slash' : 'bi-eye') + '"></i></button>'
|
||||
: '';
|
||||
return '<li class="menu-card' + (hidden ? ' hidden-item' : '') + '" data-menu-id="' + escapeHtml(menuId) + '">'
|
||||
+ '<div class="d-flex align-items-start">'
|
||||
+ '<div class="flex-grow-1">'
|
||||
+ '<div class="card-name">' + escapeHtml(it.menuName) + ' ' + srcBadge + grpBadge + '</div>'
|
||||
+ '<div class="card-id">' + escapeHtml(it.menuId) + '</div>'
|
||||
+ (it.menuPath ? '<div class="card-path">' + escapeHtml(it.menuPath) + '</div>' : '')
|
||||
+ '</div>'
|
||||
+ '<div class="card-actions">'
|
||||
+ placeBtn + eyeBtn
|
||||
+ '<button type="button" class="icon-btn" title="수정" onclick="openModal(\'' + menuId + '\')"><i class="bi bi-pencil"></i></button>'
|
||||
+ '<button type="button" class="icon-btn danger" level="W" title="' + (it.sourceType === 'PORTAL' ? '미배치로 이동' : '삭제') + '" onclick="minusItem(\'' + menuId + '\')"><i class="bi bi-dash-circle"></i></button>'
|
||||
+ '</div></div></li>';
|
||||
}
|
||||
|
||||
function laneHtml(lane) {
|
||||
var it = state.itemsById[lane.menuId];
|
||||
if (!it) return '';
|
||||
var hidden = lane.visibleYn === 'N';
|
||||
var sectionBadge = it.menuSection === 'MYPAGE'
|
||||
? '<span class="badge text-bg-primary badge-src">마이페이지</span>'
|
||||
: '<span class="badge text-bg-success badge-src">GNB</span>';
|
||||
var srcBadge = it.sourceType === 'PORTAL' ? '' : ' <span class="badge text-bg-warning badge-src">커스텀</span>';
|
||||
var cards = lane.children.map(function (c) { return cardHtml(c.menuId, c.visibleYn, true); }).join('');
|
||||
return '<div class="menu-lane' + (hidden ? ' hidden-item' : '') + '" data-menu-id="' + escapeHtml(lane.menuId) + '">'
|
||||
+ '<div class="lane-header lane-drag-handle">'
|
||||
+ '<span class="lane-title">' + escapeHtml(it.menuName) + '</span>'
|
||||
+ sectionBadge + srcBadge
|
||||
+ '<div class="lane-actions">'
|
||||
+ '<button type="button" class="icon-btn" title="노출/숨김" onclick="toggleVisible(\'' + lane.menuId + '\')"><i class="bi ' + (hidden ? 'bi-eye-slash' : 'bi-eye') + '"></i></button>'
|
||||
+ '<button type="button" class="icon-btn" title="수정" onclick="openModal(\'' + lane.menuId + '\')"><i class="bi bi-pencil"></i></button>'
|
||||
+ '<button type="button" class="icon-btn danger" level="W" title="레인 미배치" onclick="unplaceLane(\'' + lane.menuId + '\')"><i class="bi bi-dash-circle"></i></button>'
|
||||
+ '</div></div>'
|
||||
+ '<ul class="lane-body" data-lane-id="' + escapeHtml(lane.menuId) + '">' + cards + '</ul>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
function render() {
|
||||
var strip = state.lanes.map(laneHtml).join('');
|
||||
$('#laneStrip').html(strip);
|
||||
$('#unplacedBody').html(state.unplaced.map(function (id) { return cardHtml(id, 'Y', false); }).join(''));
|
||||
initSortables();
|
||||
buttonControl();
|
||||
}
|
||||
|
||||
function initSortables() {
|
||||
$('#laneStrip').sortable({
|
||||
items: '.menu-lane',
|
||||
handle: '.lane-drag-handle',
|
||||
placeholder: 'lane-placeholder',
|
||||
tolerance: 'pointer',
|
||||
update: function () { syncFromDom(); }
|
||||
});
|
||||
$('.lane-body, #unplacedBody').sortable({
|
||||
connectWith: '.lane-body, #unplacedBody',
|
||||
items: '.menu-card',
|
||||
placeholder: 'sortable-placeholder',
|
||||
tolerance: 'pointer',
|
||||
stop: function () { syncFromDom(); }
|
||||
});
|
||||
}
|
||||
|
||||
// 드래그 종료 후 DOM 순서 → 상태 동기화 (visible 값은 기존 상태 유지)
|
||||
function syncFromDom() {
|
||||
var visibleMap = {};
|
||||
state.lanes.forEach(function (l) {
|
||||
visibleMap[l.menuId] = l.visibleYn;
|
||||
l.children.forEach(function (c) { visibleMap[c.menuId] = c.visibleYn; });
|
||||
});
|
||||
|
||||
var lanes = [];
|
||||
$('#laneStrip .menu-lane').each(function () {
|
||||
var laneId = $(this).data('menu-id');
|
||||
var children = [];
|
||||
$(this).find('.lane-body .menu-card').each(function () {
|
||||
var id = $(this).data('menu-id');
|
||||
children.push({ menuId: id, visibleYn: visibleMap[id] || 'Y' });
|
||||
});
|
||||
lanes.push({ menuId: laneId, visibleYn: visibleMap[laneId] || 'Y', children: children });
|
||||
});
|
||||
var unplaced = [];
|
||||
$('#unplacedBody .menu-card').each(function () { unplaced.push($(this).data('menu-id')); });
|
||||
|
||||
state.lanes = lanes;
|
||||
state.unplaced = unplaced;
|
||||
dirty = true;
|
||||
render();
|
||||
}
|
||||
|
||||
// ── 카드/레인 조작 ──
|
||||
function toggleVisible(menuId) {
|
||||
state.lanes.forEach(function (l) {
|
||||
if (l.menuId === menuId) l.visibleYn = (l.visibleYn === 'N' ? 'Y' : 'N');
|
||||
l.children.forEach(function (c) {
|
||||
if (c.menuId === menuId) c.visibleYn = (c.visibleYn === 'N' ? 'Y' : 'N');
|
||||
});
|
||||
});
|
||||
dirty = true;
|
||||
render();
|
||||
}
|
||||
|
||||
function placeAsLane(menuId) {
|
||||
state.unplaced = state.unplaced.filter(function (id) { return id !== menuId; });
|
||||
state.lanes.push({ menuId: menuId, visibleYn: 'Y', children: [] });
|
||||
dirty = true;
|
||||
render();
|
||||
}
|
||||
|
||||
function minusItem(menuId) {
|
||||
var it = state.itemsById[menuId];
|
||||
if (!it) return;
|
||||
if (it.sourceType === 'PORTAL') {
|
||||
removeFromBoard(menuId);
|
||||
if (state.unplaced.indexOf(menuId) < 0) state.unplaced.push(menuId);
|
||||
dirty = true;
|
||||
render();
|
||||
} else {
|
||||
if (!confirm('커스텀 항목은 미배치 시 삭제됩니다.\n[' + it.menuName + '] 을(를) 삭제하시겠습니까?')) return;
|
||||
$.post(url, { cmd: 'DELETE', menuId: menuId }, function () {
|
||||
removeFromBoard(menuId);
|
||||
state.unplaced = state.unplaced.filter(function (id) { return id !== menuId; });
|
||||
delete state.itemsById[menuId];
|
||||
dirty = true;
|
||||
render();
|
||||
}, 'json').fail(ajaxFail);
|
||||
}
|
||||
}
|
||||
|
||||
function unplaceLane(laneId) {
|
||||
var lane = null;
|
||||
state.lanes = state.lanes.filter(function (l) {
|
||||
if (l.menuId === laneId) { lane = l; return false; }
|
||||
return true;
|
||||
});
|
||||
if (!lane) return;
|
||||
lane.children.forEach(function (c) {
|
||||
if (state.unplaced.indexOf(c.menuId) < 0) state.unplaced.push(c.menuId);
|
||||
});
|
||||
var it = state.itemsById[laneId];
|
||||
if (it && it.sourceType !== 'PORTAL') {
|
||||
if (confirm('커스텀 항목은 미배치 시 삭제됩니다.\n[' + it.menuName + '] 항목을 삭제하시겠습니까?')) {
|
||||
$.post(url, { cmd: 'DELETE', menuId: laneId }, function () {
|
||||
delete state.itemsById[laneId];
|
||||
render();
|
||||
}, 'json').fail(ajaxFail);
|
||||
} else {
|
||||
state.unplaced.push(laneId);
|
||||
}
|
||||
} else {
|
||||
if (state.unplaced.indexOf(laneId) < 0) state.unplaced.push(laneId);
|
||||
}
|
||||
dirty = true;
|
||||
render();
|
||||
}
|
||||
|
||||
function removeFromBoard(menuId) {
|
||||
state.lanes = state.lanes.filter(function (l) { return l.menuId !== menuId; });
|
||||
state.lanes.forEach(function (l) {
|
||||
l.children = l.children.filter(function (c) { return c.menuId !== menuId; });
|
||||
});
|
||||
}
|
||||
|
||||
// ── 저장 / 초기화 / 캐시 Reload ──
|
||||
function savePlacements() {
|
||||
// 미배치 상태의 커스텀 항목은 저장 시 삭제 처리 (스펙: 커스텀은 미배치=삭제)
|
||||
var unplacedAdmin = state.unplaced.filter(function (id) {
|
||||
var it = state.itemsById[id];
|
||||
return it && it.sourceType !== 'PORTAL';
|
||||
});
|
||||
if (unplacedAdmin.length > 0) {
|
||||
var names = unplacedAdmin.map(function (id) { return state.itemsById[id].menuName; }).join(', ');
|
||||
if (!confirm('미배치된 커스텀 항목은 저장 시 삭제됩니다: ' + names + '\n계속하시겠습니까?')) return;
|
||||
}
|
||||
|
||||
var rows = [];
|
||||
state.lanes.forEach(function (l, i) {
|
||||
rows.push({ menuId: l.menuId, parentId: null, sortOrder: (i + 1) * 10, visibleYn: l.visibleYn });
|
||||
l.children.forEach(function (c, j) {
|
||||
rows.push({ menuId: c.menuId, parentId: l.menuId, sortOrder: (j + 1) * 10, visibleYn: c.visibleYn });
|
||||
});
|
||||
});
|
||||
|
||||
var deleteCalls = unplacedAdmin.map(function (id) {
|
||||
return $.post(url, { cmd: 'DELETE', menuId: id });
|
||||
});
|
||||
$.when.apply($, deleteCalls).always(function () {
|
||||
$.ajax({
|
||||
url: url + '&cmd=TRANSACTION_PLACEMENT',
|
||||
type: 'POST',
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
data: JSON.stringify(rows)
|
||||
}).done(function () {
|
||||
dirty = false;
|
||||
loadAll(function () {
|
||||
if (confirm('배치가 저장되었습니다.\n포탈 메뉴 캐시를 즉시 반영(Reload)하시겠습니까?\n(미반영 시 캐시 TTL 후 자동 반영)')) {
|
||||
reloadPortalCache();
|
||||
}
|
||||
});
|
||||
}).fail(ajaxFail);
|
||||
});
|
||||
}
|
||||
|
||||
function initializeMenus() {
|
||||
if (!confirm('메뉴 초기화 시 포탈 기본(menu.yml) 값으로 복원되고\n커스텀 항목은 모두 삭제됩니다. 계속하시겠습니까?')) return;
|
||||
$.post(url, { cmd: 'INITIALIZE' }, function () {
|
||||
loadAll(function () { alert('메뉴가 초기화되었습니다.'); });
|
||||
}, 'json').fail(ajaxFail);
|
||||
}
|
||||
|
||||
function reloadPortalCache() {
|
||||
$.post(url, { cmd: 'TRANSACTION_RELOAD' }, function (result) {
|
||||
if (result && result.success) {
|
||||
alert('포탈 메뉴 캐시 리로드 완료 (항목 ' + result.itemCount + '건, ' + result.reloadedAt + ')');
|
||||
} else {
|
||||
alert('캐시 리로드 실패: ' + (result ? result.message : '알 수 없는 오류'));
|
||||
}
|
||||
}, 'json').fail(ajaxFail);
|
||||
}
|
||||
|
||||
function ajaxFail(xhr) {
|
||||
var msg = '처리 중 오류가 발생했습니다.';
|
||||
try {
|
||||
var body = JSON.parse(xhr.responseText);
|
||||
if (body.errorMsg) msg = body.errorMsg;
|
||||
else if (body.message) msg = body.message;
|
||||
} catch (e) { /* ignore */ }
|
||||
alert(msg + ' (HTTP ' + xhr.status + ')');
|
||||
}
|
||||
|
||||
// ── 추가/수정 모달 ──
|
||||
var menuModal;
|
||||
|
||||
function roleChecksHtml(namePrefix, checkedCsv) {
|
||||
var checked = (checkedCsv || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
||||
var html = '';
|
||||
var all = [{ roleCode: 'AUTHENTICATED', roleName: '로그인 사용자', roleType: '-' }].concat(state.roles);
|
||||
all.forEach(function (r) {
|
||||
var id = namePrefix + '_' + r.roleCode;
|
||||
html += '<div class="form-check form-check-sm">'
|
||||
+ '<input class="form-check-input" type="checkbox" name="' + namePrefix + '" id="' + id + '" value="' + escapeHtml(r.roleCode) + '"'
|
||||
+ (checked.indexOf(r.roleCode) >= 0 ? ' checked' : '') + '>'
|
||||
+ '<label class="form-check-label" for="' + id + '" style="font-size:12px;">'
|
||||
+ escapeHtml(r.roleName) + ' <span class="text-muted">(' + escapeHtml(r.roleCode) + ')</span></label>'
|
||||
+ '</div>';
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
function openModal(menuId) {
|
||||
var isNew = !menuId;
|
||||
var it = isNew ? { sourceType: 'ADMIN', groupYn: 'N', menuSection: 'GNB', newWindowYn: 'N' }
|
||||
: state.itemsById[menuId];
|
||||
if (!it) return;
|
||||
var isPortal = it.sourceType === 'PORTAL';
|
||||
|
||||
$('#modalTitle').text(isNew ? '메뉴 추가' : '메뉴 수정' + (isPortal ? ' (기본 항목)' : ''));
|
||||
$('#fMenuId').val(it.menuId || '').prop('readonly', !isNew);
|
||||
$('#fMenuName').val(it.menuName || '');
|
||||
$('#fMenuPath').val(it.menuPath || '');
|
||||
$('#fIconClass').val(it.iconClass || '');
|
||||
$('#fMenuSection').val(it.menuSection || 'GNB').prop('disabled', isPortal);
|
||||
$('#fGroupYn').prop('checked', it.groupYn === 'Y').prop('disabled', isPortal);
|
||||
$('#fNewWindowYn').prop('checked', it.newWindowYn === 'Y');
|
||||
$('#exposeRoleChecks').html(roleChecksHtml('exposeRole', it.exposeRoles));
|
||||
$('#accessRoleChecks').html(roleChecksHtml('accessRole', it.accessRoles));
|
||||
|
||||
// 기본 항목: 기본값(DFLT_*) 병기
|
||||
if (isPortal) {
|
||||
$('#hintMenuName').text('기본값: ' + (it.dfltMenuName || '-'));
|
||||
$('#hintMenuPath').text('기본값: ' + (it.dfltMenuPath || '-'));
|
||||
$('#hintExposeRoles').text('기본값: ' + (it.dfltExposeRoles || '전체 노출'));
|
||||
$('#hintAccessRoles').text('기본값: ' + (it.dfltAccessRoles || '제한 없음'));
|
||||
$('.dflt-hint').show();
|
||||
} else {
|
||||
$('.dflt-hint').hide();
|
||||
}
|
||||
|
||||
$('#modalMode').val(isNew ? 'INSERT' : 'UPDATE');
|
||||
menuModal.show();
|
||||
}
|
||||
|
||||
function submitModal() {
|
||||
var cmd = $('#modalMode').val();
|
||||
var data = {
|
||||
cmd: cmd,
|
||||
menuId: $('#fMenuId').val().trim(),
|
||||
menuName: $('#fMenuName').val().trim(),
|
||||
menuPath: $('#fMenuPath').val().trim(),
|
||||
iconClass: $('#fIconClass').val().trim(),
|
||||
menuSection: $('#fMenuSection').val(),
|
||||
groupYn: $('#fGroupYn').is(':checked') ? 'Y' : 'N',
|
||||
newWindowYn: $('#fNewWindowYn').is(':checked') ? 'Y' : 'N',
|
||||
exposeRoles: $('input[name=exposeRole]:checked').map(function () { return this.value; }).get().join(','),
|
||||
accessRoles: $('input[name=accessRole]:checked').map(function () { return this.value; }).get().join(',')
|
||||
};
|
||||
if (!data.menuId) { alert('메뉴 ID 를 입력하세요.'); return; }
|
||||
if (cmd === 'INSERT' && !/^[a-z0-9-]+$/.test(data.menuId)) {
|
||||
alert('메뉴 ID 는 kebab-case(소문자/숫자/하이픈)만 허용합니다.');
|
||||
return;
|
||||
}
|
||||
if (!data.menuName) { alert('노출명을 입력하세요.'); return; }
|
||||
|
||||
$.post(url, data, function () {
|
||||
menuModal.hide();
|
||||
loadAll();
|
||||
}, 'json').fail(ajaxFail);
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
menuModal = new bootstrap.Modal(document.getElementById('menuModal'));
|
||||
loadAll();
|
||||
|
||||
window.addEventListener('beforeunload', function (e) {
|
||||
if (dirty) { e.preventDefault(); e.returnValue = ''; }
|
||||
});
|
||||
});
|
||||
</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="title">포탈 메뉴 관리</div>
|
||||
|
||||
<div class="board-toolbar">
|
||||
<button type="button" class="btn btn-sm btn-primary" level="W" onclick="openModal()">
|
||||
<i class="bi bi-plus-lg"></i> 메뉴 추가</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" level="W" onclick="initializeMenus()">
|
||||
<i class="bi bi-arrow-counterclockwise"></i> 메뉴 초기화</button>
|
||||
<span class="spacer"></span>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" level="W" onclick="reloadPortalCache()">
|
||||
<i class="bi bi-arrow-repeat"></i> 캐시 Reload</button>
|
||||
<button type="button" class="btn btn-sm btn-success" level="W" onclick="savePlacements()">
|
||||
<i class="bi bi-save"></i> 저장</button>
|
||||
</div>
|
||||
|
||||
<div style="background:#e7f1ff; border:1px solid #0d6efd33; padding:8px 15px; margin-bottom:10px; border-radius:6px; font-size:12px; color:#31507a;">
|
||||
드래그로 위치를 조절한 뒤 <strong>저장</strong> 버튼으로 일괄 반영합니다.
|
||||
기본 항목([기본] 배지)은 [-] 시 <strong>미배치</strong>로만 이동하고, 커스텀 항목은 미배치 시 <strong>삭제</strong>됩니다.
|
||||
저장 후 <strong>캐시 Reload</strong> 를 실행해야 포탈에 즉시 반영됩니다(미실행 시 TTL 경과 후 반영).
|
||||
</div>
|
||||
|
||||
<div class="menu-board">
|
||||
<div class="menu-lane-strip" id="laneStrip"><!-- 렌더링 --></div>
|
||||
<div class="menu-lane unplaced">
|
||||
<div class="lane-header">
|
||||
<span class="lane-title">미배치</span>
|
||||
<span class="badge text-bg-warning badge-src">보관함</span>
|
||||
</div>
|
||||
<ul class="lane-body" id="unplacedBody"><!-- 렌더링 --></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 추가/수정 모달 -->
|
||||
<div class="modal fade" id="menuModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">메뉴 추가</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="modalMode" value="INSERT">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="fMenuId">메뉴 ID (kebab-case)</label>
|
||||
<input type="text" class="form-control form-control-sm" id="fMenuId" placeholder="ex) custom-link">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="fMenuName">노출명</label>
|
||||
<input type="text" class="form-control form-control-sm" id="fMenuName">
|
||||
<div class="dflt-hint" id="hintMenuName"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="fMenuPath">Path (그룹은 생략 가능)</label>
|
||||
<input type="text" class="form-control form-control-sm" id="fMenuPath" placeholder="/example 또는 https://...">
|
||||
<div class="dflt-hint" id="hintMenuPath"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="fIconClass">아이콘 클래스 (마이페이지용, ex: fa-users)</label>
|
||||
<input type="text" class="form-control form-control-sm" id="fIconClass">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="fMenuSection">섹션</label>
|
||||
<select class="form-select form-select-sm" id="fMenuSection">
|
||||
<option value="GNB">GNB (상단 메뉴)</option>
|
||||
<option value="MYPAGE">마이페이지</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="fGroupYn">
|
||||
<label class="form-check-label" for="fGroupYn">그룹(상위 메뉴)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="fNewWindowYn">
|
||||
<label class="form-check-label" for="fNewWindowYn">새 창 열기</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">노출 권한 (미선택 = 전체 노출)</label>
|
||||
<div class="role-check-group" id="exposeRoleChecks"></div>
|
||||
<div class="dflt-hint" id="hintExposeRoles"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">접근 권한 (미선택 = 제한 없음)</label>
|
||||
<div class="role-check-group" id="accessRoleChecks"></div>
|
||||
<div class="dflt-hint" id="hintAccessRoles"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" level="W" onclick="submitModal()">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,12 +17,57 @@
|
||||
<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;
|
||||
|
||||
// 코드 상수 — PortalNoticeUI 와 동기화
|
||||
var NOTICE_TYPE_INCIDENT = '3';
|
||||
|
||||
// IncidentState 라벨 — 상세 화면과 동일하게 유지
|
||||
var STATE_LABELS = {
|
||||
'INVESTIGATING': '조사중',
|
||||
'IDENTIFIED': '원인 파악',
|
||||
'MONITORING': '모니터링',
|
||||
'RESOLVED': '해소',
|
||||
'CANCELED': '취소'
|
||||
};
|
||||
|
||||
function formatIncidentState(cellvalue, options, rowObject) {
|
||||
if (!cellvalue) return '';
|
||||
var label = STATE_LABELS[cellvalue] || cellvalue;
|
||||
var closed = (cellvalue === 'RESOLVED' || cellvalue === 'CANCELED');
|
||||
return closed
|
||||
? '<span title="' + cellvalue + '">' + label + '</span>'
|
||||
: '<span style="color: red;" title="' + cellvalue + '">' + label + '</span>';
|
||||
}
|
||||
|
||||
function formatNoticeType(cellvalue, options, rowObject) {
|
||||
var name = cellvalue;
|
||||
if (combo && combo.noticeTypeList) {
|
||||
for (var i = 0; i < combo.noticeTypeList.length; i++) {
|
||||
if (combo.noticeTypeList[i].CODE == cellvalue) {
|
||||
name = combo.noticeTypeList[i].NAME;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 장애인데 종료 시각이 없으면 아직 진행 중이다
|
||||
if (cellvalue == NOTICE_TYPE_INCIDENT && rowObject && !rowObject.endAt) {
|
||||
return '<span style="color: red;">' + name + '-진행중</span>';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
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,22 +75,81 @@
|
||||
return cellvalue + icon;
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
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();
|
||||
|
||||
// 콤보(combo)가 채워진 뒤에 그리드를 생성해야 formatNoticeType 경합이 발생하지 않음
|
||||
buildGrid();
|
||||
},
|
||||
error:function(e){
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildGrid(){
|
||||
|
||||
$('#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:100, 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 : 'state' , align:'center', width:90, sortable:false, formatter: formatIncidentState },
|
||||
{ name : 'lastModifiedDate', align:'center', width:120 , formatter: timeStampFormat},
|
||||
{ name : 'createdDate' , align:'center', width:120, formatter: timeStampFormat },
|
||||
{ name : 'inquirerName' , align:'center', width:100 },
|
||||
@@ -62,6 +166,8 @@
|
||||
autowidth: true,
|
||||
viewrecords: true,
|
||||
rowList : eval('[${rmsDefaultRowList}]'),
|
||||
multiselect: true,
|
||||
multiboxonly: true,
|
||||
ondblClickRow: function(rowId) {
|
||||
var rowData = $(this).getRowData(rowId);
|
||||
var id = rowData['id'];
|
||||
@@ -99,6 +205,12 @@
|
||||
});
|
||||
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
// 콤보 로드 → 콤보 성공 콜백에서 buildGrid() 호출 (경합 방지)
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function(){
|
||||
var postData = getSearchForJqgrid("cmd","LIST");
|
||||
@@ -114,6 +226,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 +252,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 +285,6 @@
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<th style="width:180px;">제목/내용</th>
|
||||
<td><input type="text" name="searchSubjectDetail"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -19,12 +19,111 @@
|
||||
<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 = [];
|
||||
|
||||
// 장애 타임라인 (UI 상태)
|
||||
var currentIncidentId = null;
|
||||
var currentIncidentState = null;
|
||||
|
||||
// 종결 상태 — 재전이 불가 (재발은 새 장애로 등록)
|
||||
var TERMINAL_STATES = ['RESOLVED', 'CANCELED'];
|
||||
|
||||
// IncidentState 라벨 — 상태 콤보 문구와 동일하게 유지
|
||||
var STATE_LABELS = {
|
||||
'INVESTIGATING': '조사중',
|
||||
'IDENTIFIED': '원인 파악',
|
||||
'MONITORING': '모니터링',
|
||||
'RESOLVED': '해소',
|
||||
'CANCELED': '취소'
|
||||
};
|
||||
|
||||
function isIncidentType(t) {
|
||||
return t === NOTICE_TYPE_INCIDENT || t === NOTICE_TYPE_MAINTENANCE;
|
||||
}
|
||||
|
||||
function stateLabel(code) {
|
||||
if (!code) return '';
|
||||
return (STATE_LABELS[code] || code) + ' (' + code + ')';
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return $('<div>').text(text == null ? '' : text).html();
|
||||
}
|
||||
|
||||
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();
|
||||
$('.incident-endat-hint').show();
|
||||
} else {
|
||||
$('.incident-state-row').hide();
|
||||
$('.incident-endat-hint').hide();
|
||||
}
|
||||
// 장애는 종결(해소·취소) 후에만 종료 시각을 손볼 수 있다. 진행 중에는 상태를 따른다.
|
||||
var closed = $.inArray(currentIncidentState, TERMINAL_STATES) >= 0;
|
||||
$('#endAt').prop('readonly', t === NOTICE_TYPE_INCIDENT && !closed);
|
||||
$('.incident-endat-hint').text(closed
|
||||
? '※ 종결된 장애입니다. 실제 복구 시각으로 수정할 수 있습니다.'
|
||||
: '※ 타임라인에서 해소·취소로 전이하면 자동 입력됩니다.');
|
||||
} else {
|
||||
$('.incident-row').hide();
|
||||
$('.incident-state-row').hide();
|
||||
$('.incident-endat-hint').hide();
|
||||
$('#endAt').prop('readonly', false);
|
||||
}
|
||||
// 타임라인은 등록된 장애(수정 모드)에서만 다룬다
|
||||
if (t === NOTICE_TYPE_INCIDENT && isDetail && currentIncidentId) {
|
||||
$('#timelineSection').show();
|
||||
} else {
|
||||
$('#timelineSection').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function decodeHTMLEntities(text) {
|
||||
@@ -44,67 +143,278 @@
|
||||
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 applyStateChangeGuard() {
|
||||
var terminal = $.inArray(currentIncidentState, TERMINAL_STATES) >= 0;
|
||||
|
||||
$('#timelineStateChange').prop('disabled', terminal);
|
||||
if (terminal) {
|
||||
$('#timelineStateChange').prop('checked', false);
|
||||
$('#timelineNextState').prop('disabled', true).val('');
|
||||
}
|
||||
$('#timelineClosedHint').toggle(terminal);
|
||||
$('#timelineResolveHint').toggle(!terminal);
|
||||
|
||||
// 현재와 동일한 상태로는 전이할 수 없다
|
||||
$('#timelineNextState option').each(function() {
|
||||
var v = $(this).val();
|
||||
$(this).prop('disabled', v !== '' && v === currentIncidentState);
|
||||
});
|
||||
if ($('#timelineNextState').val() === currentIncidentState) {
|
||||
$('#timelineNextState').val('');
|
||||
}
|
||||
}
|
||||
|
||||
function loadTimeline() {
|
||||
if (!currentIncidentId) {
|
||||
renderTimeline([]);
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'TIMELINE_LIST', incidentId: currentIncidentId},
|
||||
success: function (rows) { renderTimeline(rows || []); },
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
function renderTimeline(rows) {
|
||||
var $tb = $('#timelineTbody');
|
||||
$tb.empty();
|
||||
if (!rows.length) {
|
||||
$tb.append('<tr><td colspan="6" style="text-align:center;color:#999;">등록된 타임라인이 없습니다.</td></tr>');
|
||||
return;
|
||||
}
|
||||
$.each(rows, function(_, row) {
|
||||
var author = row.authorType === 'SYSTEM' ? 'System' : 'Admin';
|
||||
var visible = row.visibleYn === 'Y';
|
||||
var $tr = $('<tr>');
|
||||
$tr.append('<td style="text-align:center;white-space:nowrap;">' + escapeHtml(row.eventAt) + '</td>');
|
||||
$tr.append('<td style="text-align:center;">' + author + '</td>');
|
||||
$tr.append('<td style="text-align:center;white-space:nowrap;">' + (row.stateAfter ? escapeHtml(stateLabel(row.stateAfter)) : '-') + '</td>');
|
||||
$tr.append('<td>' + escapeHtml(row.body).replace(/\n/g, '<br>') + '</td>');
|
||||
|
||||
// 현재 상태(라벨) + 눌렀을 때 바뀔 상태(버튼) 를 나눠 표기 — 버튼 하나면 의미가 모호하다
|
||||
var $visibleTd = $('<td style="text-align:center;white-space:nowrap;">');
|
||||
$('<span>')
|
||||
.text(visible ? '공개중' : '비공개')
|
||||
.css({color: visible ? '#1B8C4A' : '#999', 'margin-right': '5px'})
|
||||
.appendTo($visibleTd);
|
||||
$('<button type="button" class="cssbtn" level="W" status="DETAIL" style="min-width:0;padding:3px 8px;">')
|
||||
.text(visible ? '비공개로' : '공개로')
|
||||
.click(function() { toggleTimelineVisible(row.timelineId, visible ? 'N' : 'Y'); })
|
||||
.appendTo($visibleTd);
|
||||
$tr.append($visibleTd);
|
||||
|
||||
var $deleteTd = $('<td style="text-align:center;">');
|
||||
$('<button type="button" class="cssbtn smallBtn" level="W" status="DETAIL" style="min-width:0;width:90%;">')
|
||||
.text('삭제')
|
||||
.click(function() { deleteTimeline(row.timelineId); })
|
||||
.appendTo($deleteTd);
|
||||
$tr.append($deleteTd);
|
||||
|
||||
$tb.append($tr);
|
||||
});
|
||||
buttonControl(true);
|
||||
}
|
||||
|
||||
function addTimeline() {
|
||||
if (!currentIncidentId) {
|
||||
alert('장애 정보를 먼저 저장하여 주십시오.');
|
||||
return;
|
||||
}
|
||||
var body = $('#timelineBody').val();
|
||||
if (!body || !$.trim(body)) {
|
||||
alert('타임라인 메세지를 입력하여 주십시오.');
|
||||
$('#timelineBody').focus();
|
||||
return;
|
||||
}
|
||||
var stateChange = $('#timelineStateChange').is(':checked');
|
||||
var nextState = $('#timelineNextState').val();
|
||||
if (stateChange && !nextState) {
|
||||
alert('변경할 상태를 선택하여 주십시오.');
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'TIMELINE_ADD', incidentId: currentIncidentId, body: body,
|
||||
stateChange: stateChange, nextState: nextState},
|
||||
success: function (data) {
|
||||
$('#timelineBody').val('');
|
||||
$('#timelineStateChange').prop('checked', false).trigger('change');
|
||||
// 상태 전이 시 서버가 상태/이전 상태/종료 시각을 함께 갱신한다
|
||||
$('#stateText').text(data.state ? stateLabel(data.state) : '');
|
||||
$('#previousState').text(data.previousState ? stateLabel(data.previousState) : '없음');
|
||||
$('#endAt').val(data.endAt || '');
|
||||
currentIncidentState = data.state || null;
|
||||
applyStateChangeGuard();
|
||||
// 종결 전이 직후부터 종료 시각을 손볼 수 있어야 한다
|
||||
toggleIncidentFields();
|
||||
loadTimeline();
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
function toggleTimelineVisible(timelineId, visibleYn) {
|
||||
$.ajax({
|
||||
type: "POST", url: url,
|
||||
data: {cmd: 'TIMELINE_VISIBLE', timelineId: timelineId, visibleYn: visibleYn},
|
||||
success: loadTimeline,
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTimeline(timelineId) {
|
||||
if (confirm("<%= localeMessage.getString("common.checkDelete")%>") != true) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url,
|
||||
data: {cmd: 'TIMELINE_DELETE', timelineId: timelineId},
|
||||
success: loadTimeline,
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
/** 장애/점검 공지는 최초 게시 후 게시유형을 바꿀 수 없다. */
|
||||
function lockNoticeType(noticeType) {
|
||||
if (!isIncidentType(noticeType)) return;
|
||||
$('#noticeType').prop('disabled', true);
|
||||
if (!$('#noticeTypeHidden').length) {
|
||||
$('#ajaxForm').append('<input type="hidden" id="noticeTypeHidden" name="noticeType">');
|
||||
}
|
||||
$('#noticeTypeHidden').val(noticeType);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 파일 정보 표시
|
||||
// 게시유형은 최초 저장 후 변경 불가 (disabled 는 전송되지 않아 hidden 으로 값을 유지)
|
||||
lockNoticeType(data.noticeType);
|
||||
|
||||
// 장애/점검 필드
|
||||
$('#summary').val(data.summary || '');
|
||||
$('#startedAt').val(data.startedAt || '');
|
||||
$('#endAt').val(data.endAt || '');
|
||||
// 상태/이전 상태는 타임라인 게시로만 바뀐다 (직접 수정 불가)
|
||||
$('#stateText').text(data.state ? stateLabel(data.state) : '');
|
||||
$('#previousState').text(data.previousState ? stateLabel(data.previousState) : '없음');
|
||||
affectedApis = (data.affectedApis || []).map(function(a){
|
||||
return {apiId: a.apiId, apiName: a.apiName || ''};
|
||||
});
|
||||
renderAffectedApis();
|
||||
|
||||
currentIncidentId = data.incidentId || null;
|
||||
currentIncidentState = data.state || null;
|
||||
applyStateChangeGuard();
|
||||
toggleIncidentFields();
|
||||
if (data.noticeType === NOTICE_TYPE_INCIDENT && currentIncidentId) {
|
||||
loadTimeline();
|
||||
}
|
||||
|
||||
if (data.fileInfo) {
|
||||
fileInfo = {
|
||||
originalFileName: data.fileInfo.fileDetails[0].originalFileName,
|
||||
@@ -115,37 +425,28 @@
|
||||
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);
|
||||
|
||||
$('#timelineStateChange').on('change', function() {
|
||||
var checked = $(this).is(':checked');
|
||||
$('#timelineNextState').prop('disabled', !checked);
|
||||
if (!checked) $('#timelineNextState').val('');
|
||||
});
|
||||
$('#btn_addTimeline').click(addTimeline);
|
||||
|
||||
$('#fileInput').change(function(e) {
|
||||
var file = e.target.files[0];
|
||||
@@ -153,51 +454,71 @@
|
||||
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;
|
||||
}
|
||||
// datetime-local 값은 'YYYY-MM-DDTHH:mm' 이라 문자열 비교로 선후가 가려진다
|
||||
if ($('#endAt').val() && $('#endAt').val() < $('#startedAt').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 +526,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 +547,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,34 +558,141 @@
|
||||
<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="해소시 자동 채움">
|
||||
<div class="incident-endat-hint" style="display:none;color:#888;font-size:12px;margin-top:3px;">
|
||||
※ 타임라인에서 해소·취소로 전이하면 자동 입력됩니다.
|
||||
</div>
|
||||
<div id="attachFiles" style="margin: 5px 0px; display: inline-block;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 상태/이전 상태는 타임라인 게시로만 전이한다 (직접 수정 불가) -->
|
||||
<tr class="incident-row incident-state-row" style="display:none;">
|
||||
<th>상태</th>
|
||||
<td><span id="stateText"></span></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>
|
||||
</form>
|
||||
|
||||
<!-- 타임라인은 [수정] 저장과 별개로 게시 즉시 반영되므로 폼 밖에 둔다 -->
|
||||
<div id="timelineSection" style="display:none;margin-top:20px;">
|
||||
<div class="title">타임라인</div>
|
||||
<table class="table_row" cellspacing="0" style="width:100%;">
|
||||
<colgroup>
|
||||
<col style="width: 130px"/>
|
||||
<col style="width: 80px"/>
|
||||
<col style="width: 140px"/>
|
||||
<col/>
|
||||
<col style="width: 160px"/>
|
||||
<col style="width: 80px"/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>시간</th>
|
||||
<th>작성자</th>
|
||||
<th>상태</th>
|
||||
<th>메세지</th>
|
||||
<th>개발자포탈 공개</th>
|
||||
<th>삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="timelineTbody">
|
||||
<tr><td colspan="6" style="text-align:center;color:#999;">등록된 타임라인이 없습니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="margin-top:8px;display:flex;align-items:center;gap:8px;">
|
||||
<label>
|
||||
<input type="checkbox" id="timelineStateChange"> 상태 변경
|
||||
</label>
|
||||
<div class="select-style" style="width:220px;">
|
||||
<select id="timelineNextState" disabled>
|
||||
<option value="">(현재 상태 유지)</option>
|
||||
<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>
|
||||
<span id="timelineResolveHint" style="color:#888;font-size:12px;">※ 해소(RESOLVED)·취소(CANCELED)로 전이하면 종료 시각이 자동 입력됩니다.</span>
|
||||
<span id="timelineClosedHint" style="display:none;color:#C0392B;font-size:12px;">
|
||||
※ 종결된 장애입니다. 상태 변경 불가 — 재발 시 새 장애로 등록하십시오.
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top:5px;display:flex;gap:5px;">
|
||||
<textarea id="timelineBody" style="flex:1;height:60px;"
|
||||
placeholder="타임라인 메세지 입력 예 : 모니터링 5분간 정상 응답 확인 후 완료 상태 전이 예정"></textarea>
|
||||
<button type="button" class="cssbtn" id="btn_addTimeline" level="W" status="DETAIL">
|
||||
<i class="material-icons">send</i> 게시
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -312,7 +312,9 @@ $(document).ready(function() {
|
||||
<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> <%= localeMessage.getString("button.new") %></button>
|
||||
<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>
|
||||
<%--법인정보관리--%>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
|
||||
<%@ page import="java.io.*"%>
|
||||
<%@ page import="com.eactive.eai.rms.common.util.CommonUtil"%>
|
||||
<%@ page import="com.eactive.eai.rms.common.context.MonitoringContext"%>
|
||||
<%@ 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" %>
|
||||
@@ -7,6 +9,9 @@
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
|
||||
MonitoringContext monitoringContext = (MonitoringContext)CommonUtil.getBean(request, "monitoringContext");
|
||||
String reverseProxyUrl = monitoringContext.getStringProperty(MonitoringContext.RMS_WEBHOOK_REVERSE_PROXY_URL, "");
|
||||
%>
|
||||
<%!
|
||||
public String getRequiredLabel(String label) {
|
||||
@@ -133,10 +138,15 @@
|
||||
$("#btn_delete").hide();
|
||||
}
|
||||
|
||||
$("#approvalStatus").val(portalOrgData.approvalStatus);
|
||||
$("select[name=approvalStatus]").val(portalOrgData.approvalStatus);
|
||||
// 승인상태는 읽기전용(수정 불가) - 값은 그대로 제출되도록 disabled 대신 잠금 처리
|
||||
$("select[name=approvalStatus]")
|
||||
.css({'pointer-events':'none', 'background-color':'#eee'})
|
||||
.attr('tabindex', '-1');
|
||||
$("#compRegNo").val(formatCompanyRegNo(portalOrgData.compRegNo));
|
||||
$("#corpRegNo").val(formatCorpRegNo(portalOrgData.corpRegNo));
|
||||
$("#createdDate").inputmask("9999-99-99 99:99:99", {'autoUnmask': true});
|
||||
$("#reverseProxyPath").val(portalOrgData.reverseProxyPath);
|
||||
|
||||
// 전화번호 처리
|
||||
if (portalOrgData.scPhoneNumber) {
|
||||
@@ -404,47 +414,6 @@
|
||||
downloadFile(fileId, '1'); // fileSn은 '1'로 가정
|
||||
});
|
||||
|
||||
// OBP 파트너코드 조회 버튼 클릭
|
||||
$("#btn_query_obp").click(function() {
|
||||
var compRegNo = $("#compRegNo").val();
|
||||
if (!compRegNo) {
|
||||
jSuites.notification({ name: '알림', message: '사업자등록번호를 먼저 입력해주세요.', error: true });
|
||||
$("#compRegNo").focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 하이픈 제거
|
||||
compRegNo = compRegNo.replace(/-/g, '');
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
dataType: "json",
|
||||
data: {
|
||||
cmd: 'QUERY_OBP_PARTNER_CODE',
|
||||
compRegNo: compRegNo
|
||||
},
|
||||
beforeSend: function() {
|
||||
$("#btn_query_obp").prop("disabled", true).html('<i class="material-icons">hourglass_empty</i> 조회중...');
|
||||
},
|
||||
success: function(json) {
|
||||
if (json.success) {
|
||||
$("#orgCode").val(json.partnerCode);
|
||||
jSuites.notification({ name: '성공', message: '파트너코드가 조회되었습니다: ' + json.partnerCode });
|
||||
} else {
|
||||
jSuites.notification({ name: '실패', message: json.message, error: true });
|
||||
}
|
||||
},
|
||||
error: function(e) {
|
||||
jSuites.notification({ name: '오류', message: '파트너코드 조회 중 오류가 발생했습니다.', error: true });
|
||||
console.error(e.responseText);
|
||||
},
|
||||
complete: function() {
|
||||
$("#btn_query_obp").prop("disabled", false).html('<i class="material-icons">search</i> 파트너코드 조회');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#btn_modify").click(function(){
|
||||
if (!checkRequired("ajaxForm")) return;
|
||||
|
||||
@@ -515,8 +484,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (confirm("<%= localeMessage.getString("common.checkSave")%>") !== true)
|
||||
return;
|
||||
// 저장 확인/사유 입력은 유효성 검사 및 FormData 구성 후 처리 (하단 submit 분기)
|
||||
|
||||
// FormData 생성 전에 분리된 전화번호 필드 제거
|
||||
$("#scPhonePrefix, #scPhoneMiddle, #scPhoneLast, #orgPhonePrefix, #orgPhoneMiddle, #orgPhoneLast").removeAttr('name');
|
||||
@@ -551,23 +519,47 @@
|
||||
}
|
||||
|
||||
|
||||
// cmd 파라미터 추가
|
||||
formData.append("cmd", isDetail ? "UPDATE" : "INSERT");
|
||||
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url:url,
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success:function(json){
|
||||
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error:function(e){
|
||||
alert(e.responseText);
|
||||
// cmd 및 사유(auditReason) 첨부 후 전송
|
||||
function submitOrg(cmd, auditReason) {
|
||||
formData.append("cmd", cmd);
|
||||
// 감사로그(AUDIT_LOG) 발화를 위한 systemCode. 법인/가입자 리포지토리는 고정 EMS 스키마라 스키마 전환 영향 없음
|
||||
formData.append("serviceType", "APIGW");
|
||||
if (auditReason) {
|
||||
formData.append("auditReason", auditReason);
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url:url,
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success:function(json){
|
||||
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error:function(e){
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isDetail) {
|
||||
// 수정: 상태를 REMOVED로 바꾼 경우 삭제(cmd=DELETE)로 감사 기록 + 익명화
|
||||
var targetCmd = ($("#orgStatus").val() === 'REMOVED') ? "DELETE" : "UPDATE";
|
||||
var isRemove = (targetCmd === "DELETE");
|
||||
showReasonPrompt(
|
||||
isRemove ? "해당 법인을 삭제(탈퇴) 처리합니다. 사유를 입력해 주세요." : "법인 정보를 수정합니다. 사유를 입력해 주세요.",
|
||||
{
|
||||
title: isRemove ? "삭제 사유" : "수정 사유",
|
||||
onConfirm: function(reason) { submitOrg(targetCmd, reason); }
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// 신규 등록: 사유 불필요
|
||||
if (confirm("<%= localeMessage.getString("common.checkSave")%>") !== true) return;
|
||||
submitOrg("INSERT", null);
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_previous").click(function() {
|
||||
@@ -576,25 +568,28 @@
|
||||
|
||||
$("#btn_delete").click(function(){
|
||||
|
||||
if(confirm("<%= localeMessage.getString("common.confirmMsg")%>")){
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
postData.push({name: "cmd" , value:"DELETE"});
|
||||
showReasonPrompt("해당 법인을 삭제 처리합니다. 사유를 입력해 주세요.", {
|
||||
title: "삭제 사유",
|
||||
onConfirm: function(reason) {
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
postData.push({name: "cmd" , value:"DELETE"});
|
||||
postData.push({name: "serviceType", value: "APIGW"});
|
||||
postData.push({name: "auditReason", value: reason});
|
||||
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url:url,
|
||||
data:postData,
|
||||
success:function(args){
|
||||
alert("<%= localeMessage.getString("common.deleteMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error:function(e){
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url:url,
|
||||
data:postData,
|
||||
success:function(args){
|
||||
alert("<%= localeMessage.getString("common.deleteMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error:function(e){
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -630,10 +625,7 @@
|
||||
<tr>
|
||||
<th style="width:15%;"><%= localeMessage.getString("portalOrg.orgCode") %></th>
|
||||
<td colspan="3" style="width:35%;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input type="text" name="orgCode" id="orgCode" style="width: 200px; margin-right: 10px;"/>
|
||||
<button type="button" class="cssbtn smallBtn" id="btn_query_obp"><i class="material-icons">search</i> 파트너코드 조회</button>
|
||||
</div>
|
||||
<input type="text" name="orgCode" id="orgCode"/>
|
||||
</td>
|
||||
<th><%= localeMessage.getString("portalOrg.orgIndustryType") %></th> <%--업종--%>
|
||||
<td><input type="text" name="orgIndustryType" id="orgIndustryType" maxlength="50" /></td>
|
||||
@@ -738,6 +730,16 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("portalOrg.reverseProxyPath") %></th>
|
||||
<td colspan="3">
|
||||
<%=reverseProxyUrl %>/<input type="text" name="reverseProxyPath" id="reverseProxyPath" style="width:100px; "/>
|
||||
</td>
|
||||
<th></th>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr><%--사업자등록증--%>
|
||||
<th><%= localeMessage.getString("portalOrg.compRegFile") %></th>
|
||||
<td colspan="5" style="padding-top: 5px; padding-bottom: 5px;">
|
||||
|
||||
@@ -26,7 +26,38 @@ function formatFile(cellvalue, options, rowObject) {
|
||||
return cellvalue + icon;
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
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('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#grid').jqGrid({
|
||||
datatype:"json",
|
||||
mtype: 'POST',
|
||||
@@ -45,7 +76,7 @@ $(document).ready(function() {
|
||||
],
|
||||
jsonReader: {
|
||||
repeatitems:false
|
||||
},
|
||||
},
|
||||
pager : $('#pager'),
|
||||
page : '${param.page}',
|
||||
rowNum : '${rmsDefaultRowNum}',
|
||||
@@ -54,6 +85,8 @@ $(document).ready(function() {
|
||||
autowidth: true,
|
||||
viewrecords: true,
|
||||
rowList : eval('[${rmsDefaultRowList}]'),
|
||||
multiselect: true,
|
||||
multiboxonly: true,
|
||||
ondblClickRow: function(rowId) {
|
||||
var rowData = $(this).getRowData(rowId);
|
||||
var id = rowData['id'];
|
||||
@@ -94,10 +127,14 @@ $(document).ready(function() {
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
|
||||
$("#btn_search").click(function(){
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
$("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("#btn_delete_selected").click(function(){
|
||||
deleteSelectedRows();
|
||||
});
|
||||
|
||||
$("input[name^=search]").keydown(function(key){
|
||||
if (key.keyCode == 13){
|
||||
$("#btn_search").click();
|
||||
@@ -120,8 +157,9 @@ $(document).ready(function() {
|
||||
<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>
|
||||
<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">사업제휴 신청 목록</div>
|
||||
<div class="title">피드백/개선요청 신청 목록</div>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<tbody>
|
||||
|
||||
@@ -92,6 +92,27 @@ $(document).ready(function() {
|
||||
goNav(returnUrl);//LIST로 이동
|
||||
});
|
||||
|
||||
$("#btn_delete").click(function(){
|
||||
if (!confirm('선택한 항목을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
dataType: "json",
|
||||
data: { cmd: 'DELETE', id: key },
|
||||
success: function(data) {
|
||||
alert('삭제되었습니다.');
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
goNav(returnUrl);//LIST로 이동
|
||||
},
|
||||
error: function(e) {
|
||||
alert('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#btn_unmask").click(function () {
|
||||
const reason = prompt("마스킹 해제 사유를 입력하세요:");
|
||||
|
||||
@@ -134,9 +155,10 @@ $(document).ready(function() {
|
||||
<div class="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_unmask" level="W" status="DETAIL"><i class="material-icons">lock_open</i> 마스킹해제</button>
|
||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 삭제</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">사업제휴 신청 상세</div>
|
||||
<div class="title">피드백/개선요청 신청 상세</div>
|
||||
|
||||
<table id="grid" ></table>
|
||||
<div id="pager"></div>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
var url = '<c:url value="/onl/apim/portalproperty/propertyMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/portalproperty/propertyMan.view" />';
|
||||
var lastsel2;
|
||||
var clickedCol; //클릭한 셀의 컬럼 인덱스(포커스 이동용)
|
||||
|
||||
function isValid() {
|
||||
if ($('input[name=prptyGroupName]').val() == "") {
|
||||
@@ -86,9 +87,11 @@
|
||||
editurl: "clientArray",
|
||||
colNames: ['<%= localeMessage.getString("propertyDetail.propertyKey") %>',
|
||||
'<%= localeMessage.getString("propertyDetail.propertyValue") %>',
|
||||
'설명',
|
||||
'<%= localeMessage.getString("propertyDetail.delYn") %>'],
|
||||
colModel: [{name: 'PRPTYNAME', width: 50, align: 'left', editable: true},
|
||||
{name: 'PRPTY2VAL', width: 200, align: 'left', editable: true},
|
||||
{name: 'PRPTY2VAL', width: 150, align: 'left', editable: true},
|
||||
{name: 'PRPTYDESC', width: 200, align: 'left', editable: true, edittype: 'textarea', editoptions: {rows: 2}},
|
||||
{
|
||||
name: 'DELETEYN',
|
||||
width: 20,
|
||||
@@ -101,6 +104,10 @@
|
||||
},
|
||||
loadComplete: function () {
|
||||
},
|
||||
onCellSelect: function (rowid, iCol) {
|
||||
//onSelectRow보다 먼저 발화 → 클릭한 컬럼 인덱스 저장
|
||||
clickedCol = iCol;
|
||||
},
|
||||
onSelectRow: function (rowid, status) {
|
||||
if (lastsel2 != undefined) {
|
||||
if ($("#grid tr#" + lastsel2).attr("editable") == 1) { //editable=1 means row in edit mode
|
||||
@@ -108,7 +115,8 @@
|
||||
}
|
||||
}
|
||||
$('#grid').restoreRow(lastsel2);
|
||||
$('#grid').editRow(rowid, true);
|
||||
//focusField로 클릭한 셀에 포커스(미지정 시 첫 편집 컬럼으로 감)
|
||||
$('#grid').editRow(rowid, {keys: true, focusField: clickedCol});
|
||||
lastsel2 = rowid;
|
||||
},
|
||||
onSortCol: function () {
|
||||
@@ -258,6 +266,7 @@
|
||||
var data = new Object();
|
||||
data["PRPTYNAME"] = $("input[name=prptyName]").val();
|
||||
data["PRPTY2VAL"] = $("input[name=prpty2Val]").val();
|
||||
data["PRPTYDESC"] = $("input[name=prptyDesc]").val();
|
||||
|
||||
var rows = $("#grid")[0].rows;
|
||||
var index = Number($(rows[rows.length - 1]).attr("id"));
|
||||
@@ -340,6 +349,11 @@
|
||||
<td colspan="3"><input type="text" name="prpty2Val"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>설명</th>
|
||||
<td colspan="3"><input type="text" name="prptyDesc"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- grid -->
|
||||
<table id="grid"></table>
|
||||
|
||||
@@ -29,6 +29,16 @@
|
||||
// 원본 행 데이터를 ID로 조회하기 위한 맵
|
||||
var gridRowDataMap = {};
|
||||
|
||||
// 휴대폰번호 중복 조회 모드 여부
|
||||
var isDupView = false;
|
||||
|
||||
// 현재 검색조건 + 중복 조회 모드 플래그를 합쳐 LIST 요청 파라미터를 생성
|
||||
function buildListPostData() {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
postData.onlyDuplicateMobile = isDupView;
|
||||
return postData;
|
||||
}
|
||||
|
||||
/* 선택된 사용자 삭제 - 상태에 따라 Soft/Hard Delete 자동 분기 */
|
||||
function deleteSelectedUsers() {
|
||||
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
|
||||
@@ -166,6 +176,13 @@
|
||||
|
||||
$('#grid').jqGrid('setColProp', 'approvalStatus', {editoptions: {value: select_approvalStatus}});
|
||||
$('#grid').jqGrid('setColProp', 'userStatus', {editoptions: {value: select_userStatus}});
|
||||
|
||||
// 휴대폰번호 중복 허용안함(기능 ON) + 중복 사용자 존재 시 배너 노출
|
||||
if (json.mobileDuplicateCheckEnabled && json.mobileDuplicateUserCount > 0) {
|
||||
$("#dupMobileCount").text(json.mobileDuplicateUserCount);
|
||||
$("#dupMobileBanner").show();
|
||||
}
|
||||
|
||||
$('#grid').trigger('reloadGrid');
|
||||
|
||||
},
|
||||
@@ -176,7 +193,7 @@
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
var gridPostData = getSearchForJqgrid("cmd", "LIST");
|
||||
var gridPostData = buildListPostData();
|
||||
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json",
|
||||
@@ -188,7 +205,7 @@
|
||||
'<%= localeMessage.getString("portalUser.orgName") %>',
|
||||
'<%= localeMessage.getString("portalUser.name") %>',
|
||||
'<%= localeMessage.getString("portalUser.userId") %>',
|
||||
<%--'<%= localeMessage.getString("portalUser.mobile") %>',--%>
|
||||
'<%= localeMessage.getString("portalUser.mobileNumber") %>',
|
||||
'<%= localeMessage.getString("portalUser.roleName") %>',
|
||||
'<%= localeMessage.getString("portalUser.userStatus") %>',
|
||||
'<%= localeMessage.getString("portalUser.createOn") %>',
|
||||
@@ -201,7 +218,7 @@
|
||||
{name: 'portalOrgUIs.orgName', align: 'center', width: "120"},
|
||||
{name: 'userName', align: 'center', width: "80" },
|
||||
{name: 'loginId', align: 'center', width: "150" },
|
||||
// {name: 'mobileNumber', align: 'center', width: "100" },
|
||||
{name: 'mobileNumber', align: 'center', width: "110", hidden: true},
|
||||
{name: 'roleCodeDescription', align: 'center', width: "80"},
|
||||
{name: 'userStatusDescription', align: 'center', width: "50"},
|
||||
{name: 'createdDate', align: 'center', width: "100", formatter: timeStampFormat},
|
||||
@@ -274,9 +291,20 @@
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function () {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: buildListPostData(), page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
// 중복 휴대폰번호 사용자만 보기 / 전체 보기 토글
|
||||
$("#btn_dup_toggle").click(function () {
|
||||
isDupView = !isDupView;
|
||||
if (isDupView) {
|
||||
$(this).text("전체 보기");
|
||||
$("#grid").jqGrid('showCol', 'mobileNumber');
|
||||
} else {
|
||||
$(this).text("중복만 보기");
|
||||
$("#grid").jqGrid('hideCol', 'mobileNumber');
|
||||
}
|
||||
$("#grid").setGridParam({url: url, postData: buildListPostData(), page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("#btn_new").click(function () {
|
||||
@@ -319,9 +347,11 @@
|
||||
<button type="button" class="cssbtn" id="btn_new" level="W"><i
|
||||
class="material-icons">add</i> <%= localeMessage.getString("button.new") %>
|
||||
</button>
|
||||
<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>
|
||||
@@ -334,6 +364,11 @@
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div id="dupMobileBanner" style="display:none; background:#f8d7da; border:1px solid #dc3545; padding:8px 15px; margin:5px 0; border-radius:4px; color:#721c24;">
|
||||
<strong>⚠ 중복 휴대폰번호 사용자 <span id="dupMobileCount">0</span>건 발견</strong>
|
||||
<button type="button" class="cssbtn" id="btn_dup_toggle" style="margin-left:10px;">중복만 보기</button>
|
||||
</div>
|
||||
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -365,15 +400,16 @@
|
||||
<td><input type="text" name="searchUserName"></td>
|
||||
|
||||
<th style="width:10%; min-width:100px;">
|
||||
<%= localeMessage.getString("portalUser.userId") %>
|
||||
<%= localeMessage.getString("portalUser.userId") %><span style="color:#dc3545;">(*)</span>
|
||||
</th>
|
||||
<td><input type="text" name="searchUserId" value="${param.searchUserId}"></td>
|
||||
<th style="width:10%; min-width:100px;"><%= localeMessage.getString("portalUser.mobileNumber") %>
|
||||
<th style="width:10%; min-width:100px;"><%= localeMessage.getString("portalUser.mobileNumber") %><span style="color:#dc3545;">(*)</span>
|
||||
</th>
|
||||
<td><input type="text" name="searchMobileNumber" value="${param.searchMobileNumber}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="color:#dc3545; font-size:12px; margin:4px 2px;">(*) 암호화 항목 · 부분검색 불가(전체 일치)</div>
|
||||
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
|
||||
@@ -165,6 +165,10 @@
|
||||
$("#userStatus").val(data.userStatus);
|
||||
$("#roleCode").val(data.roleCode).trigger('change');
|
||||
$("#approvalStatus").val(data.approvalStatus);
|
||||
// 승인상태는 읽기전용(수정 불가) - 값은 그대로 제출되도록 disabled 대신 잠금 처리
|
||||
$("#approvalStatus")
|
||||
.css({'pointer-events':'none', 'background-color':'#eee'})
|
||||
.attr('tabindex', '-1');
|
||||
|
||||
// 탈퇴 상태 처리
|
||||
if (originalUserStatus === STATUS.REMOVED) {
|
||||
@@ -281,7 +285,7 @@
|
||||
}
|
||||
|
||||
const fullLoginId = id + "@" + domain;
|
||||
const emailPattern = /^[A-Za-z0-9._-]*@[A-Za-z0-9][-A-Za-z0-9.]+\.[A-Za-z]{2,}$/;
|
||||
const emailPattern = /^[A-Za-z0-9._%+-]*@[A-Za-z0-9][-A-Za-z0-9.]+\.[A-Za-z]{2,}$/;
|
||||
if (!emailPattern.test(fullLoginId)) {
|
||||
alert(fullLoginId + " 은 올바른 이메일 형식이 아닙니다.");
|
||||
return;
|
||||
@@ -313,7 +317,7 @@
|
||||
}
|
||||
|
||||
function validateEmailId(input) {
|
||||
input.value = input.value.replace(/[^A-Za-z0-9._-]/g, '');
|
||||
input.value = input.value.replace(/[^A-Za-z0-9._%+-]/g, '');
|
||||
}
|
||||
|
||||
function validateDomainInput(input) {
|
||||
@@ -488,29 +492,49 @@
|
||||
formData.push({name: 'mobileNumber', value: mobileNumber});
|
||||
}
|
||||
|
||||
formData.push({
|
||||
name: 'cmd',
|
||||
value: isDetail ? "UPDATE" : "INSERT"
|
||||
});
|
||||
|
||||
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: formData,
|
||||
success: function (json) {
|
||||
if (json.status === "fail") {
|
||||
alert(json.errorMsg || "저장에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
// cmd 및 사유(auditReason) 첨부 후 전송
|
||||
function submitUser(cmd, auditReason) {
|
||||
formData.push({ name: 'cmd', value: cmd });
|
||||
// 감사로그(AUDIT_LOG) 발화를 위한 systemCode. 가입자 리포지토리는 고정 EMS 스키마라 스키마 전환 영향 없음
|
||||
formData.push({ name: 'serviceType', value: 'APIGW' });
|
||||
if (auditReason) {
|
||||
formData.push({ name: 'auditReason', value: auditReason });
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: formData,
|
||||
success: function (json) {
|
||||
if (json.status === "fail") {
|
||||
alert(json.errorMsg || "저장에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
alert("<%= localeMessage.getString("common.saveMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isDetail) {
|
||||
// 수정: 상태를 REMOVED로 바꾼 경우 삭제(cmd=DELETE)로 감사 기록 + 익명화
|
||||
var targetCmd = ($("#userStatus").val() === 'REMOVED') ? "DELETE" : "UPDATE";
|
||||
var isRemove = (targetCmd === "DELETE");
|
||||
showReasonPrompt(
|
||||
isRemove ? "해당 가입자를 삭제(탈퇴) 처리합니다. 사유를 입력해 주세요." : "가입자 정보를 수정합니다. 사유를 입력해 주세요.",
|
||||
{
|
||||
title: isRemove ? "삭제 사유" : "수정 사유",
|
||||
onConfirm: function(reason) { submitUser(targetCmd, reason); }
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// 신규 등록: 사유 불필요. 초기 패스워드는 비밀번호 초기화와 동일(이메일ID + subfix)
|
||||
if (!confirm("가입자를 등록하시겠습니까?\n초기 패스워드는 '이메일ID${passwordInitSubfix}' 입니다.")) return;
|
||||
submitUser("INSERT", null);
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_previous").click(function () {
|
||||
@@ -518,27 +542,32 @@
|
||||
});
|
||||
|
||||
$("#btn_delete").click(function () {
|
||||
if (!confirm("<%= localeMessage.getString("common.confirmMsg")%>")) return;
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {
|
||||
cmd: "DELETE",
|
||||
id: $('input[name=id]').val()
|
||||
},
|
||||
success: function () {
|
||||
alert("<%= localeMessage.getString("common.deleteMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
showReasonPrompt("해당 가입자를 삭제 처리합니다. 사유를 입력해 주세요.", {
|
||||
title: "삭제 사유",
|
||||
onConfirm: function(reason) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: {
|
||||
cmd: "DELETE",
|
||||
id: $('input[name=id]').val(),
|
||||
serviceType: "APIGW",
|
||||
auditReason: reason
|
||||
},
|
||||
success: function () {
|
||||
alert("<%= localeMessage.getString("common.deleteMsg") %>");
|
||||
goNav(returnUrl);
|
||||
},
|
||||
error: function (e) {
|
||||
alert(e.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#btn_password_reset").click(function () {
|
||||
if (!confirm("패스워드를 초기화 하시겠습니까?\n초기 패스워드는 '!' + 이메일ID 입니다.")) return;
|
||||
if (!confirm("패스워드를 초기화 하시겠습니까?\n초기 패스워드는 '이메일ID${passwordInitSubfix}' 입니다.")) return;
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
|
||||
@@ -172,6 +172,9 @@ $(document).ready(function() {
|
||||
<form id="ajaxForm">
|
||||
<input type="hidden" name="id">
|
||||
<table class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col width="200px">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("messageTemplate.messageCode") %> <font color="red"> *</font></th>
|
||||
<td><input type="text" name="messageCode" data-required data-warning="메세지 코드는 필수입니다"/></td>
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<%@ 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"/>
|
||||
<style>
|
||||
.url-cell { display:inline-block; max-width:380px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; vertical-align:middle; }
|
||||
.succ-y { color:#1a73e8; font-weight:bold; }
|
||||
.succ-n { color:#d93025; font-weight:bold; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/webhook/webhookSendLogMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/webhook/webhookSendLogMan.view" />';
|
||||
var combo;
|
||||
|
||||
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
|
||||
function escapeAttr(s) { return escapeHtmlJs(s).replace(/"/g, '"'); }
|
||||
|
||||
function formatTargetUrl(cellvalue, options, rowObject) {
|
||||
var v = cellvalue || '';
|
||||
return '<span class="url-cell" title="' + escapeAttr(v) + '">' + escapeHtmlJs(v) + '</span>';
|
||||
}
|
||||
|
||||
// 이벤트유형 코드 → 모니터링 공통코드(EVENT_TYPE) 코드명
|
||||
function formatEventType(cellvalue, options, rowObject) {
|
||||
if (!combo || !combo.eventTypeList) {
|
||||
return escapeHtmlJs(cellvalue);
|
||||
}
|
||||
for (var i = 0; i < combo.eventTypeList.length; i++) {
|
||||
if (combo.eventTypeList[i].CODE == cellvalue) {
|
||||
return escapeHtmlJs(combo.eventTypeList[i].NAME);
|
||||
}
|
||||
}
|
||||
return escapeHtmlJs(cellvalue);
|
||||
}
|
||||
|
||||
function formatSuccess(cellvalue, options, rowObject) {
|
||||
if (cellvalue === true) return '<span class="succ-y">성공</span>';
|
||||
if (cellvalue === false) return '<span class="succ-n">실패</span>';
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
function init() {
|
||||
|
||||
$("#startDatepicker").datepicker().inputmask("yyyy-mm-dd",{'autoUnmask':true});
|
||||
$("#endDatepicker").datepicker().inputmask("yyyy-mm-dd",{'autoUnmask':true});
|
||||
|
||||
var today = getToday();
|
||||
|
||||
$("input[name=searchStartYYYYMMDD]").val(today.substring(0,6)+"01");
|
||||
$("input[name=searchEndYYYYMMDD]").val(today);
|
||||
$("input[name=searchStartDate]").val(today.substring(0,6)+"01");
|
||||
$("input[name=searchEndDate]").val(today);
|
||||
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'LIST_INIT_COMBO'},
|
||||
success: function (json) {
|
||||
combo = json;
|
||||
new makeOptions("CODE", "NAME").setObj($("select[name=searchEventType]"))
|
||||
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
|
||||
.setData(json.eventTypeList).rendering();
|
||||
|
||||
// 뒤로가기 등으로 이전 검색조건 파라미터가 있으면 기본값을 덮어씀
|
||||
putSelectFromParam();
|
||||
|
||||
// 콤보(이벤트유형) 로드 후 그리드 생성 — formatEventType 경합 방지
|
||||
buildGrid();
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
function buildGrid() {
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||
colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Proxy URL', 'Status', '성공여부', '발송서버', '발송일시'],
|
||||
colModel: [
|
||||
{name: 'id', align: 'center', key: true, hidden: true},
|
||||
{name: 'rowNum', align: 'center', width: 45, sortable: false},
|
||||
{name: 'orgName', align: 'left', width: 140},
|
||||
{name: 'eventType', align: 'center', width: 120, formatter: formatEventType},
|
||||
{name: 'targetUrl', align: 'left', width: 300, formatter: formatTargetUrl},
|
||||
{name: 'proxyUrl', align: 'left', width: 300, formatter: formatTargetUrl},
|
||||
{name: 'statusCode', align: 'center', width: 60},
|
||||
{name: 'success', align: 'center', width: 70, formatter: formatSuccess},
|
||||
{name: 'sendBy', align: 'center', width: 100},
|
||||
{name: 'sentAt', align: 'center', width: 140}
|
||||
],
|
||||
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 id = rowData['id'];
|
||||
var url2 = url_view + '?cmd=DETAIL';
|
||||
url2 += '&page=' + $(this).getGridParam("page");
|
||||
url2 += '&returnUrl=' + getReturnUrl();
|
||||
url2 += '&menuId=' + '${param.menuId}';
|
||||
url2 += '&id=' + id;
|
||||
url2 += '&' + getSearchUrl();
|
||||
goNav(url2);
|
||||
},
|
||||
loadComplete: function () {
|
||||
var page = $(this).getGridParam('page');
|
||||
var rowNum = $(this).getGridParam('rowNum');
|
||||
var rows = $(this).getDataIDs();
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var number = ((page - 1) * rowNum + i) + 1;
|
||||
$(this).setCell(rows[i], 'rowNum', number);
|
||||
}
|
||||
// 서버 고정 정렬(등록일시 역순) — 컬럼 정렬 비활성
|
||||
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');
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function () {
|
||||
var start = $("input[name=searchStartYYYYMMDD]").val().replace(/-/gi, "");
|
||||
var end = $("input[name=searchEndYYYYMMDD]").val().replace(/-/gi, "");
|
||||
|
||||
if (start && start > getToday()) {
|
||||
alert("시작일이 오늘 이후입니다. 시작일을 확인해주세요.");
|
||||
$("input[name=searchStartYYYYMMDD]").focus();
|
||||
return false;
|
||||
}
|
||||
if (start && end && start > end) {
|
||||
alert("조회기간 종료일은 시작일보다 커야합니다.");
|
||||
$("input[name=searchEndYYYYMMDD]").focus();
|
||||
return false;
|
||||
}
|
||||
$("input[name=searchStartDate]").val(start);
|
||||
$("input[name=searchEndDate]").val(end);
|
||||
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("select[name^=search]").change(function () { $("#btn_search").click(); });
|
||||
$("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">외부로 발송된 웹훅 이력을 조회합니다.</span></div>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<colgroup>
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>기관명</th>
|
||||
<td><input type="text" name="searchOrgName" autocomplete="off"></td>
|
||||
<th>이벤트유형</th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="searchEventType"></select>
|
||||
</div>
|
||||
</td>
|
||||
<th>성공여부</th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="searchSuccess">
|
||||
<option value="">전체</option>
|
||||
<option value="Y">성공</option>
|
||||
<option value="N">실패</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Target URL</th>
|
||||
<td><input type="text" name="searchTargetUrl" autocomplete="off" style="width:95%;"></td>
|
||||
<th>발송기간</th>
|
||||
<td>
|
||||
<input type="text" name="searchStartYYYYMMDD" id="startDatepicker" readonly="readonly" value="" size="10" style="width:40%; border:1px solid #ebebec;">
|
||||
~
|
||||
<input type="text" name="searchEndYYYYMMDD" value="" id="endDatepicker" size="10" readonly="readonly" style="width:40%; border:1px solid #ebebec;">
|
||||
<input type="hidden" name="searchStartDate" value="">
|
||||
<input type="hidden" name="searchEndDate" value="">
|
||||
</td>
|
||||
<th></th>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,137 @@
|
||||
<%@ 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"/>
|
||||
<style>
|
||||
.succ-y { color:#1a73e8; font-weight:bold; }
|
||||
.succ-n { color:#d93025; font-weight:bold; }
|
||||
.pre-wrap { white-space:pre-wrap; word-break:break-all; min-height:60px; line-height:1.5; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/webhook/webhookSendLogMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/webhook/webhookSendLogMan.view" />';
|
||||
var key = '${param.id}';
|
||||
|
||||
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
|
||||
|
||||
function detail() {
|
||||
if (!key) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'DETAIL', id: key},
|
||||
success: function (data) {
|
||||
$('#id').text(data.id != null ? data.id : '');
|
||||
$('#orgName').text(data.orgName || '');
|
||||
$('#eventType').text(data.eventTypeName || data.eventType || '')
|
||||
.attr('title', data.eventType || '');
|
||||
$('#targetUrl').text(data.targetUrl || '');
|
||||
$('#proxyUrl').text(data.proxyUrl || '');
|
||||
$('#statusCode').text(data.statusCode != null ? data.statusCode : '');
|
||||
$('#retryCount').text(data.retryCount != null ? data.retryCount : '');
|
||||
$('#sendBy').text(data.sendBy || '');
|
||||
$('#sentAt').text(data.sentAt || '');
|
||||
$('#signature').text(data.signature || '');
|
||||
$('#errorMessage').text(data.errorMessage || '');
|
||||
$('#payload').text(data.payload || '');
|
||||
$('#responseBody').text(data.responseBody || '');
|
||||
|
||||
if (data.success === true) {
|
||||
$('#success').html('<span class="succ-y">성공</span>');
|
||||
} else if (data.success === false) {
|
||||
$('#success').html('<span class="succ-n">실패</span>');
|
||||
} else {
|
||||
$('#success').text('');
|
||||
}
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
|
||||
buttonControl();
|
||||
detail();
|
||||
|
||||
$('#btn_previous').click(function () { goNav(returnUrl); });
|
||||
});
|
||||
</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_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||
</div>
|
||||
<div class="title" id="title">웹훅 발송 로그 상세</div>
|
||||
<table class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<td><span id="id"></span></td>
|
||||
<th>기관명</th>
|
||||
<td><span id="orgName"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>이벤트유형</th>
|
||||
<td><span id="eventType"></span></td>
|
||||
<th>Status Code</th>
|
||||
<td><span id="statusCode"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Target URL</th>
|
||||
<td><span id="targetUrl"></span></td>
|
||||
<th>Proxy URL</th>
|
||||
<td><span id="proxyUrl"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>성공여부</th>
|
||||
<td><span id="success"></span></td>
|
||||
<th>재시도횟수</th>
|
||||
<td><span id="retryCount"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>발송서버</th>
|
||||
<td><span id="sendBy"></span></td>
|
||||
<th>발송일시</th>
|
||||
<td><span id="sentAt"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Signature</th>
|
||||
<td colspan="3"><span id="signature"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Payload</th>
|
||||
<td colspan="3"><div id="payload" class="pre-wrap"></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Response Body</th>
|
||||
<td colspan="3"><div id="responseBody" class="pre-wrap"></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>에러 메세지</th>
|
||||
<td colspan="3"><div id="errorMessage" class="pre-wrap"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</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 = {
|
||||
@@ -68,22 +66,27 @@
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
formatter: function (params) {
|
||||
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 10,
|
||||
data: ['성공', 'Timeout', '시스템오류', '업무오류']
|
||||
orient: 'vertical',
|
||||
right: 30,
|
||||
top: 'middle',
|
||||
data: ['성공', 'Timeout', '시스템오류']
|
||||
},
|
||||
series: [{
|
||||
name: '총건수',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
radius: ['38%', '65%'],
|
||||
center: ['44%', '55%'],
|
||||
avoidLabelOverlap: true,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}'
|
||||
formatter: function (params) {
|
||||
return params.name + ': ' + params.value.toLocaleString();
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
@@ -98,88 +101,18 @@
|
||||
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: [{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: '0',
|
||||
textAlign: 'center',
|
||||
fill: '#333',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
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 = {
|
||||
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
|
||||
title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross', label: { backgroundColor: '#6a7985' } }
|
||||
@@ -191,32 +124,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,33 +182,12 @@
|
||||
data: [
|
||||
{ value: totalSuccess, name: '성공' },
|
||||
{ value: totalTimeout, name: 'Timeout' },
|
||||
{ value: totalSystemErr, name: '시스템오류' },
|
||||
{ value: totalBizErr, name: '업무오류' }
|
||||
{ value: totalSystemErr, name: '시스템오류' }
|
||||
]
|
||||
}],
|
||||
graphic: [{
|
||||
style: {
|
||||
text: grandTotal.toLocaleString()
|
||||
}
|
||||
}]
|
||||
});
|
||||
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
|
||||
|
||||
// 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 +195,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 +417,7 @@
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'API명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
],
|
||||
colModel: [
|
||||
@@ -545,10 +426,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 }
|
||||
@@ -567,6 +444,9 @@
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||
}
|
||||
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -580,9 +460,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 +475,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'),
|
||||
@@ -620,6 +493,9 @@
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||
}
|
||||
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -685,9 +561,7 @@
|
||||
// 윈도우 리사이즈 시 차트 리사이즈
|
||||
$(window).resize(function() {
|
||||
if (totalDonutChart) totalDonutChart.resize();
|
||||
if (seq900DonutChart) seq900DonutChart.resize();
|
||||
if (callChart) callChart.resize();
|
||||
if (respChart) respChart.resize();
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
@@ -708,11 +582,19 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="title" id="title">API 일별 통계</div>
|
||||
<table class="search_condition" cellspacing="0">
|
||||
<table class="search_condition" cellspacing="0" style="table-layout:fixed;">
|
||||
<colgroup>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:100px;">조회기간</th>
|
||||
<td colspan="3">
|
||||
<th>조회기간</th>
|
||||
<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;">
|
||||
@@ -720,31 +602,29 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:100px;">API명</th>
|
||||
<th>API명</th>
|
||||
<td>
|
||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||
</td>
|
||||
<th style="width:100px;">인스턴스</th>
|
||||
<th>업무구분</th>
|
||||
<td>
|
||||
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
||||
</td>
|
||||
<th>인스턴스</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>
|
||||
<th>클라이언트ID</th>
|
||||
<td>
|
||||
<input type="text" name="searchClientId" value="${param.searchClientId}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:100px;">Inbound Adapter</th>
|
||||
<th>Inbound Adapter</th>
|
||||
<td>
|
||||
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
|
||||
</td>
|
||||
<th style="width:100px;">Outbound Adapter</th>
|
||||
<th>Outbound Adapter</th>
|
||||
<td>
|
||||
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
||||
</td>
|
||||
@@ -753,13 +633,11 @@
|
||||
</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 class="chart" style="position:relative; padding:0;">
|
||||
<div id="totalDonutChart" style="width:100%; height:100%;"></div>
|
||||
<div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
|
||||
</div>
|
||||
<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 = {
|
||||
@@ -69,22 +67,27 @@
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
formatter: function (params) {
|
||||
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 10,
|
||||
data: ['성공', 'Timeout', '시스템오류', '업무오류']
|
||||
orient: 'vertical',
|
||||
right: 30,
|
||||
top: 'middle',
|
||||
data: ['성공', 'Timeout', '시스템오류']
|
||||
},
|
||||
series: [{
|
||||
name: '총건수',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
radius: ['38%', '65%'],
|
||||
center: ['44%', '55%'],
|
||||
avoidLabelOverlap: true,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}'
|
||||
formatter: function (params) {
|
||||
return params.name + ': ' + params.value.toLocaleString();
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
@@ -99,133 +102,40 @@
|
||||
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: [{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: '0',
|
||||
textAlign: 'center',
|
||||
fill: '#333',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
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 = {
|
||||
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
|
||||
title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
|
||||
tooltip: {
|
||||
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,33 +218,12 @@
|
||||
data: [
|
||||
{ value: totalSuccess, name: '성공' },
|
||||
{ value: totalTimeout, name: 'Timeout' },
|
||||
{ value: totalSystemErr, name: '시스템오류' },
|
||||
{ value: totalBizErr, name: '업무오류' }
|
||||
{ value: totalSystemErr, name: '시스템오류' }
|
||||
]
|
||||
}],
|
||||
graphic: [{
|
||||
style: {
|
||||
text: grandTotal.toLocaleString()
|
||||
}
|
||||
}]
|
||||
});
|
||||
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
|
||||
|
||||
// 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 +231,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 +419,7 @@
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'API명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
],
|
||||
colModel: [
|
||||
@@ -551,10 +428,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 }
|
||||
@@ -573,6 +446,9 @@
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||
}
|
||||
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -586,9 +462,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 +477,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'),
|
||||
@@ -626,6 +495,9 @@
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||
}
|
||||
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -669,9 +541,7 @@
|
||||
// 윈도우 리사이즈 시 차트 리사이즈
|
||||
$(window).resize(function() {
|
||||
if (totalDonutChart) totalDonutChart.resize();
|
||||
if (seq900DonutChart) seq900DonutChart.resize();
|
||||
if (callChart) callChart.resize();
|
||||
if (respChart) respChart.resize();
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
@@ -692,41 +562,47 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="title" id="title">API 시간별 통계</div>
|
||||
<table class="search_condition" cellspacing="0">
|
||||
<table class="search_condition" cellspacing="0" style="table-layout:fixed;">
|
||||
<colgroup>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:100px;">조회일자</th>
|
||||
<td colspan="3">
|
||||
<th>조회일자</th>
|
||||
<td colspan="5">
|
||||
<input type="text" name="searchDate" value="${param.searchDate}" style="width:100px;">
|
||||
<span style="color:#888; font-size:12px; margin-left:10px;">(선택일 00시 ~ 23시 조회)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:100px;">API명</th>
|
||||
<th>API명</th>
|
||||
<td>
|
||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||
</td>
|
||||
<th style="width:100px;">인스턴스</th>
|
||||
<th>업무구분</th>
|
||||
<td>
|
||||
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
||||
</td>
|
||||
<th>인스턴스</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>
|
||||
<th>클라이언트ID</th>
|
||||
<td>
|
||||
<input type="text" name="searchClientId" value="${param.searchClientId}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:100px;">Inbound Adapter</th>
|
||||
<th>Inbound Adapter</th>
|
||||
<td>
|
||||
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
|
||||
</td>
|
||||
<th style="width:100px;">Outbound Adapter</th>
|
||||
<th>Outbound Adapter</th>
|
||||
<td>
|
||||
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
||||
</td>
|
||||
@@ -735,13 +611,11 @@
|
||||
</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 class="chart" style="position:relative; padding:0;">
|
||||
<div id="totalDonutChart" style="width:100%; height:100%;"></div>
|
||||
<div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
|
||||
</div>
|
||||
<div id="callChart" class="chart"></div>
|
||||
<div id="respChart" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<!-- 요약 그리드 -->
|
||||
@@ -752,7 +626,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 = {
|
||||
@@ -61,22 +59,27 @@
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
formatter: function (params) {
|
||||
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 10,
|
||||
data: ['성공', 'Timeout', '시스템오류', '업무오류']
|
||||
orient: 'vertical',
|
||||
right: 30,
|
||||
top: 'middle',
|
||||
data: ['성공', 'Timeout', '시스템오류']
|
||||
},
|
||||
series: [{
|
||||
name: '총건수',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
radius: ['38%', '65%'],
|
||||
center: ['44%', '55%'],
|
||||
avoidLabelOverlap: true,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}'
|
||||
formatter: function (params) {
|
||||
return params.name + ': ' + params.value.toLocaleString();
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
@@ -91,93 +94,23 @@
|
||||
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: [{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: '0',
|
||||
textAlign: 'center',
|
||||
fill: '#333',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
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 = {
|
||||
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
|
||||
var callOption = {
|
||||
title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
|
||||
tooltip: {
|
||||
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 +121,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,33 +180,12 @@
|
||||
data: [
|
||||
{ value: totalSuccess, name: '성공' },
|
||||
{ value: totalTimeout, name: 'Timeout' },
|
||||
{ value: totalSystemErr, name: '시스템오류' },
|
||||
{ value: totalBizErr, name: '업무오류' }
|
||||
{ value: totalSystemErr, name: '시스템오류' }
|
||||
]
|
||||
}],
|
||||
graphic: [{
|
||||
style: {
|
||||
text: grandTotal.toLocaleString()
|
||||
}
|
||||
}]
|
||||
});
|
||||
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
|
||||
|
||||
// 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 +193,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 +432,7 @@
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'API명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
],
|
||||
colModel: [
|
||||
@@ -562,10 +441,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,6 +459,9 @@
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||
}
|
||||
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -597,9 +475,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 +490,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'),
|
||||
@@ -637,6 +508,9 @@
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||
}
|
||||
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -646,35 +520,26 @@
|
||||
$("input[name=searchStartDate], input[name=searchEndDate]").inputmask("9999-99-99", { 'autoUnmask': true });
|
||||
$("input[name=searchStartTime], input[name=searchEndTime]").inputmask("99:99", { 'autoUnmask': true });
|
||||
|
||||
// 기본값 설정 (현재 시간 기준 1시간 전 정각 ~ 현재 정각)
|
||||
// 기본값 설정 (현재 시간(분) 기준 1시간 전 ~ 현재 시간(분))
|
||||
var now = new Date();
|
||||
var endHour = now.getHours();
|
||||
var startHour = endHour - 1;
|
||||
var startDate, endDate;
|
||||
var oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
|
||||
if (startHour < 0) {
|
||||
startHour = 23;
|
||||
var yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
startDate = yesterday.getFullYear() + '-' +
|
||||
String(yesterday.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(yesterday.getDate()).padStart(2, '0');
|
||||
endDate = now.getFullYear() + '-' +
|
||||
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(now.getDate()).padStart(2, '0');
|
||||
} else {
|
||||
var today = getToday();
|
||||
startDate = today;
|
||||
endDate = today;
|
||||
function formatDate(d) {
|
||||
return d.getFullYear() + '-' +
|
||||
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(d.getDate()).padStart(2, '0');
|
||||
}
|
||||
function formatTime(d) {
|
||||
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
||||
}
|
||||
|
||||
if (!$("input[name=searchStartDate]").val()) {
|
||||
$("input[name=searchStartDate]").val(startDate);
|
||||
$("input[name=searchStartTime]").val(String(startHour).padStart(2, '0') + ':00');
|
||||
$("input[name=searchStartDate]").val(formatDate(oneHourAgo));
|
||||
$("input[name=searchStartTime]").val(formatTime(oneHourAgo));
|
||||
}
|
||||
if (!$("input[name=searchEndDate]").val()) {
|
||||
$("input[name=searchEndDate]").val(endDate);
|
||||
$("input[name=searchEndTime]").val(String(endHour).padStart(2, '0') + ':00');
|
||||
$("input[name=searchEndDate]").val(formatDate(now));
|
||||
$("input[name=searchEndTime]").val(formatTime(now));
|
||||
}
|
||||
|
||||
initCharts();
|
||||
@@ -711,9 +576,7 @@
|
||||
// 윈도우 리사이즈 시 차트 리사이즈
|
||||
$(window).resize(function() {
|
||||
if (totalDonutChart) totalDonutChart.resize();
|
||||
if (seq900DonutChart) seq900DonutChart.resize();
|
||||
if (callChart) callChart.resize();
|
||||
if (respChart) respChart.resize();
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
@@ -733,7 +596,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,57 +610,26 @@
|
||||
<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 class="chart" style="position:relative; padding:0;">
|
||||
<div id="totalDonutChart" style="width:100%; height:100%;"></div>
|
||||
<div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -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 = {
|
||||
@@ -68,22 +66,27 @@
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
formatter: function (params) {
|
||||
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 10,
|
||||
data: ['성공', 'Timeout', '시스템오류', '업무오류']
|
||||
orient: 'vertical',
|
||||
right: 30,
|
||||
top: 'middle',
|
||||
data: ['성공', 'Timeout', '시스템오류']
|
||||
},
|
||||
series: [{
|
||||
name: '총건수',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
radius: ['38%', '65%'],
|
||||
center: ['44%', '55%'],
|
||||
avoidLabelOverlap: true,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}'
|
||||
formatter: function (params) {
|
||||
return params.name + ': ' + params.value.toLocaleString();
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
@@ -98,125 +101,34 @@
|
||||
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: [{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: '0',
|
||||
textAlign: 'center',
|
||||
fill: '#333',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
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 = {
|
||||
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
|
||||
title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
|
||||
tooltip: {
|
||||
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,33 +180,13 @@
|
||||
data: [
|
||||
{ value: totalSuccess, name: '성공' },
|
||||
{ value: totalTimeout, name: 'Timeout' },
|
||||
{ value: totalSystemErr, name: '시스템오류' },
|
||||
{ value: totalBizErr, name: '업무오류' }
|
||||
{ value: totalSystemErr, name: '시스템오류' }
|
||||
]
|
||||
}],
|
||||
graphic: [{
|
||||
style: {
|
||||
text: grandTotal.toLocaleString()
|
||||
}
|
||||
}]
|
||||
});
|
||||
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
|
||||
|
||||
// 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 +199,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 +422,7 @@
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'API명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
],
|
||||
colModel: [
|
||||
@@ -549,10 +431,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 }
|
||||
@@ -571,6 +449,9 @@
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||
}
|
||||
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -584,9 +465,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 +480,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'),
|
||||
@@ -624,6 +498,9 @@
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||
}
|
||||
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -675,9 +552,7 @@
|
||||
// 윈도우 리사이즈 시 차트 리사이즈
|
||||
$(window).resize(function() {
|
||||
if (totalDonutChart) totalDonutChart.resize();
|
||||
if (seq900DonutChart) seq900DonutChart.resize();
|
||||
if (callChart) callChart.resize();
|
||||
if (respChart) respChart.resize();
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
@@ -698,11 +573,19 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="title" id="title">API 월별 통계</div>
|
||||
<table class="search_condition" cellspacing="0">
|
||||
<table class="search_condition" cellspacing="0" style="table-layout:fixed;">
|
||||
<colgroup>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:100px;">조회기간</th>
|
||||
<td colspan="3">
|
||||
<th>조회기간</th>
|
||||
<td colspan="5">
|
||||
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;" placeholder="YYYY-MM">
|
||||
~
|
||||
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;" placeholder="YYYY-MM">
|
||||
@@ -710,31 +593,29 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:100px;">API명</th>
|
||||
<th>API명</th>
|
||||
<td>
|
||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||
</td>
|
||||
<th style="width:100px;">인스턴스</th>
|
||||
<th>업무구분</th>
|
||||
<td>
|
||||
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
||||
</td>
|
||||
<th>인스턴스</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>
|
||||
<th>클라이언트ID</th>
|
||||
<td>
|
||||
<input type="text" name="searchClientId" value="${param.searchClientId}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:100px;">Inbound Adapter</th>
|
||||
<th>Inbound Adapter</th>
|
||||
<td>
|
||||
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
|
||||
</td>
|
||||
<th style="width:100px;">Outbound Adapter</th>
|
||||
<th>Outbound Adapter</th>
|
||||
<td>
|
||||
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
||||
</td>
|
||||
@@ -743,15 +624,14 @@
|
||||
</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 class="chart" style="position:relative; padding:0;">
|
||||
<div id="totalDonutChart" style="width:100%; height:100%;"></div>
|
||||
<div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
|
||||
</div>
|
||||
<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 = {
|
||||
@@ -68,22 +66,27 @@
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
formatter: function (params) {
|
||||
return params.name + ': ' + params.value.toLocaleString() + ' (' + params.percent + '%)';
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 10,
|
||||
data: ['성공', 'Timeout', '시스템오류', '업무오류']
|
||||
orient: 'vertical',
|
||||
right: 30,
|
||||
top: 'middle',
|
||||
data: ['성공', 'Timeout', '시스템오류']
|
||||
},
|
||||
series: [{
|
||||
name: '총건수',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
radius: ['38%', '65%'],
|
||||
center: ['44%', '55%'],
|
||||
avoidLabelOverlap: true,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c}'
|
||||
formatter: function (params) {
|
||||
return params.name + ': ' + params.value.toLocaleString();
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
@@ -98,125 +101,36 @@
|
||||
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: [{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
style: {
|
||||
text: '0',
|
||||
textAlign: 'center',
|
||||
fill: '#333',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
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 = {
|
||||
title: { text: '호출량 추이', left: 'center', textStyle: { fontSize: 14 } },
|
||||
title: { text: '호출량 추이', left: 'center', top: 10, textStyle: { fontSize: 14 } },
|
||||
tooltip: {
|
||||
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,33 +182,13 @@
|
||||
data: [
|
||||
{ value: totalSuccess, name: '성공' },
|
||||
{ value: totalTimeout, name: 'Timeout' },
|
||||
{ value: totalSystemErr, name: '시스템오류' },
|
||||
{ value: totalBizErr, name: '업무오류' }
|
||||
{ value: totalSystemErr, name: '시스템오류' }
|
||||
]
|
||||
}],
|
||||
graphic: [{
|
||||
style: {
|
||||
text: grandTotal.toLocaleString()
|
||||
}
|
||||
}]
|
||||
});
|
||||
$("#totalDonutCenterText").text(grandTotal.toLocaleString());
|
||||
|
||||
// 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 +201,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 +398,7 @@
|
||||
postData: gridPostData,
|
||||
colNames: [
|
||||
'API명',
|
||||
'총건수', '성공', 'Timeout', '시스템오류', '업무오류',
|
||||
'Seq900 Timeout', 'Seq900 시스템오류', 'Seq900 업무오류',
|
||||
'총건수', '성공', 'Timeout', '시스템오류',
|
||||
'평균응답(ms)', '최소응답(ms)', '최대응답(ms)'
|
||||
],
|
||||
colModel: [
|
||||
@@ -523,10 +407,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 }
|
||||
@@ -545,6 +425,9 @@
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||
}
|
||||
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -558,9 +441,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 +456,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'),
|
||||
@@ -598,6 +474,9 @@
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, { sortable: false });
|
||||
}
|
||||
// 조회(reloadGrid) 시 컬럼 폭이 고정폭(shrinkToFit:false)으로 남아
|
||||
// 실제 컨테이너 폭과 어긋나 가로 스크롤이 생기는 것을 방지
|
||||
$(this).jqGrid('setGridWidth', $('#content_middle').width(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -644,9 +523,7 @@
|
||||
// 윈도우 리사이즈 시 차트 리사이즈
|
||||
$(window).resize(function() {
|
||||
if (totalDonutChart) totalDonutChart.resize();
|
||||
if (seq900DonutChart) seq900DonutChart.resize();
|
||||
if (callChart) callChart.resize();
|
||||
if (respChart) respChart.resize();
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
@@ -667,42 +544,48 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="title" id="title">API 연간 통계</div>
|
||||
<table class="search_condition" cellspacing="0">
|
||||
<table class="search_condition" cellspacing="0" style="table-layout:fixed;">
|
||||
<colgroup>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
<col style="width:150px;">
|
||||
<col>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width:100px;">조회기간</th>
|
||||
<td colspan="3">
|
||||
<th>조회기간</th>
|
||||
<td colspan="5">
|
||||
<input type="text" name="searchStartDateTime" value="${param.searchStartDateTime}" style="width:100px;" placeholder="YYYY">
|
||||
~
|
||||
<input type="text" name="searchEndDateTime" value="${param.searchEndDateTime}" style="width:100px;" placeholder="YYYY">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:100px;">API명</th>
|
||||
<th>API명</th>
|
||||
<td>
|
||||
<input type="text" name="searchApiName" value="${param.searchApiName}">
|
||||
</td>
|
||||
<th style="width:100px;">인스턴스</th>
|
||||
<th>업무구분</th>
|
||||
<td>
|
||||
<input type="text" name="searchBizDivCode" value="${param.searchBizDivCode}">
|
||||
</td>
|
||||
<th>인스턴스</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>
|
||||
<th>클라이언트ID</th>
|
||||
<td>
|
||||
<input type="text" name="searchClientId" value="${param.searchClientId}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:100px;">Inbound Adapter</th>
|
||||
<th>Inbound Adapter</th>
|
||||
<td>
|
||||
<input type="text" name="searchInboundAdapter" value="${param.searchInboundAdapter}">
|
||||
</td>
|
||||
<th style="width:100px;">Outbound Adapter</th>
|
||||
<th>Outbound Adapter</th>
|
||||
<td>
|
||||
<input type="text" name="searchOutboundAdapter" value="${param.searchOutboundAdapter}">
|
||||
</td>
|
||||
@@ -711,13 +594,11 @@
|
||||
</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 class="chart" style="position:relative; padding:0;">
|
||||
<div id="totalDonutChart" style="width:100%; height:100%;"></div>
|
||||
<div id="totalDonutCenterText" style="position:absolute; left:44.5%; top:55%; transform:translate(-50%,-50%); font-size:24px; font-weight:bold; color:#333; pointer-events:none; z-index:10;">0</div>
|
||||
</div>
|
||||
<div id="callChart" class="chart"></div>
|
||||
<div id="respChart" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<!-- 요약 그리드 -->
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
<%@ 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.substring(0,6)+"01";
|
||||
var endDate = today;
|
||||
|
||||
|
||||
if (!$("input[name=searchStartDate]").val()) {
|
||||
$("input[name=searchStartDate]").val(startDate);
|
||||
}
|
||||
if (!$("input[name=searchEndDate]").val()) {
|
||||
$("input[name=searchEndDate]").val(endDate);
|
||||
}
|
||||
|
||||
list();
|
||||
search();
|
||||
|
||||
resizeJqGridWidth('grid', 'content_middle', '1200');
|
||||
|
||||
$("#btn_search").click(function() {
|
||||
search();
|
||||
});
|
||||
|
||||
$("input[name=searchType]").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>
|
||||
@@ -76,6 +76,15 @@
|
||||
return cellvalue.substring(1, 4);
|
||||
}
|
||||
|
||||
function apiStatusFormatter(cellvalue, options, rowObject) {
|
||||
switch (cellvalue) {
|
||||
case 'E': return '장애';
|
||||
case 'D': return '지연';
|
||||
case 'C': return '점검';
|
||||
default: return '정상';
|
||||
}
|
||||
}
|
||||
|
||||
function list() {
|
||||
detail()
|
||||
var gridPostData = getSearchForJqgrid("cmd", "LIST"); //jqgrid에서는 object 로
|
||||
@@ -90,6 +99,7 @@
|
||||
'API FULL PATH',
|
||||
'요청',
|
||||
'응답',
|
||||
'상태',
|
||||
'작성자',
|
||||
'가상응답여부',
|
||||
'변경일시(*)',
|
||||
@@ -99,11 +109,12 @@
|
||||
{name: 'eaiSvcName', align: 'left', width: '100', sortable: true},
|
||||
{name: 'eaiSvcDesc', align: 'left', sortable: false},
|
||||
{name: 'apiFullPath', align: 'left', sortable: false},
|
||||
{name: 'fromAdapter', align: 'center', width: '40', formatter: adapterNameShortFormatter, sortable: false},
|
||||
{name: 'toAdapter', align: 'center', width: '40', formatter: adapterNameShortFormatter, sortable: false},
|
||||
{name: 'author', align: 'center', width: '60', sortable: false},
|
||||
{name: 'fromAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
|
||||
{name: 'toAdapter', align: 'center', width: '30', formatter: adapterNameShortFormatter, sortable: false},
|
||||
{name: 'statusCode', align: 'center', width: '30', formatter: apiStatusFormatter, sortable: false},
|
||||
{name: 'author', align: 'center', width: '40', sortable: false},
|
||||
{name : 'simYn', align : 'center' , width:'40', hidden: true },
|
||||
{name : 'lastModifiedDate', align : 'center' , width:'40', sortable: true},
|
||||
{name : 'lastModifiedDate', align : 'center' , width:'60', sortable: true},
|
||||
{name: 'syncAsyncType', align: 'center', width: '40', hidden: true},
|
||||
],
|
||||
jsonReader: {
|
||||
|
||||
@@ -77,13 +77,31 @@
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
/* iframe 모달(showModal) 콘텐츠 패딩 제거 — iframe 이 다이얼로그 폭을 최대한 사용해 내부 가로스크롤 방지 */
|
||||
.ui-dialog .ui-dialog-content { padding: 0 !important; }
|
||||
|
||||
/* OpenAPI Spec 버튼 상태 색상 (PTL_API_SPEC_INFO 기준) */
|
||||
#btn_openapi_spec.spec-exists { background:#ffe08a !important; border-color:#f0c040 !important; color:#5a4600 !important; }
|
||||
#btn_openapi_spec.spec-public { background:#b8e6a0 !important; border-color:#7cc257 !important; color:#2c4a15 !important; }
|
||||
/* 호버 상태 안내 레이어 */
|
||||
#specStatusLayer {
|
||||
position: fixed; z-index: 100000; display: none;
|
||||
background: rgba(30,30,30,0.92); color: #fff;
|
||||
padding: 8px 12px; border-radius: 6px;
|
||||
font-size: 12px; line-height: 1.6; max-width: 340px;
|
||||
white-space: pre-line; pointer-events: none;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
</style>
|
||||
<script language="javascript" >
|
||||
var isDetail = false;
|
||||
var url ='<c:url value="/onl/transaction/apim/apiInterfaceMan.json" />';
|
||||
const url_excel ='<c:url value="/onl/transaction/apim/apiInterfaceMan.excel" />';
|
||||
var url_spec = '<c:url value="/onl/transaction/apim/apiSpecMan.view"/>';
|
||||
var url_openapi_spec = '<c:url value="/onl/transaction/apim/djbApiSpecMan.view"/>';
|
||||
var url_openapi_spec_json = '<c:url value="/onl/transaction/apim/djbApiSpecMan.json"/>';
|
||||
var returnUrl ;
|
||||
let headerValues = [];
|
||||
let headerNames = [];
|
||||
@@ -298,7 +316,7 @@
|
||||
if(!isStandardMessage){
|
||||
$(adapterSettings.adapterUrlId).text(adapterInfo.urlPath);
|
||||
} else {
|
||||
$(adapterSettings.methodSelectId).val('');
|
||||
//$(adapterSettings.methodSelectId).val('');
|
||||
$(adapterSettings.restPathInputId).val('');
|
||||
$(adapterSettings.adapterUrlId).text('');
|
||||
}
|
||||
@@ -411,7 +429,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,
|
||||
@@ -486,6 +504,7 @@
|
||||
}
|
||||
|
||||
if(data.errResponseTransform != null && data.errResponseTransform != '' && data.errTransformYn == 'Y') {
|
||||
$('#toggleTransformYn').bootstrapToggle('on');
|
||||
$('#toggleErrTransformYn').bootstrapToggle('on');
|
||||
$('#rowErrResponseTransform').show();
|
||||
} else {
|
||||
@@ -535,6 +554,8 @@
|
||||
|
||||
$('#inboundHttpMethod').trigger('change');
|
||||
$('#outboundHttpMethod').trigger('change');
|
||||
|
||||
applyOpenApiSpecStatus(); // OpenAPI Spec 버튼 색상(PTL_API_SPEC_INFO)
|
||||
},
|
||||
error:function(e){
|
||||
alert(e.responseText);
|
||||
@@ -871,6 +892,10 @@
|
||||
});
|
||||
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
|
||||
console.log('modify', postData);
|
||||
console.log('method', $('#inboundHttpMethod').val());
|
||||
|
||||
if (isDetail){
|
||||
postData.push({ name: "cmd" , value:"UPDATE"});
|
||||
}else{
|
||||
@@ -962,11 +987,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -986,6 +1013,64 @@
|
||||
showModal(popupUrl, args, 1200, 800);
|
||||
}
|
||||
|
||||
// v2: OpenAPI Spec — 기본은 모달(iframe), Shift+클릭은 새 창.
|
||||
// 컨텍스트는 전부 query string 으로 전달(dialogArguments 미사용).
|
||||
function showOpenApiSpecPopup(e) {
|
||||
var eaiSvcName = getFullSvcName();
|
||||
var eaiSvcDesc = $('#eaiSvcDesc').val();
|
||||
var apipath = $('#apiFullPath').val(); // METHOD|URL ('|' 포함 → 인코딩 필수)
|
||||
var contentType = $('#contentType').val();
|
||||
var baseUrl = url_openapi_spec
|
||||
+ '?cmd=DETAIL'
|
||||
+ '&eaiSvcName=' + encodeURIComponent(eaiSvcName)
|
||||
+ '&apipath=' + encodeURIComponent(apipath)
|
||||
+ '&eaiSvcDesc=' + encodeURIComponent(eaiSvcDesc)
|
||||
+ '&contentType=' + encodeURIComponent(contentType);
|
||||
if (e && e.shiftKey) {
|
||||
// Shift+클릭: 새 창. window.open 은 serviceType/menuId 자동 부착 안 되므로 직접 붙임.
|
||||
window.open(urlAddServiceType(baseUrl + '&menuId=' + encodeURIComponent(getMenuId())), '_blank');
|
||||
} else {
|
||||
// 기본: 모달 + iframe. showModal 이 serviceType/menuId/pop 을 자동 부착.
|
||||
// 콘텐츠 고정폭 1600 + 다이얼로그 chrome(패딩/보더 ~40px) 때문에 iframe 내부폭이 1600 미만이면 가로스크롤 발생.
|
||||
// 뷰포트 내에서 최대한 크게 열어(캡 1720) iframe 내부폭이 1600 이상 확보되게 한다.
|
||||
var vw = $(window).width(), vh = $(window).height();
|
||||
var modalW = Math.min(vw - 12, 1720);
|
||||
var modalH = Math.min(vh - 20, 900);
|
||||
// 스펙 편집 모달 닫힘 시 버튼 색상 재조회(공개여부/등록여부 변경 반영)
|
||||
showModal(baseUrl, { title: 'OpenAPI Spec' }, modalW, modalH, function(){ applyOpenApiSpecStatus(); });
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAPI Spec 버튼 색상 = PTL_API_SPEC_INFO(api_id = getFullSvcName()) 상태.
|
||||
// - 미등록: 기본색 - 등록·비공개(display_yn≠Y): 노랑 - 등록·공개(display_yn=Y): 하늘
|
||||
function applyOpenApiSpecStatus() {
|
||||
var $btn = $('#btn_openapi_spec');
|
||||
$btn.removeClass('spec-exists spec-public').data('specMsg', '');
|
||||
var eaiSvcName = getFullSvcName();
|
||||
if (!eaiSvcName || eaiSvcName.length < 2) return;
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url_openapi_spec_json,
|
||||
dataType: 'json',
|
||||
data: { cmd: 'SPEC_STATUS', eaiSvcName: eaiSvcName },
|
||||
success: function(res) {
|
||||
var exists = res && (res.exists === true || res.exists === 'true');
|
||||
var isPublic = res && (res.displayYn === 'Y');
|
||||
var msg;
|
||||
if (!exists) {
|
||||
msg = 'OpenAPI Spec 미등록\n버튼을 눌러 스펙을 생성/편집하세요.';
|
||||
} else if (isPublic) {
|
||||
$btn.addClass('spec-public');
|
||||
msg = 'OpenAPI Spec 등록됨 · 포탈 공개중 (display_yn = Y)\n포탈 메인/문서에 노출됩니다.';
|
||||
} else {
|
||||
$btn.addClass('spec-exists');
|
||||
msg = 'OpenAPI Spec 등록됨 · 비공개 (display_yn ≠ Y)\n포탈에는 노출되지 않습니다.';
|
||||
}
|
||||
$btn.data('specMsg', msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
makeValidate();
|
||||
// var bootstrapButton = $.fn.button.noConflict()
|
||||
@@ -1000,9 +1085,31 @@
|
||||
}
|
||||
init(url,key,detail);
|
||||
|
||||
// v1: 기존 API 스펙 (모달) — 기능 비교용 복원
|
||||
$("#btn_api_spec").click(function() {
|
||||
showApiSpecPopup();
|
||||
});
|
||||
// v2: 신규 OpenAPI Spec (새 탭)
|
||||
$("#btn_openapi_spec").click(function(e) {
|
||||
showOpenApiSpecPopup(e);
|
||||
});
|
||||
|
||||
// OpenAPI Spec 버튼 호버 시 현재 상태 안내 레이어
|
||||
if ($('#specStatusLayer').length === 0) {
|
||||
$('body').append('<div id="specStatusLayer"></div>');
|
||||
}
|
||||
$("#btn_openapi_spec")
|
||||
.on('mouseenter', function() {
|
||||
var msg = $(this).data('specMsg');
|
||||
if (!msg) return;
|
||||
$('#specStatusLayer').text(msg).css('display', 'block');
|
||||
})
|
||||
.on('mousemove', function(e) {
|
||||
$('#specStatusLayer').css({ left: (e.clientX + 14) + 'px', top: (e.clientY + 16) + 'px' });
|
||||
})
|
||||
.on('mouseleave', function() {
|
||||
$('#specStatusLayer').hide();
|
||||
});
|
||||
|
||||
$("#btn_modify").click(modifyApi);
|
||||
$("#btn_delete").click(function(){
|
||||
@@ -1501,7 +1608,10 @@
|
||||
<button type="button" class="cssbtn" id="btn_json_export" level="R" status="DETAIL,NEW"><i class="material-icons">download</i> <%=localeMessage.getString("button.exportJson")%></button></button>
|
||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_modify" level="R" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_api_spec" level="R" status="DETAIL,NEW"><i class="material-icons">description</i> API 스펙</button>
|
||||
<!-- v1: 기존 API 스펙 (모달) — 기능 비교용 복원 -->
|
||||
<button type="button" class="cssbtn" id="btn_api_spec" level="R" status="DETAIL"><i class="material-icons">description</i> API 스펙(삭제예정)</button>
|
||||
<!-- v2: 신규 OpenAPI Spec (새 탭) -->
|
||||
<button type="button" class="cssbtn" id="btn_openapi_spec" level="R" status="DETAIL"><i class="material-icons">api</i> OpenAPI Spec</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" id="title" style="font-size:1.4em">${rmsMenuName}</div>
|
||||
@@ -1572,7 +1682,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 +1725,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 +1794,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 +1881,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">
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/transaction/apim/apiSpecMan.json" />';
|
||||
var url_org ='<c:url value="/onl/transaction/apim/apiSpecManPopup.view" />';
|
||||
var url_org ='<c:url value="/onl/transaction/apim/apiSpecMan.view" />';
|
||||
var isDetail = false;
|
||||
var sampleRequestEditor, sampleResponseEditor, testbedSpecEditor;
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<%@ 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");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title>API 그룹 선택</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css_custom.jsp"/>
|
||||
<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/apim/apigroup/apiGroupMan.json" />';
|
||||
|
||||
function search() {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var selectedGroupIds = urlParams.get("selectedGroupIds");
|
||||
|
||||
if (selectedGroupIds) {
|
||||
selectedGroupIds = selectedGroupIds.split(','); // 콤마로 구분된 ID들을 배열로 변환
|
||||
} else {
|
||||
selectedGroupIds = [];
|
||||
}
|
||||
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||
colNames: ['API 그룹 ID', 'API 그룹 명', '설명'],
|
||||
colModel: [
|
||||
{name: 'id', hidden: true},
|
||||
{name: 'groupName', align: 'center', width: "180"},
|
||||
{name: 'groupDesc', align: 'left', width: "200"}
|
||||
],
|
||||
jsonReader: {
|
||||
repeatitems: false
|
||||
},
|
||||
pager: $('#pager'),
|
||||
page: '${param.page}',
|
||||
rowNum: '${rmsDefaultRowNum}',
|
||||
autoheight: true,
|
||||
height: 'auto',
|
||||
width: 410,
|
||||
autowidth: false,
|
||||
viewrecords: true,
|
||||
shrinkToFit: false,
|
||||
multiselect: true,
|
||||
multiboxonly: false,
|
||||
loadComplete: function(data) {
|
||||
var $grid = $(this);
|
||||
$.each($grid.getDataIDs(), function(_, id) {
|
||||
var rowData = $grid.getRowData(id);
|
||||
if ($.inArray(rowData.id, selectedGroupIds) !== -1) {
|
||||
$grid.jqGrid('setSelection', id, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_search").click(search);
|
||||
|
||||
$("#btn_save").click(function() {
|
||||
var selectedRows = $('#grid').getGridParam('selarrrow');
|
||||
|
||||
var selectedGroups = [];
|
||||
if (selectedRows.length > 0) {
|
||||
selectedGroups = selectedRows.map(function(rowid) {
|
||||
var rowData = $('#grid').getRowData(rowid);
|
||||
return {
|
||||
id: rowData.id,
|
||||
groupName: rowData.groupName
|
||||
};
|
||||
});
|
||||
}
|
||||
// 부모 창의 selectApiGroup 함수 호출
|
||||
if (window.parent && typeof window.parent.selectApiGroup === "function") {
|
||||
window.parent.selectApiGroup(selectedGroups);
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_close").click(function () {
|
||||
window.close();
|
||||
});
|
||||
|
||||
// 검색어 입력 후 엔터 키 처리
|
||||
$("input[name=searchGroupName]").keydown(function(key) {
|
||||
if (key.keyCode == 13) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="popup_box">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_save" level="W">
|
||||
<i class="material-icons">save</i> <%= localeMessage.getString("button.save") %>
|
||||
</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_close" level="R" status="DETAIL"><i
|
||||
class="material-icons">close</i> <%= localeMessage.getString("button.close") %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="title"></div>
|
||||
<table class="search_condition" cellspacing="0">
|
||||
<tr>
|
||||
<th style="width:80px;">API 그룹 명</th>
|
||||
<td>
|
||||
<input type="text" name="searchGroupName" value="${param.searchGroupName}">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,179 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1600" />
|
||||
<title>OpenAPI Spec 에디터</title>
|
||||
|
||||
<%-- Tailwind (벤더된 Play CDN JS, 오프라인) --%>
|
||||
<script src="<c:url value='/plugins/openapi/tailwind.js'/>"></script>
|
||||
<script>
|
||||
tailwind.config = { theme: { extend: { colors: { brand: {
|
||||
50:'#eff6ff', 100:'#dbeafe', 500:'#3b82f6', 600:'#2563eb', 700:'#1d4ed8'
|
||||
} } } } };
|
||||
</script>
|
||||
|
||||
<%-- Swagger UI 5 (벤더) --%>
|
||||
<link rel="stylesheet" href="<c:url value='/plugins/swaggerUI/swagger-ui.css'/>" />
|
||||
<script src="<c:url value='/plugins/swaggerUI/swagger-ui-bundle.js'/>"></script>
|
||||
<script src="<c:url value='/plugins/swaggerUI/swagger-ui-standalone-preset.js'/>"></script>
|
||||
<script src="<c:url value='/plugins/swaggerUI/djb-swagger-i18n.js'/>" charset="UTF-8"></script>
|
||||
|
||||
<%-- js-yaml (벤더) --%>
|
||||
<script src="<c:url value='/plugins/openapi/js-yaml.min.js'/>"></script>
|
||||
|
||||
<%-- CodeMirror (YAML 편집기 — Monaco 대체, admin addon) --%>
|
||||
<link rel="stylesheet" href="<c:url value='/addon/codemirror/lib/codemirror.css'/>" />
|
||||
<script src="<c:url value='/addon/codemirror/lib/codemirror.js'/>"></script>
|
||||
<script src="<c:url value='/addon/codemirror/mode/yaml/yaml.js'/>"></script>
|
||||
<script src="<c:url value='/addon/codemirror/mode/javascript/javascript.js'/>"></script>
|
||||
|
||||
<%-- jQuery + Summernote(lite, 무-Bootstrap) — 설명 리치 에디터 --%>
|
||||
<link rel="stylesheet" href="<c:url value='/addon/summernote/summernote-lite.css'/>" />
|
||||
<%-- 개발자포탈과 동일한 에디터 콘텐츠 스타일(.editor-content, Noto Sans KR 16px) --%>
|
||||
<link rel="stylesheet" href="<c:url value='/js/djb/apispec/editor-content.css'/>" />
|
||||
<script src="<c:url value='/js/jquery-1.12.1.min.js'/>"></script>
|
||||
<script src="<c:url value='/addon/summernote/summernote-lite.min.js'/>"></script>
|
||||
<script src="<c:url value='/addon/summernote/lang/summernote-ko-KR.js'/>"></script>
|
||||
|
||||
<link rel="stylesheet" href="<c:url value='/js/djb/apispec/styles.css'/>" />
|
||||
<style>
|
||||
/* 페이지 자체 세로 스크롤 제거 — 뷰포트 고정, 내부 패널만 스크롤 */
|
||||
html, body { height: 100%; margin: 0; overflow-x: auto; overflow-y: hidden; }
|
||||
#djb-wrap { height: 100vh; display: flex; flex-direction: column; }
|
||||
#djb-wrap > header, #djb-wrap > nav { flex: 0 0 auto; }
|
||||
#body { flex: 1 1 auto; min-height: 0 !important; overflow: hidden; }
|
||||
#form-panel { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||
#form-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
||||
#preview-panel { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||
#preview-swagger, #preview-monaco { flex: 1 1 auto; min-height: 0; height: auto !important; overflow: auto; }
|
||||
#preview-monaco .CodeMirror { height: 100%; }
|
||||
/* 패널 내부 sticky 는 flex 행으로 (스크롤 컨테이너는 form-content / preview) */
|
||||
#form-panel > .sticky, #preview-panel > .sticky { position: static !important; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.DJB_CTX = {
|
||||
eaiSvcName: "${param.eaiSvcName}",
|
||||
apiName: "${param.eaiSvcDesc}",
|
||||
detailUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>?cmd=DETAIL_SPEC&serviceType=APIGW",
|
||||
saveUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>?cmd=SAVE&serviceType=APIGW",
|
||||
swaggerBase: "<c:url value='/plugins/swaggerUI/'/>",
|
||||
gwAddress: "${gwAddress}",
|
||||
useProxy: ${useProxy},
|
||||
tokenUseProxy: ${tokenUseProxy},
|
||||
exportEnabled: ${exportEnabled},
|
||||
orgPopupUrl: "<c:url value='/onl/transaction/apim/apiSpecMan.view'/>",
|
||||
groupPopupUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.view'/>?cmd=GROUP_POPUP",
|
||||
jsonUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>"
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800 antialiased" style="min-width:1600px;">
|
||||
|
||||
<div id="djb-wrap" class="mx-auto" style="width:1600px;">
|
||||
|
||||
<!-- ===== Header ===== -->
|
||||
<header class="h-14 flex items-center justify-between px-6 bg-white border-b border-slate-200 sticky top-0 z-30">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="font-semibold text-slate-900">OpenAPI Spec 에디터</div>
|
||||
<span class="text-slate-300">|</span>
|
||||
<span class="text-sm text-slate-500">인터페이스:</span>
|
||||
<span class="text-sm font-medium text-slate-800" id="hdr-project-name">${param.eaiSvcName}</span>
|
||||
<span class="ml-2 px-2 py-0.5 text-[11px] rounded-full bg-slate-100 text-slate-600 border border-slate-200" id="hdr-version">v1.0.0</span>
|
||||
<span id="hdr-source" class="hidden ml-1 px-2 py-0.5 text-[11px] rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="btn-newwin" class="hidden px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">새창으로 띄우기</button>
|
||||
<button id="btn-regen" class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">자동 생성(초기화)</button>
|
||||
<button id="btn-save" class="px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700">저장</button>
|
||||
<%-- 내보내기 버튼은 PTL_PROPERTY(djb.openapi.export.enabled=true) 일 때만 노출. 기본 미노출. --%>
|
||||
<c:if test="${exportEnabled}">
|
||||
<div class="relative">
|
||||
<button id="btn-export" class="px-3 py-1.5 text-sm rounded bg-brand-600 text-white hover:bg-brand-700">내보내기 ▾</button>
|
||||
<div id="export-menu" class="hidden absolute right-0 mt-1 w-44 bg-white border border-slate-200 rounded shadow-lg z-40">
|
||||
<button data-export="yaml" class="block w-full text-left px-3 py-2 text-sm hover:bg-slate-50">YAML 다운로드</button>
|
||||
<button data-export="json" class="block w-full text-left px-3 py-2 text-sm hover:bg-slate-50">JSON 다운로드</button>
|
||||
</div>
|
||||
</div>
|
||||
</c:if>
|
||||
<button id="btn-close" class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">닫기</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ===== Stepper ===== -->
|
||||
<nav class="bg-white border-b border-slate-200 sticky z-20" style="top:56px;">
|
||||
<div class="h-1 bg-slate-100">
|
||||
<div id="stepper-progress" class="h-1 bg-brand-500 transition-all duration-300" style="width:16.66%"></div>
|
||||
</div>
|
||||
<ol id="stepper" class="grid grid-cols-5 px-2"></ol>
|
||||
</nav>
|
||||
|
||||
<!-- ===== Body ===== -->
|
||||
<main id="body" class="flex" style="min-height: calc(100vh - 56px - 72px);">
|
||||
<section id="form-panel" class="bg-white border-r border-slate-200 transition-all duration-300" style="width:960px;">
|
||||
<div class="flex items-center justify-between px-6 py-3 border-b border-slate-100 sticky bg-white z-10" style="top:128px;">
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="text-xs uppercase tracking-wide text-slate-400">Step <span id="form-step-num">1</span> / 5</span>
|
||||
<h2 class="text-base font-semibold text-slate-800" id="form-step-title">기본정보</h2>
|
||||
</div>
|
||||
<button id="btn-toggle-preview-from-form" class="hidden text-xs px-2 py-1 rounded border border-slate-200 text-slate-600 hover:bg-slate-50">▶ 미리보기 다시 열기</button>
|
||||
</div>
|
||||
<div id="form-content" class="px-6 py-5"></div>
|
||||
<div class="sticky bottom-0 bg-white border-t border-slate-200 px-6 py-3 flex items-center justify-between">
|
||||
<button id="btn-prev" class="px-4 py-2 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed">◀ 이전</button>
|
||||
<div class="text-xs text-slate-500" id="footer-hint">필수 항목을 채우고 다음 단계로 이동하세요</div>
|
||||
<button id="btn-next" class="px-4 py-2 text-sm rounded bg-brand-600 text-white hover:bg-brand-700">다음 ▶</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside id="preview-panel" class="bg-slate-50 transition-all duration-300" style="width:640px;">
|
||||
<div class="flex items-center justify-between px-4 py-2 border-b border-slate-200 bg-white sticky z-10" style="top:128px;">
|
||||
<div class="flex items-center gap-1">
|
||||
<button data-ptab="swagger" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-brand-600 text-brand-700 font-medium">Swagger UI</button>
|
||||
<button data-ptab="yaml-ro" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-transparent text-slate-600 hover:text-slate-800">YAML 보기</button>
|
||||
<button data-ptab="yaml-edit" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-transparent text-slate-600 hover:text-slate-800">스펙 편집</button>
|
||||
</div>
|
||||
<button id="btn-toggle-preview" class="text-xs px-2 py-1 rounded border border-slate-200 text-slate-600 hover:bg-slate-100">패널 숨기기 ▶</button>
|
||||
</div>
|
||||
<div id="yaml-edit-bar" class="hidden flex items-center justify-between bg-amber-50 border-b border-amber-200 px-4 py-2 text-xs">
|
||||
<span class="text-amber-800">YAML 을 직접 편집한 뒤 [Form 에 적용] 을 눌러 양식에 동기화하세요.</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="btn-yaml-revert" class="px-2 py-1 rounded border border-amber-300 bg-white hover:bg-amber-100">변경 취소</button>
|
||||
<button id="btn-yaml-apply" class="px-2 py-1 rounded bg-amber-600 text-white hover:bg-amber-700">Form 에 적용</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="preview-swagger" class="overflow-auto" style="height: calc(100vh - 56px - 72px - 41px);"></div>
|
||||
<div id="preview-monaco" class="hidden" style="height: calc(100vh - 56px - 72px - 41px);"></div>
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="fixed bottom-5 left-1/2 -translate-x-1/2 hidden px-4 py-2 rounded bg-slate-900 text-white text-sm shadow-lg z-50"></div>
|
||||
|
||||
<!-- Security scheme modal -->
|
||||
<div id="sec-modal" class="hidden fixed inset-0 z-50 bg-slate-900/40 flex items-center justify-center">
|
||||
<div class="bg-white rounded-lg shadow-xl w-[640px] max-h-[80vh] overflow-auto">
|
||||
<div class="flex items-center justify-between px-5 py-3 border-b border-slate-200">
|
||||
<h3 class="font-semibold text-slate-800">보안 스킴 추가</h3>
|
||||
<button class="text-slate-400 hover:text-slate-700" data-sec-close>✕</button>
|
||||
</div>
|
||||
<div class="p-5" id="sec-modal-body"></div>
|
||||
<div class="px-5 py-3 border-t border-slate-200 flex justify-end gap-2 bg-slate-50">
|
||||
<button class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white" data-sec-close>취소</button>
|
||||
<button id="btn-sec-save" class="px-3 py-1.5 text-sm rounded bg-brand-600 text-white">추가</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="<c:url value='/js/djb/apispec/sample-data.js'/>"></script>
|
||||
<script src="<c:url value='/js/djb/apispec/app.js'/>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -380,15 +380,15 @@
|
||||
{name: 'TRACKASISKEY2CTNT', hidden: true},
|
||||
|
||||
{name: 'EAISVCSERNO_TMP', align: 'left', width: '220', formatter: trimFormatter},
|
||||
{name: 'KEYMGTMSGCTNT', align: 'left', width: '240'},
|
||||
{name: 'KEYMGTMSGCTNT', align: 'left', width: '300'},
|
||||
{name: 'LOGPRCSSSERNO_TMP', align: 'center', width: '40'},
|
||||
{name: 'TELGMRECVTRANCD', align: 'left', width: '135', hidden: true},
|
||||
{name: 'EAISVCNAME', align: 'left', width: '130'},
|
||||
{name: 'EAISVCNAME', align: 'left', width: '160'},
|
||||
{name: 'EAISVCDESC', align: 'left', width: '280'},
|
||||
{name: 'EAIBZWKDSTCD_TMP', align: 'center', width: '50'},
|
||||
{name: 'EXTBIZCD', align: 'center', width: '50', hidden: isApiGw},
|
||||
{name: 'REFKEY', align: 'left', width: '280', hidden: isApiGw},
|
||||
{name: 'CLIENTNAME', align: 'left', width: '260', hidden: !isApiGw},
|
||||
{name: 'CLIENTNAME', align: 'left', width: '120', hidden: !isApiGw},
|
||||
{name: 'ORGNAME', align: 'left', width: '100', hidden: !isApiGw, formatter: orgNameFormatter},
|
||||
{name: 'MSGDPSTYMS_TMP', align: 'center', width: '150', formatter: timeStampFormat2},
|
||||
{name: 'MSGPRCSSYMS', align: 'center', width: '150', formatter: timeStampFormat2},
|
||||
|
||||
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,122 @@
|
||||
/*!
|
||||
* djb-swagger-i18n.js
|
||||
* Swagger UI 영문 라벨을 한글로 치환하는 i18n 패치 (방법 B - DOM patch)
|
||||
* 배포 경로 권장: static/plugins/swaggerUI/djb-swagger-i18n.js
|
||||
* index.html 의 swagger-initializer.js 직전 또는 직후에 <script> 로 로드
|
||||
*
|
||||
* ?lang=en 쿼리스트링이 있으면 패치 비활성화 (원문 보기)
|
||||
* 매핑 카탈로그는 01-i18n-mapping/i18n-strings.csv 와 동기 유지
|
||||
*/
|
||||
(function (window) {
|
||||
'use strict';
|
||||
|
||||
// ── 비활성화 옵션 ──
|
||||
if (/[?&]lang=en\b/.test(window.location.search)) return;
|
||||
|
||||
// ── 매핑 (key 는 디버깅용, 실제 비교는 EN 문자열 일치) ──
|
||||
var I18N_MAP = {
|
||||
// P0 — OAuth2 모달 본문
|
||||
'auth.modal.title': { en: 'Available authorizations', ko: '사용 가능한 인증' },
|
||||
'auth.scopes.description': {
|
||||
en: 'Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.',
|
||||
ko: '스코프는 최종 사용자를 대신하여 애플리케이션이 데이터에 접근할 수 있는 권한 수준을 부여합니다. 각 API는 하나 이상의 스코프를 선언할 수 있습니다.'
|
||||
},
|
||||
'auth.scopes.swagger_grant': {
|
||||
en: 'API requires the following scopes. Select which ones you want to grant to Swagger UI.',
|
||||
ko: '이 API 는 아래 스코프가 필요합니다. Swagger UI 에 부여할 스코프를 선택하세요.'
|
||||
},
|
||||
'auth.flow.clientCredentials': { en: 'OAuth2 client credentials flow', ko: 'OAuth2 클라이언트 자격증명 흐름' },
|
||||
'auth.flow.implicit': { en: 'OAuth2 implicit flow', ko: 'OAuth2 암묵적 흐름' },
|
||||
'auth.flow.password': { en: 'OAuth2 password flow', ko: 'OAuth2 비밀번호 흐름' },
|
||||
'auth.flow.authorizationCode': { en: 'OAuth2 authorization code flow', ko: 'OAuth2 인가 코드 흐름' },
|
||||
'auth.field.tokenUrl': { en: 'Token URL:', ko: '토큰 URL:' },
|
||||
'auth.field.flow': { en: 'Flow:', ko: '흐름:' },
|
||||
'auth.field.clientId': { en: 'client_id:', ko: '클라이언트 ID (client_id):' },
|
||||
'auth.field.clientSecret': { en: 'client_secret:',ko: '클라이언트 시크릿 (client_secret):' },
|
||||
'auth.field.scopes': { en: 'Scopes:', ko: '스코프:' },
|
||||
'auth.action.selectAll': { en: 'select all', ko: '모두 선택' },
|
||||
'auth.action.selectNone': { en: 'select none', ko: '선택 해제' },
|
||||
'auth.action.authorize': { en: 'Authorize', ko: '인증' },
|
||||
'auth.action.close': { en: 'Close', ko: '닫기' },
|
||||
'auth.action.logout': { en: 'Logout', ko: '로그아웃' },
|
||||
'auth.modal.authorized': { en: 'authorized', ko: '인증됨' },
|
||||
|
||||
// P1 — 기타 인증 흐름
|
||||
'auth.scheme.apiKey': { en: 'API key', ko: 'API 키' },
|
||||
'auth.field.name': { en: 'name:', ko: '이름:' },
|
||||
'auth.field.in': { en: 'in:', ko: '위치:' },
|
||||
'auth.field.value': { en: 'Value:', ko: '값:' },
|
||||
'basic.field.username': { en: 'Username:',ko: '사용자 ID:' },
|
||||
'basic.field.password': { en: 'Password:',ko: '비밀번호:' }
|
||||
// P2 (오퍼레이션/응답) 는 필요 시 추가
|
||||
};
|
||||
|
||||
// 빠른 lookup 을 위해 EN → KO 인덱스 사전 생성
|
||||
var EN_TO_KO = Object.create(null);
|
||||
Object.keys(I18N_MAP).forEach(function (k) {
|
||||
var e = I18N_MAP[k];
|
||||
EN_TO_KO[e.en] = e.ko;
|
||||
});
|
||||
|
||||
// ── 치환 대상 노드 식별 + 치환 ──
|
||||
function translateNode(node) {
|
||||
if (!node) return;
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
var raw = node.nodeValue;
|
||||
if (!raw) return;
|
||||
var trimmed = raw.trim();
|
||||
if (!trimmed) return;
|
||||
// 1) 완전 일치 (가장 안전)
|
||||
if (EN_TO_KO[trimmed]) {
|
||||
node.nodeValue = raw.replace(trimmed, EN_TO_KO[trimmed]);
|
||||
return;
|
||||
}
|
||||
// 2) 라벨 colon 패턴: "Token URL:" 같이 끝이 ":" 인 짧은 라벨
|
||||
// (불필요 — 1번 완전 일치로 대부분 커버)
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
if (node.tagName === 'SCRIPT' || node.tagName === 'STYLE') return;
|
||||
|
||||
// input placeholder / button title 도 일부 케이스에 대비
|
||||
if (node.tagName === 'INPUT' && node.placeholder && EN_TO_KO[node.placeholder.trim()]) {
|
||||
node.placeholder = EN_TO_KO[node.placeholder.trim()];
|
||||
}
|
||||
|
||||
var i, child = node.childNodes, len = child.length;
|
||||
for (i = 0; i < len; i++) translateNode(child[i]);
|
||||
}
|
||||
|
||||
function translateAll() {
|
||||
var root = document.getElementById('swagger-ui') || document.body;
|
||||
translateNode(root);
|
||||
}
|
||||
|
||||
// ── MutationObserver — SwaggerUI 가 동적으로 DOM 을 다시 그릴 때마다 재적용 ──
|
||||
var observer = new MutationObserver(function (mutations) {
|
||||
for (var i = 0; i < mutations.length; i++) {
|
||||
var m = mutations[i];
|
||||
if (m.addedNodes && m.addedNodes.length) {
|
||||
for (var j = 0; j < m.addedNodes.length; j++) translateNode(m.addedNodes[j]);
|
||||
}
|
||||
if (m.type === 'characterData') {
|
||||
translateNode(m.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function start() {
|
||||
translateAll();
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', start);
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
})(window);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user