Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
Vendored
+142
-148
@@ -1,180 +1,174 @@
|
||||
pipeline {
|
||||
agent { label 'djb-vm' }
|
||||
agent none
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
skipDefaultCheckout() // stage별 agent의 자동 SCM checkout 방지 (Deploy 노드는 git 접근 불필요, unstash로만 WAR 수신)
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
JAVA_HOME = '/apps/opts/jdk8'
|
||||
JAVA_HOME_TOMCAT = '/apps/opts/jdk17'
|
||||
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}"
|
||||
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
|
||||
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'
|
||||
WEBHOOK_URL = 'http://172.30.1.50:18000/api/hooks/01ba51b01d3c4c98ae8f3183a23a84ed713197857d024b5da4f303458195eb32'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
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/.."
|
||||
stage('Checkout dependencies') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
cd "$WORKSPACE/.."
|
||||
|
||||
mkdir -p eapim-online
|
||||
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-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
|
||||
'''
|
||||
}
|
||||
}
|
||||
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('Verify toolchain') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
java -version
|
||||
gradle --version
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build WAR') {
|
||||
steps {
|
||||
sh 'gradle clean build -x test --no-daemon'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
sh '''
|
||||
set -eu
|
||||
cd build/libs
|
||||
sha1sum eapim-admin.war > eapim-admin.war.sha1
|
||||
sha256sum eapim-admin.war > eapim-admin.war.sha256
|
||||
md5sum eapim-admin.war > eapim-admin.war.md5
|
||||
'''
|
||||
archiveArtifacts artifacts: 'build/libs/eapim-admin.war,build/libs/eapim-admin.war.sha1,build/libs/eapim-admin.war.sha256,build/libs/eapim-admin.war.md5', fingerprint: true
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Stop Tomcat') {
|
||||
steps {
|
||||
sh '''
|
||||
set +e
|
||||
systemctl --user stop eapim-admin 2>/dev/null
|
||||
|
||||
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"
|
||||
'''
|
||||
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 & 로 백그라운드 기동, 즉시 리턴
|
||||
|
||||
stage('Deploy monitoring.war') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
DEPLOY_DIR="$CATALINA_BASE/webapps"
|
||||
rm -rf "$DEPLOY_DIR/monitoring" "$DEPLOY_DIR/monitoring.war"
|
||||
cp build/libs/eapim-admin.war "$DEPLOY_DIR/monitoring.war"
|
||||
ls -la "$DEPLOY_DIR"
|
||||
'''
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
stage('Start Tomcat and readiness') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
LOG="$CATALINA_BASE/logs/catalina.$(date +%Y-%m-%d).log"
|
||||
BEFORE=0
|
||||
[ -f "$LOG" ] && BEFORE=$(stat -c %s "$LOG")
|
||||
|
||||
systemctl --user start eapim-admin
|
||||
|
||||
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 "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 "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 not listening)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "--- key boot log lines ---"
|
||||
tail -c +$((BEFORE+1)) "$LOG" | grep -E "Server startup|Deployment of web" | head -10 || true
|
||||
'''
|
||||
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"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:310px; 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:'';}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,334 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
// 계좌이체 API 모의 데이터
|
||||
// locked: true 인 필드는 게이트웨이가 자동 주입한 값(회색 + 자물쇠)
|
||||
// locked: false / 미지정 인 필드는 사용자가 자유롭게 편집
|
||||
|
||||
window.SAMPLE_DATA = (function () {
|
||||
return {
|
||||
info: {
|
||||
title: { value: '계좌이체 API', locked: false },
|
||||
version: { value: '1.0.0', locked: false },
|
||||
summary: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
termsOfService: { value: '', locked: false },
|
||||
contact: {
|
||||
name: { value: 'DJB API Team', locked: false },
|
||||
email: { value: 'api@djb.co.kr', locked: false },
|
||||
url: { value: '', locked: false }
|
||||
},
|
||||
license: {
|
||||
name: { value: '', locked: false },
|
||||
url: { value: '', locked: false }
|
||||
}
|
||||
},
|
||||
|
||||
tags: [
|
||||
{ name: 'transfer', description: '계좌이체 관련 API' }
|
||||
],
|
||||
|
||||
externalDocs: {
|
||||
description: { value: '', locked: false },
|
||||
url: { value: '', locked: false }
|
||||
},
|
||||
|
||||
servers: [
|
||||
{
|
||||
url: { value: 'https://api-dev.djb.co.kr', locked: true },
|
||||
env: 'development',
|
||||
description: { value: '개발 환경', locked: false }
|
||||
},
|
||||
{
|
||||
url: { value: 'https://api-stg.djb.co.kr', locked: true },
|
||||
env: 'staging',
|
||||
description: { value: '스테이징 환경', locked: false }
|
||||
},
|
||||
{
|
||||
url: { value: 'https://api.djb.co.kr', locked: true },
|
||||
env: 'production',
|
||||
description: { value: '운영 환경', locked: false }
|
||||
}
|
||||
],
|
||||
|
||||
serverVariables: [
|
||||
{
|
||||
name: { value: 'basePath', locked: true },
|
||||
default: { value: '/v1', locked: false },
|
||||
enumStr: { value: '/v1,/v2', locked: false },
|
||||
description: { value: 'API 버전 경로', locked: false }
|
||||
}
|
||||
],
|
||||
|
||||
operation: {
|
||||
method: { value: 'POST', locked: true },
|
||||
path: { value: '/v1/accounts/transfer', locked: true },
|
||||
operationId: { value: 'transferAccount', locked: false },
|
||||
summary: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
tags: { value: ['transfer'], locked: false },
|
||||
deprecated: { value: false, locked: false }
|
||||
},
|
||||
|
||||
// 파라미터 (Path/Query/Header/Cookie)
|
||||
parameters: [
|
||||
{
|
||||
in: 'header',
|
||||
name: { value: 'X-Request-Id', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: 'uuid', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '', locked: false }
|
||||
}
|
||||
],
|
||||
|
||||
// Request Body 스키마 (중첩 3단)
|
||||
requestBody: {
|
||||
mediaType: 'application/json',
|
||||
required: { value: true, locked: true },
|
||||
schema: [
|
||||
{
|
||||
name: { value: 'transactionId', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '40', locked: false },
|
||||
pattern: { value: '^tx-[0-9a-zA-Z-]+$', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: 'tx-20260522-001', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'sender', locked: true },
|
||||
type: { value: 'object', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '송금인 정보', locked: false },
|
||||
example: { value: '', locked: false },
|
||||
children: [
|
||||
{
|
||||
name: { value: 'accountNumber', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '20', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '110-1234-567890', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'bankCode', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '3', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '088', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'holderName', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '60', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '김철수', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { value: 'receiver', locked: true },
|
||||
type: { value: 'object', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '수취인 정보', locked: false },
|
||||
example: { value: '', locked: false },
|
||||
children: [
|
||||
{
|
||||
name: { value: 'accountNumber', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '20', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '111-2345-678901', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'bankCode', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '3', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '020', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'holderName', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '60', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '이영희', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { value: 'amount', locked: true },
|
||||
type: { value: 'object', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '이체 금액', locked: false },
|
||||
example: { value: '', locked: false },
|
||||
children: [
|
||||
{
|
||||
name: { value: 'value', locked: true },
|
||||
type: { value: 'integer', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: 'int64', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '50000', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'currency', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '3', locked: false },
|
||||
pattern: { value: '^[A-Z]{3}$', locked: false },
|
||||
description: { value: 'ISO 4217 통화코드', locked: false },
|
||||
example: { value: 'KRW', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { value: 'memo', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: false, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '100', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '5월 회비', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 응답 스키마 (상태코드 별)
|
||||
responses: {
|
||||
'200': {
|
||||
description: { value: '이체 성공', locked: false },
|
||||
schema: [
|
||||
{ name: { value: 'transactionId', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: 'tx-20260522-001', locked: false }, children: null },
|
||||
{ name: { value: 'status', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: 'COMPLETED', locked: false }, children: null },
|
||||
{ name: { value: 'completedAt', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: 'date-time', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '2026-05-22T10:00:00Z', locked: false }, children: null },
|
||||
{
|
||||
name: { value: 'balance', locked: true }, type: { value: 'object', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '잔액 정보', locked: false }, example: { value: '', locked: false },
|
||||
children: [
|
||||
{ name: { value: 'before', locked: true }, type: { value: 'integer', locked: true }, required: { value: true, locked: true }, format: { value: 'int64', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '1000000', locked: false }, children: null },
|
||||
{ name: { value: 'after', locked: true }, type: { value: 'integer', locked: true }, required: { value: true, locked: true }, format: { value: 'int64', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '950000', locked: false }, children: null }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
'400': {
|
||||
description: { value: '잘못된 요청', locked: false },
|
||||
schema: [
|
||||
{
|
||||
name: { value: 'error', locked: true }, type: { value: 'object', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '', locked: false },
|
||||
children: [
|
||||
{ name: { value: 'code', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: 'INVALID_AMOUNT', locked: false }, children: null },
|
||||
{ name: { value: 'message', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '금액이 올바르지 않습니다', locked: false }, children: null },
|
||||
{ name: { value: 'details', locked: true }, type: { value: 'array', locked: true }, required: { value: false, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '', locked: false }, itemsType: { value: 'string', locked: false }, children: null }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
'401': { description: { value: '인증 실패', locked: false }, schema: [] },
|
||||
'500': { description: { value: '서버 오류', locked: false }, schema: [] }
|
||||
},
|
||||
|
||||
// 보안 스킴
|
||||
securitySchemes: [
|
||||
{
|
||||
key: 'ApiKeyAuth',
|
||||
type: 'apiKey',
|
||||
locked: true,
|
||||
name: { value: 'X-API-Key', locked: true },
|
||||
in: { value: 'header', locked: true },
|
||||
description: { value: '', locked: false }
|
||||
}
|
||||
],
|
||||
globalSecurity: ['ApiKeyAuth'],
|
||||
|
||||
// 예제
|
||||
examples: {
|
||||
request: {
|
||||
'application/json': {
|
||||
default: '{\n "transactionId": "tx-20260522-001",\n "sender": {\n "accountNumber": "110-1234-567890",\n "bankCode": "088",\n "holderName": "김철수"\n },\n "receiver": {\n "accountNumber": "111-2345-678901",\n "bankCode": "020",\n "holderName": "이영희"\n },\n "amount": {\n "value": 50000,\n "currency": "KRW"\n },\n "memo": "5월 회비"\n}'
|
||||
}
|
||||
},
|
||||
response: {
|
||||
'200': { body: '{\n "transactionId": "tx-20260522-001",\n "status": "COMPLETED"\n}', headers: [ { name: 'X-RateLimit-Remaining', type: 'integer', description: '남은 요청 횟수', example: '99' } ] }
|
||||
}
|
||||
},
|
||||
|
||||
// 문서 옵션
|
||||
docOptions: {
|
||||
theme: 'light',
|
||||
lang: 'ko',
|
||||
includeExamples: true,
|
||||
tryItOut: true,
|
||||
defaultExpandDepth: -1,
|
||||
layout: 'BaseLayout',
|
||||
tagFilter: [],
|
||||
sideToc: true
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -1,103 +0,0 @@
|
||||
/* 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; }
|
||||
@@ -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>
|
||||
@@ -531,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 모달 스타일 -->
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
@@ -559,13 +574,20 @@
|
||||
<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>
|
||||
|
||||
@@ -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("수정");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,11 +71,11 @@
|
||||
|
||||
$('#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("layoutMan.itmeEn") %>', name: 'loutItemName', type: 'text', width:280, resize:true, minWidth:120 },
|
||||
{ title: '<%=localeMessage.getString("standardLayout.itemLevel")%>', name: 'loutItemDepth', type: 'text', 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 },
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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/apiSpecMan.view"/>';
|
||||
var url_popup_view = '<c:url value="/onl/transaction/apim/apiSpecManPopup.view"/>';
|
||||
|
||||
var isDetail = false;
|
||||
let dialog;
|
||||
|
||||
@@ -131,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()%>/';
|
||||
@@ -176,7 +194,7 @@
|
||||
<div class="title">승인 요청 목록<span class="tooltip">승인 요청을 관리합니다.</span></div>
|
||||
<c:if test="${hardDeleteEnabled}">
|
||||
<div style="background:#fff3cd; border:1px solid #ffc107; padding:8px 15px; margin:5px 0; border-radius:4px; color:#856404;">
|
||||
<strong>[주의]</strong> 선택 삭제 시 승인 요청과 승인자 이력이 DB에서 영구 삭제됩니다.
|
||||
<strong>[주의]</strong> 삭제 기능은 <strong>개발(dev) 환경에서만</strong> 동작합니다. 선택 삭제 시 승인 요청과 승인자 이력이 DB에서 영구 삭제됩니다.
|
||||
</div>
|
||||
</c:if>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
function formatInquiryStatus(cellvalue, options, rowObject) {
|
||||
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') {
|
||||
@@ -207,6 +209,7 @@
|
||||
<select name="searchInquiryStatus">
|
||||
<option value="">전체</option>
|
||||
<option value="PENDING">문의중</option>
|
||||
<option value="REVIEWING">문의검토중</option>
|
||||
<option value="RESPONDED">답변완료</option>
|
||||
<option value="CLOSED">답변종료</option>
|
||||
</select>
|
||||
|
||||
@@ -108,7 +108,9 @@
|
||||
|
||||
inquiryStatus = data.inquiryStatus;
|
||||
if (data.inquiryStatus == 'PENDING') {
|
||||
$("#inquiryStatus").text('문의중');
|
||||
$("#inquiryStatus").text('문의중');
|
||||
} else if (data.inquiryStatus == 'REVIEWING') {
|
||||
$("#inquiryStatus").text('문의검토중');
|
||||
} else if (data.inquiryStatus == 'RESPONDED') {
|
||||
$("#inquiryStatus").text('답변완료');
|
||||
} else if (data.inquiryStatus == 'CLOSED') {
|
||||
|
||||
@@ -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>
|
||||
<%--법인정보관리--%>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
response.setHeader("Expires", "0");
|
||||
|
||||
MonitoringContext monitoringContext = (MonitoringContext)CommonUtil.getBean(request, "monitoringContext");
|
||||
String reverseProxyUrl = monitoringContext.getStringProperty("djb.reverse_proxy.url", "");
|
||||
String reverseProxyUrl = monitoringContext.getStringProperty(MonitoringContext.RMS_WEBHOOK_REVERSE_PROXY_URL, "");
|
||||
%>
|
||||
<%!
|
||||
public String getRequiredLabel(String label) {
|
||||
@@ -138,7 +138,11 @@
|
||||
$("#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});
|
||||
@@ -480,8 +484,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (confirm("<%= localeMessage.getString("common.checkSave")%>") !== true)
|
||||
return;
|
||||
// 저장 확인/사유 입력은 유효성 검사 및 FormData 구성 후 처리 (하단 submit 분기)
|
||||
|
||||
// FormData 생성 전에 분리된 전화번호 필드 제거
|
||||
$("#scPhonePrefix, #scPhoneMiddle, #scPhoneLast, #orgPhonePrefix, #orgPhoneMiddle, #orgPhoneLast").removeAttr('name');
|
||||
@@ -516,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() {
|
||||
@@ -541,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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -86,11 +86,9 @@
|
||||
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: 150, align: 'left', editable: true},
|
||||
{name: 'PRPTYDESC', width: 200, align: 'left', editable: true, edittype: 'textarea', editoptions: {rows: 2}},
|
||||
{name: 'PRPTY2VAL', width: 200, align: 'left', editable: true},
|
||||
{
|
||||
name: 'DELETEYN',
|
||||
width: 20,
|
||||
@@ -260,7 +258,6 @@
|
||||
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"));
|
||||
@@ -343,11 +340,6 @@
|
||||
<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>
|
||||
|
||||
@@ -347,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>
|
||||
|
||||
@@ -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) {
|
||||
@@ -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 {
|
||||
// 신규 등록: 사유 불필요
|
||||
if (!confirm("<%= localeMessage.getString("common.checkSave")%>")) return;
|
||||
submitUser("INSERT", null);
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_previous").click(function () {
|
||||
@@ -518,21 +542,26 @@
|
||||
});
|
||||
|
||||
$("#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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
@@ -525,35 +525,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();
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
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 returnUrl ;
|
||||
let headerValues = [];
|
||||
let headerNames = [];
|
||||
@@ -487,6 +486,7 @@
|
||||
}
|
||||
|
||||
if(data.errResponseTransform != null && data.errResponseTransform != '' && data.errTransformYn == 'Y') {
|
||||
$('#toggleTransformYn').bootstrapToggle('on');
|
||||
$('#toggleErrTransformYn').bootstrapToggle('on');
|
||||
$('#rowErrResponseTransform').show();
|
||||
} else {
|
||||
@@ -993,28 +993,6 @@
|
||||
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 을 자동 부착.
|
||||
showModal(baseUrl, { title: 'OpenAPI Spec' }, 1600, 880);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
makeValidate();
|
||||
// var bootstrapButton = $.fn.button.noConflict()
|
||||
@@ -1029,14 +1007,9 @@
|
||||
}
|
||||
init(url,key,detail);
|
||||
|
||||
// v1: 기존 API 스펙 (모달) — 기능 비교용 복원
|
||||
$("#btn_api_spec").click(function() {
|
||||
showApiSpecPopup();
|
||||
});
|
||||
// v2: 신규 OpenAPI Spec (새 탭)
|
||||
$("#btn_openapi_spec").click(function(e) {
|
||||
showOpenApiSpecPopup(e);
|
||||
});
|
||||
|
||||
$("#btn_modify").click(modifyApi);
|
||||
$("#btn_delete").click(function(){
|
||||
@@ -1535,10 +1508,7 @@
|
||||
<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>
|
||||
<!-- v1: 기존 API 스펙 (모달) — 기능 비교용 복원 -->
|
||||
<button type="button" class="cssbtn" id="btn_api_spec" level="R" status="DETAIL,NEW"><i class="material-icons">description</i> API 스펙</button>
|
||||
<!-- v2: 신규 OpenAPI Spec (새 탭) -->
|
||||
<button type="button" class="cssbtn" id="btn_openapi_spec" level="R" status="DETAIL,NEW"><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>
|
||||
|
||||
@@ -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/apiSpecMan.view" />';
|
||||
var url_org ='<c:url value="/onl/transaction/apim/apiSpecManPopup.view" />';
|
||||
var isDetail = false;
|
||||
var sampleRequestEditor, sampleResponseEditor, testbedSpecEditor;
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<%@ page import="java.io.*"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<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>
|
||||
@@ -1,173 +0,0 @@
|
||||
<%@ 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}",
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,122 +0,0 @@
|
||||
/*!
|
||||
* 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);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+85
-80
@@ -1,9 +1,10 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'java-library'
|
||||
id 'eclipse'
|
||||
id 'eclipse-wtp'
|
||||
id 'eclipse-wtp'
|
||||
id 'idea'
|
||||
id 'war'
|
||||
id 'com.diffplug.eclipse.apt' version '3.41.1'
|
||||
}
|
||||
|
||||
group 'com.eactive'
|
||||
@@ -31,11 +32,11 @@ allprojects {
|
||||
project.webAppDirName = 'WebContent'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(8)
|
||||
}
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(8)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
@@ -45,56 +46,56 @@ sourceSets {
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs = ['-parameters']
|
||||
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs = ['-parameters']
|
||||
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
|
||||
|
||||
aptOptions {
|
||||
processorArgs = [ 'querydsl.generatedAnnotationClass' : 'com.querydsl.core.annotations.Generated' ]
|
||||
}
|
||||
}
|
||||
|
||||
war {
|
||||
def profile = project.findProperty("profile") ?: "tomcat"
|
||||
|
||||
if (profile == "weblogic") {
|
||||
webXml = file("WebContent/WEB-INF/weblogic-web.xml")ㅇ
|
||||
exclude 'WEB-INF/web.xml'
|
||||
}
|
||||
|
||||
|
||||
// rootSpec.exclude '**/persistence.xml'
|
||||
exclude '**/context.xml'
|
||||
exclude '**/context-*.xml'
|
||||
archiveFileName = "eapim-admin.war"
|
||||
duplicatesStrategy = DuplicatesStrategy.WARN
|
||||
def profile = project.findProperty("profile") ?: "tomcat"
|
||||
|
||||
if (profile == "weblogic") {
|
||||
webXml = file("WebContent/WEB-INF/weblogic-web.xml")
|
||||
exclude 'WEB-INF/web.xml'
|
||||
}
|
||||
|
||||
|
||||
// rootSpec.exclude '**/persistence.xml'
|
||||
exclude '**/context.xml'
|
||||
exclude '**/context-*.xml'
|
||||
archiveFileName = "eapim-admin.war"
|
||||
duplicatesStrategy = DuplicatesStrategy.WARN
|
||||
}
|
||||
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs += ['-parameters', "-Xlint:unchecked", "-Xlint:deprecation"]
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs += ['-parameters', "-Xlint:unchecked", "-Xlint:deprecation"]
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
annotationProcessor "org.projectlombok:lombok:1.18.28",
|
||||
"org.projectlombok:lombok-mapstruct-binding:0.2.0",
|
||||
"org.mapstruct:mapstruct-processor:1.5.5.Final"
|
||||
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jpa",
|
||||
"jakarta.persistence:jakarta.persistence-api:2.2.3",
|
||||
"jakarta.annotation:jakarta.annotation-api:1.3.5"
|
||||
|
||||
|
||||
annotationProcessor "org.projectlombok:lombok:1.18.28", "org.projectlombok:lombok-mapstruct-binding:0.2.0", "org.mapstruct:mapstruct-processor:1.5.5.Final"
|
||||
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jpa", "jakarta.persistence:jakarta.persistence-api:2.2.3", "jakarta.annotation:jakarta.annotation-api:1.3.5"
|
||||
|
||||
implementation project(':elink-online-core')
|
||||
implementation project(':elink-online-transformer')
|
||||
implementation project(':elink-online-common')
|
||||
implementation project(':elink-online-emsclient')
|
||||
implementation project(':elink-portal-common')
|
||||
//implementation project(':kjb-safedb')
|
||||
implementation project(':elink-online-emsclient')
|
||||
implementation project(':elink-portal-common')
|
||||
//implementation project(':kjb-safedb')
|
||||
implementation project(":eapim-admin-djb")
|
||||
|
||||
/* Custom Libs (damo-manager.jar 는 Tomcat lib 가 런타임 제공 → compileOnly, WAR 미포함) */
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||
compileOnly files('libs/damo-manager.jar')
|
||||
|
||||
//implementation "org.dom4j:dom4j:2.1.3"
|
||||
//implementation "org.dom4j:dom4j:2.1.3"
|
||||
//implementation "com.rabbitmq:amqp-client:3.6.6"
|
||||
|
||||
implementation "commons-primitives:commons-primitives:1.0"
|
||||
@@ -116,10 +117,10 @@ dependencies {
|
||||
|
||||
// https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
|
||||
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
|
||||
|
||||
|
||||
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.1'
|
||||
//implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1'
|
||||
|
||||
implementation "javax.jms:javax.jms-api:2.0"
|
||||
implementation "org.snmp4j:snmp4j:1.10.1"
|
||||
@@ -186,32 +187,32 @@ dependencies {
|
||||
implementation 'ch.qos.logback:logback-classic:1.2.10'
|
||||
implementation 'ch.qos.logback:logback-access:1.2.10'
|
||||
implementation 'ch.qos.logback:logback-core:1.2.10'
|
||||
|
||||
implementation files("libs/eai-bap-client.jar")
|
||||
|
||||
compileOnly 'javax.xml.bind:jaxb-api:2.3.1'
|
||||
|
||||
//implementation 'org.postgresql:postgresql:42.2.23'
|
||||
|
||||
implementation 'backport-util-concurrent:backport-util-concurrent:3.0'
|
||||
|
||||
implementation ('software.amazon.awssdk:s3:2.20.142') {
|
||||
exclude group: 'org.slf4j', module: 'slf4j-api'
|
||||
exclude group: 'org.slf4j', module: 'logback-classic'
|
||||
}
|
||||
implementation 'software.amazon.awssdk:sso:2.20.142'
|
||||
implementation 'software.amazon.awssdk:sts:2.20.142'
|
||||
|
||||
implementation files("libs/eai-bap-client.jar")
|
||||
implementation group: 'commons-net', name: 'commons-net', version: '3.5'
|
||||
|
||||
compileOnly 'javax.xml.bind:jaxb-api:2.3.1'
|
||||
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없어 xercesImpl 가 NCDFE 를 일으킴.
|
||||
// xml-apis 1.4.01 에 그 클래스가 포함됨. 직접 의존성으로 묶어 WAR 에 패키징되도록 함.
|
||||
implementation 'xml-apis:xml-apis:1.4.01'
|
||||
|
||||
//implementation 'org.postgresql:postgresql:42.2.23'
|
||||
|
||||
implementation 'backport-util-concurrent:backport-util-concurrent:3.0'
|
||||
|
||||
implementation ('software.amazon.awssdk:s3:2.20.142') {
|
||||
exclude group: 'org.slf4j', module: 'slf4j-api'
|
||||
exclude group: 'org.slf4j', module: 'logback-classic'
|
||||
}
|
||||
implementation 'software.amazon.awssdk:sso:2.20.142'
|
||||
implementation 'software.amazon.awssdk:sts:2.20.142'
|
||||
|
||||
implementation group: 'commons-net', name: 'commons-net', version: '3.5'
|
||||
|
||||
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없어 xercesImpl 가 NCDFE 를 일으킴.
|
||||
// xml-apis 1.4.01 에 그 클래스가 포함됨. 직접 의존성으로 묶어 WAR 에 패키징되도록 함.
|
||||
implementation 'xml-apis:xml-apis:1.4.01'
|
||||
|
||||
testRuntimeOnly 'com.h2database:h2:2.1.214'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.15'
|
||||
testRuntimeOnly 'com.h2database:h2:2.1.214'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.15'
|
||||
}
|
||||
|
||||
test {
|
||||
@@ -219,9 +220,9 @@ test {
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude group: 'log4j', module: 'log4j'
|
||||
exclude group: 'org.apache.activemq'
|
||||
exclude group: 'org.codehaus.jackson'
|
||||
exclude group: 'log4j', module: 'log4j'
|
||||
exclude group: 'org.apache.activemq'
|
||||
exclude group: 'org.codehaus.jackson'
|
||||
resolutionStrategy {
|
||||
// 10 minute cache of dynamic version navigation
|
||||
cacheDynamicVersionsFor 10, 'minutes'
|
||||
@@ -245,25 +246,29 @@ task settingEclipseEncoding {
|
||||
}
|
||||
|
||||
task initDirs() {
|
||||
file(generatedJavaDir).mkdirs()
|
||||
file(generatedJavaDir).mkdirs()
|
||||
}
|
||||
|
||||
eclipse {
|
||||
wtp {
|
||||
component {
|
||||
contextPath = 'monitoring'
|
||||
}
|
||||
}
|
||||
synchronizationTasks settingEclipseEncoding, initDirs
|
||||
jdt {
|
||||
|
||||
}
|
||||
project {
|
||||
if (!natures.contains('org.eclipse.buildship.core.gradleprojectnature')) {
|
||||
natures.add('org.eclipse.buildship.core.gradleprojectnature')
|
||||
buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
|
||||
}
|
||||
}
|
||||
wtp {
|
||||
component {
|
||||
contextPath = 'monitoring'
|
||||
}
|
||||
}
|
||||
synchronizationTasks settingEclipseEncoding, initDirs
|
||||
jdt {
|
||||
apt {
|
||||
// generated 된 패스 경로를 .setting/org.eclipse.jdt.apt.core.prefs 의 org.eclipse.jdt.apt.genSrcDir에 적용한다.
|
||||
// project > properties > Java Compiler > Annoation Processing 화면에서 확인 가능하다.
|
||||
genSrcDir = file(generatedJavaDir)
|
||||
}
|
||||
}
|
||||
project {
|
||||
if (!natures.contains('org.eclipse.buildship.core.gradleprojectnature')) {
|
||||
natures.add('org.eclipse.buildship.core.gradleprojectnature')
|
||||
buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
||||
@@ -87,8 +87,8 @@ dependencies {
|
||||
implementation project(":eapim-admin-djb")
|
||||
|
||||
/* Custom Libs (damo-manager.jar 는 Tomcat lib 가 런타임 제공 → compileOnly, WAR 미포함) */
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||
compileOnly files('libs/damo-manager.jar')
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||
compileOnly files('libs/damo-manager.jar')
|
||||
|
||||
//implementation "org.dom4j:dom4j:2.1.3"
|
||||
//implementation "com.rabbitmq:amqp-client:3.6.6"
|
||||
|
||||
+3
-1
@@ -41,10 +41,12 @@ public class AuditLogInterceptor implements HandlerInterceptor {
|
||||
|
||||
if (isAuditableUsecase.isAuditable(userId, command, logType, systemCode)) {
|
||||
String parameters = getParametersAsString(request);
|
||||
String message = request.getParameter("auditReason");
|
||||
log
|
||||
.debug("audit log save : logType={}, cmd={}, user={}, parameters={}", logType, command,
|
||||
userId, parameters);
|
||||
saveAuditLogUseCase.log(systemCode, logType, command, remoteAddress, userId, parameters);
|
||||
saveAuditLogUseCase
|
||||
.log(systemCode, logType, command, remoteAddress, userId, parameters, message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ public class AuditLog {
|
||||
|
||||
private String logTypeText;
|
||||
|
||||
private String message;
|
||||
|
||||
private String parameters;
|
||||
|
||||
private LocalDateTime lastModifiedDate;
|
||||
|
||||
@@ -3,6 +3,6 @@ package com.eactive.eai.rms.common.acl.audit.port.in;
|
||||
public interface SaveAuditLogUseCase {
|
||||
|
||||
public void log(String systemCode, String logType, String command, String remoteAddress, String userId,
|
||||
String parameters);
|
||||
String parameters, String message);
|
||||
|
||||
}
|
||||
|
||||
@@ -59,15 +59,16 @@ class AuditLogService
|
||||
|
||||
@Override
|
||||
public void log(String systemCode, String logType, String command, String remoteAddress, String userId,
|
||||
String parameters) {
|
||||
String parameters, String message) {
|
||||
|
||||
AuditLog auditLog = new AuditLog(systemCode, logType, command, userId, remoteAddress);
|
||||
String auditPointKey = AuditPoint.generateKey(logType, systemCode, command);
|
||||
AuditPoint auditPoint = auditPoints.get(auditPointKey);
|
||||
|
||||
|
||||
auditLog.setLogTypeText(auditPoint.getLogTypeText());
|
||||
auditLog.setLastModifiedDate(LocalDateTime.now());
|
||||
auditLog.setParameters(parameters);
|
||||
auditLog.setMessage(message);
|
||||
|
||||
saveAuditLogPort.save(auditLog);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.eactive.eai.rms.common.acl.sitemap;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import com.eactive.eai.rms.common.base.BaseAnnotationController;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class SitemapController extends BaseAnnotationController {
|
||||
|
||||
private final SitemapService sitemapService;
|
||||
|
||||
@GetMapping(value = "/common/acl/sitemap/sitemapMan.view")
|
||||
public String view(@RequestParam(value = "serviceType", required = false) String serviceType, Model model) {
|
||||
model.addAttribute("sitemapTree", sitemapService.getSitemapTree(serviceType));
|
||||
return "/common/acl/sitemap/sitemapMan";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.eactive.eai.rms.common.acl.sitemap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.eactive.eai.rms.common.acl.sitemap.ui.SitemapNode;
|
||||
|
||||
public interface SitemapService {
|
||||
|
||||
public List<SitemapNode> getSitemapTree(String serviceType);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.eactive.eai.rms.common.acl.sitemap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.eai.rms.common.acl.sitemap.ui.SitemapNode;
|
||||
import com.eactive.eai.rms.common.base.BaseService;
|
||||
import com.eactive.eai.rms.data.entity.man.menu.Menu;
|
||||
import com.eactive.eai.rms.data.entity.man.menu.MenuService;
|
||||
import com.eactive.eai.rms.data.entity.man.role.RoleMenuAuthService;
|
||||
|
||||
@Service("sitemapService")
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
public class SitemapServiceImpl extends BaseService implements SitemapService {
|
||||
|
||||
@Autowired
|
||||
private MenuService menuService;
|
||||
|
||||
@Autowired
|
||||
private RoleMenuAuthService roleMenuAuthService;
|
||||
|
||||
public List<SitemapNode> getSitemapTree(String serviceType) {
|
||||
Menu root = menuService.getById("ROOT_DEV");
|
||||
Menu langRoot = findLangRoot(root);
|
||||
List<SitemapNode> tree = new ArrayList<>();
|
||||
if (langRoot == null) {
|
||||
return tree;
|
||||
}
|
||||
|
||||
Set<String> serviceTypeMenuIds = StringUtils.isBlank(serviceType)
|
||||
? null
|
||||
: new HashSet<>(roleMenuAuthService.findMenuIdsByServiceType(serviceType));
|
||||
|
||||
for (Menu menu : sortedChildren(langRoot)) {
|
||||
if (isVisible(menu) && matchesServiceType(menu, serviceTypeMenuIds)) {
|
||||
tree.add(buildNode(menu, serviceTypeMenuIds));
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
// 엔티티의 childMenus는 menuId 기준으로 정렬되어 있어, 실제 메뉴(운영화면 GNB)와 동일하게 sortOrder 기준으로 재정렬한다.
|
||||
private List<Menu> sortedChildren(Menu menu) {
|
||||
List<Menu> children = new ArrayList<>(menu.getChildMenus());
|
||||
children.sort(Comparator.comparing(Menu::getSortOrder, Comparator.nullsLast(Comparator.naturalOrder())));
|
||||
return children;
|
||||
}
|
||||
|
||||
// MenuService.getMenuList()의 GNB 렌더링과 동일하게 "ROOT"(한글)/"ROOT_EN"(영문) 이름의 언어별 루트를 찾는다.
|
||||
private Menu findLangRoot(Menu root) {
|
||||
String lang = LocaleContextHolder.getLocale().getLanguage();
|
||||
String langRootName = (StringUtils.isBlank(lang) || "ko".equalsIgnoreCase(lang))
|
||||
? "ROOT"
|
||||
: "ROOT_" + lang.toUpperCase();
|
||||
|
||||
return root.getChildMenus().stream()
|
||||
.filter(m -> langRootName.equalsIgnoreCase(m.getMenuName()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private SitemapNode buildNode(Menu menu, Set<String> serviceTypeMenuIds) {
|
||||
SitemapNode node = new SitemapNode(menu);
|
||||
for (Menu child : sortedChildren(menu)) {
|
||||
if (isVisible(child) && matchesServiceType(child, serviceTypeMenuIds)) {
|
||||
node.getChildren().add(buildNode(child, serviceTypeMenuIds));
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private boolean isVisible(Menu menu) {
|
||||
return "Y".equalsIgnoreCase(menu.getDisplayYn()) && "Y".equalsIgnoreCase(menu.getUseYn());
|
||||
}
|
||||
|
||||
// serviceType 필터가 없으면 통과. 있으면 자신이나 하위메뉴 중 하나라도 해당 serviceType 권한이 있어야 통과(상위메뉴 누락 방지).
|
||||
private boolean matchesServiceType(Menu menu, Set<String> serviceTypeMenuIds) {
|
||||
if (serviceTypeMenuIds == null) {
|
||||
return true;
|
||||
}
|
||||
if (serviceTypeMenuIds.contains(menu.getMenuId())) {
|
||||
return true;
|
||||
}
|
||||
return menu.getChildMenus().stream().anyMatch(child -> matchesServiceType(child, serviceTypeMenuIds));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.eactive.eai.rms.common.acl.sitemap.ui;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.eactive.eai.rms.data.entity.man.menu.Menu;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SitemapNode {
|
||||
|
||||
private String menuId;
|
||||
|
||||
private String menuName;
|
||||
|
||||
private String menuUrl;
|
||||
|
||||
private List<SitemapNode> children = new ArrayList<>();
|
||||
|
||||
public SitemapNode(Menu menu) {
|
||||
this.menuId = menu.getMenuId();
|
||||
this.menuName = menu.getMenuName();
|
||||
this.menuUrl = menu.getMenuUrl();
|
||||
}
|
||||
|
||||
public SitemapNode() {
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,8 @@ import lombok.RequiredArgsConstructor;
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
@RequiredArgsConstructor
|
||||
public class UserManService extends BaseService {
|
||||
|
||||
|
||||
|
||||
private final UserInfoService userInfoService;
|
||||
private final UserRoleService userRoleService;
|
||||
@@ -98,9 +100,11 @@ public class UserManService extends BaseService {
|
||||
|
||||
UserInfo userInfo = userUIMapper.toEntity(userUI);
|
||||
userInfo.setRoleidnfiname(CommonConstants.DEPT_DEVELOPER);
|
||||
|
||||
String subfix = monitoringContext.getStringProperty(MonitoringContext.RMS_PASSWORD_INIT_SUBFIX, "@!");
|
||||
|
||||
try {
|
||||
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256,userUI.getUserId()));
|
||||
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256,userUI.getUserId()+subfix));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error encrypting password", e);
|
||||
}
|
||||
@@ -140,8 +144,9 @@ public class UserManService extends BaseService {
|
||||
}
|
||||
|
||||
public void updatePassword(UserUI userUI) throws Exception {
|
||||
String subfix = monitoringContext.getStringProperty(MonitoringContext.RMS_PASSWORD_INIT_SUBFIX, "@!");
|
||||
UserInfo userInfo = userInfoService.getById(userUI.getUserId());
|
||||
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256, userUI.getUserId()));
|
||||
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256, userUI.getUserId()+subfix));
|
||||
userUIMapper.updateToEntity(userUI, userInfo);
|
||||
userInfoService.save(userInfo);
|
||||
}
|
||||
|
||||
@@ -258,9 +258,15 @@ public interface MonitoringContext {
|
||||
// 자동 로그아웃 타임아웃 (단위: 분, 기본값: 10)
|
||||
public static final String RMS_AUTO_LOGOUT_TIMEOUT = "rms.auto.logout.timeout";
|
||||
|
||||
// 웹훅 재전송 설정
|
||||
public static final String API_WEBHOOK_RETRY_COUNT = "api.webhook.retry_count";
|
||||
public static final String API_WEBHOOK_RETRY_TIME = "api.webhook.retry_time";
|
||||
// 웹훅
|
||||
public static final String RMS_WEBHOOK_REVERSE_PROXY_URL = "rms.webhook.reverse_proxy.url";
|
||||
public static final String RMS_WEBHOOK_RETRY_COUNT = "rms.webhook.retry_count";
|
||||
public static final String RMS_WEBHOOK_RETRY_TIME = "rms.webhook.retry_time";
|
||||
|
||||
//비밀번호 초기화 접미사
|
||||
public static final String RMS_PASSWORD_INIT_SUBFIX = "rms.password.init.subfix";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -45,4 +45,9 @@ public class LoginResponseDto {
|
||||
* 에러 메시지 (로그인 실패 시)
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 초기 비밀번호 변경 필요 여부
|
||||
*/
|
||||
private boolean changePassword;
|
||||
}
|
||||
|
||||
@@ -191,6 +191,19 @@ public class MainController implements InterceptorSkipController {
|
||||
.redirectUrl(redirectUrl)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
// 2.1 초기 비밀번호인 경우, 비밀번호 변경해야 함
|
||||
String subfix = monitoringContext.getStringProperty(MonitoringContext.RMS_PASSWORD_INIT_SUBFIX, "@!");
|
||||
if (userInfo.getPassword().equals(DamoManager.getInstance().hash(DamoManager.SHA256, userInfo.getUserid()+subfix))
|
||||
|| userInfo.getPassword().equals(DamoManager.getInstance().hash(DamoManager.SHA256, userInfo.getUserid()))) {
|
||||
return LoginResponseDto.builder()
|
||||
.success(false)
|
||||
.changePassword(true)
|
||||
.errorMessage("비밀번호 변경후 사용가능합니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
// 3. SMS 인증 필요 여부 체크
|
||||
SmsAuthService smsAuthService = getSmsAuthService();
|
||||
@@ -758,6 +771,9 @@ public class MainController implements InterceptorSkipController {
|
||||
String changePassword, String confirmPassword,
|
||||
String resetUserId, String resetPassword) {
|
||||
HttpSession session = request.getSession();
|
||||
// AJAX 로그인 인증 후 초기비밀번호 변경 경로에서 세션에 부분 로그인 상태가 남아
|
||||
// root emergency.jsp가 main.do로 잘못 리다이렉트하는 것을 방지
|
||||
session.removeAttribute(CommonConstants.LOGIN);
|
||||
|
||||
try {
|
||||
|
||||
@@ -815,6 +831,8 @@ public class MainController implements InterceptorSkipController {
|
||||
.getString("login.pwdchanged"));
|
||||
session.setAttribute(RESULT_MSG, localeMessage
|
||||
.getString("login.pwdchanged"));
|
||||
logger.debug("\n\n\n\n===================" + localeMessage
|
||||
.getString("login.pwdchanged"));
|
||||
return REDIRECT_LOGIN_URL;
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -51,4 +51,17 @@ public class RoleMenuAuthService extends AbstractEMSDataSerivce<RoleMenuAuth, Ro
|
||||
.fetch();
|
||||
}
|
||||
|
||||
public List<String> findMenuIdsByServiceType(String serviceType) {
|
||||
|
||||
QRoleMenuAuth qRoleMenuAuth = QRoleMenuAuth.roleMenuAuth;
|
||||
|
||||
return getJPAQueryFactory()
|
||||
.select(qRoleMenuAuth.id.menuId)
|
||||
.distinct()
|
||||
.from(qRoleMenuAuth)
|
||||
.where(qRoleMenuAuth.id.serviceType.eq(serviceType))
|
||||
.setHint(QueryHints.CACHEABLE, true)
|
||||
.fetch();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.eactive.eai.rms.data.entity.onl.apim.apigroup;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroupApi;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroupApiId;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroup;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
|
||||
import com.eactive.eai.data.jpa.AbstractDataService;
|
||||
@@ -76,37 +75,4 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
|
||||
.where(qApiGroup.id.in(apiGroupIds))
|
||||
.fetch();
|
||||
}
|
||||
|
||||
/** API 를 지정한 그룹 목록에만 소속시킨다(기존 소속 제거 후 재설정). */
|
||||
public void setApiGroupsForApi(String apiId, List<String> groupIds) {
|
||||
deleteApiGroupApiByApiId(apiId);
|
||||
if (groupIds == null) {
|
||||
return;
|
||||
}
|
||||
for (String gid : groupIds) {
|
||||
if (StringUtils.isBlank(gid)) {
|
||||
continue;
|
||||
}
|
||||
ApiGroup group = repository.findById(gid).orElse(null);
|
||||
if (group == null) {
|
||||
continue;
|
||||
}
|
||||
Hibernate.initialize(group.getApiGroupApiList());
|
||||
List<ApiGroupApi> list = group.getApiGroupApiList();
|
||||
boolean exists = false;
|
||||
for (ApiGroupApi a : list) {
|
||||
if (a.getId() != null && apiId.equals(a.getId().getApiId())) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
ApiGroupApi a = new ApiGroupApi();
|
||||
a.setId(new ApiGroupApiId(gid, apiId));
|
||||
a.setDisplayOrder(list.size() + 1);
|
||||
list.add(a);
|
||||
repository.save(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,12 @@ public class WebhookSendLog {
|
||||
)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "TARGET_URL", nullable = false, length = 500)
|
||||
@Column(name = "TARGET_URL", length = 500)
|
||||
private String targetUrl;
|
||||
|
||||
@Column(name = "PROXY_URL", length = 500)
|
||||
private String proxyUrl;
|
||||
|
||||
@Column(name = "EVENT_TYPE", length = 100)
|
||||
private String eventType;
|
||||
|
||||
@@ -66,6 +69,9 @@ public class WebhookSendLog {
|
||||
@Column(name = "SENT_AT")
|
||||
private LocalDateTime sentAt;
|
||||
|
||||
@Column(name = "SEND_BY", length = 100)
|
||||
private String sendBy;
|
||||
|
||||
/* Boolean 편의 메서드 */
|
||||
public void setSuccess(Boolean success) {
|
||||
this.success = (success != null && success) ? "Y" : "N";
|
||||
|
||||
@@ -126,6 +126,15 @@ public class EAILogRepositoryImpl implements EAILogRepository {
|
||||
query.where(qeaiLog.eaisvcname.containsIgnoreCase(eaiLogSearch.getSearchEaiSvcName()));
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(eaiLogSearch.getSearchEaiSvcDesc())) {
|
||||
QEAIMessageEntity qeaiMessageEntity = QEAIMessageEntity.eAIMessageEntity;
|
||||
JPAQuery<String> subQuery = new JPAQueryFactory(em)
|
||||
.select(qeaiMessageEntity.eaisvcname)
|
||||
.from(qeaiMessageEntity)
|
||||
.where(qeaiMessageEntity.eaisvcdesc.containsIgnoreCase(eaiLogSearch.getSearchEaiSvcDesc()));
|
||||
query.where(qeaiLog.eaisvcname.in(subQuery));
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(eaiLogSearch.getSearchKeyMgtMsgCtnt())) {
|
||||
query.where(qeaiLog.keymgtmsgctnt.contains(eaiLogSearch.getSearchKeyMgtMsgCtnt()));
|
||||
}
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.controller;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.web.SortDefault;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
import com.eactive.eai.rms.common.base.BaseAnnotationController;
|
||||
import com.eactive.eai.rms.common.combo.ComboService;
|
||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendLogUI;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendLogUISearch;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookSendLogManService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 웹훅 발송 로그(PTL_WEBHOOK_SEND_LOG) 조회 화면 컨트롤러.
|
||||
*
|
||||
* <p>조회 전용 (등록/수정/삭제 없음)</p>
|
||||
*/
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class WebhookSendLogManController extends BaseAnnotationController {
|
||||
|
||||
private final WebhookSendLogManService webhookSendLogManService;
|
||||
private final ComboService comboService;
|
||||
|
||||
@GetMapping(value = "/onl/apim/webhook/webhookSendLogMan.view")
|
||||
public void view() {
|
||||
// 목록 view
|
||||
}
|
||||
|
||||
@GetMapping(value = "/onl/apim/webhook/webhookSendLogMan.view", params = "cmd=DETAIL")
|
||||
public String detailView() {
|
||||
return "/onl/apim/webhook/webhookSendLogManDetail";
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/webhook/webhookSendLogMan.json", params = "cmd=LIST")
|
||||
public ResponseEntity<GridResponse<WebhookSendLogUI>> selectList(
|
||||
@SortDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
|
||||
WebhookSendLogUISearch search) {
|
||||
Page<WebhookSendLogUI> page = webhookSendLogManService.selectList(pageable, search);
|
||||
return ResponseEntity.ok(new GridResponse<>(page));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/webhook/webhookSendLogMan.json", params = "cmd=LIST_INIT_COMBO")
|
||||
public ResponseEntity<Map<String, Object>> initCombo() {
|
||||
Map<String, Object> resultMap = new HashMap<>();
|
||||
resultMap.put("eventTypeList", comboService.getMonitoringCodeSortedBySeq(WebhookSendLogManService.CODE_GROUP_EVENT_TYPE));
|
||||
return ResponseEntity.ok(resultMap);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/webhook/webhookSendLogMan.json", params = "cmd=DETAIL")
|
||||
public ResponseEntity<WebhookSendLogUI> selectDetail(Long id) {
|
||||
return ResponseEntity.ok(webhookSendLogManService.selectDetail(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 웹훅 발송 로그(PTL_WEBHOOK_SEND_LOG) 화면 응답 DTO.
|
||||
*
|
||||
* <p>목록에서는 일부 항목만 사용하고, 상세에서는 전체 항목을 사용한다.
|
||||
* 날짜는 yyyyMMdd HH:mm:ss 형식 문자열로 제공한다.</p>
|
||||
*/
|
||||
@Data
|
||||
public class WebhookSendLogUI {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String orgId;
|
||||
private String orgName;
|
||||
|
||||
private String eventType;
|
||||
/** 모니터링 공통코드(EVENT_TYPE) 코드명. 상세 조회에서만 채워진다(목록은 화면에서 콤보로 변환). */
|
||||
private String eventTypeName;
|
||||
private String targetUrl;
|
||||
private String proxyUrl;
|
||||
|
||||
private String payload;
|
||||
private String signature;
|
||||
|
||||
private Integer statusCode;
|
||||
private String responseBody;
|
||||
|
||||
private Boolean success;
|
||||
private String sendBy;
|
||||
private String errorMessage;
|
||||
private Integer retryCount;
|
||||
|
||||
/* 등록/발송 시간 — yyyyMMdd HH:mm:ss */
|
||||
private String createdAt;
|
||||
private String sentAt;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WebhookSendLogUISearch {
|
||||
|
||||
/* 기관명 (ptl_org.org_name, LIKE 검색) */
|
||||
private String searchOrgName;
|
||||
|
||||
/* 이벤트 유형 (모니터링 공통코드 EVENT_TYPE, 콤보 선택 값과 정확히 일치) */
|
||||
private String searchEventType;
|
||||
|
||||
/* 수신 URL (LIKE 검색) */
|
||||
private String searchTargetUrl;
|
||||
|
||||
/* 성공여부 (Y/N, 공백=전체) */
|
||||
private String searchSuccess;
|
||||
|
||||
/* 발송일시 기간 검색 (yyyyMMdd, 8자리 — userLoginHistoryMan.jsp 기간검색 스타일) */
|
||||
private String searchStartDate;
|
||||
private String searchEndDate;
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import lombok.Setter;
|
||||
public class WebhookSendRequest {
|
||||
private String orgId;
|
||||
private String targetUrl;
|
||||
private String proxyUrl;
|
||||
private String eventType;
|
||||
private String secret;
|
||||
private String reverseProxyPath;
|
||||
private Object data;
|
||||
}
|
||||
+2
-2
@@ -4,17 +4,17 @@ package com.eactive.eai.rms.ext.djb.webhook.repository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
import com.eactive.eai.rms.data.EMSDataSource;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
|
||||
|
||||
@Repository
|
||||
@EMSDataSource
|
||||
public interface WebhookSendLogRepository extends JpaRepository<WebhookSendLog, Long> {
|
||||
public interface WebhookSendLogRepository extends BaseRepository<WebhookSendLog, Long> {
|
||||
|
||||
// 오라클 CHAR(1) Y/N 기준 조회
|
||||
@Query("SELECT l FROM WebhookSendLog l " +
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.PersistenceContext;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.apim.portal.monitoringcode.entity.MonitoringCode;
|
||||
import com.eactive.apim.portal.monitoringcode.entity.MonitoringCodeId;
|
||||
import com.eactive.apim.portal.portalorg.entity.QPortalOrg;
|
||||
import com.eactive.eai.rms.common.base.BaseService;
|
||||
import com.eactive.eai.rms.common.util.StringUtils;
|
||||
import com.eactive.eai.rms.data.entity.man.monitoringCode.repository.MonitoringCodeRepository;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookSendLog;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.webhook.WebhookSendLog;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendLogUI;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendLogUISearch;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository;
|
||||
import com.eactive.eai.rms.onl.common.exception.BizException;
|
||||
import com.querydsl.core.BooleanBuilder;
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 웹훅 발송 로그(PTL_WEBHOOK_SEND_LOG) 조회 화면 서비스.
|
||||
*
|
||||
* <p>조회 전용. orgId 는 엔티티 연관관계가 아닌 단순 컬럼이므로,
|
||||
* 기관명은 QueryDSL 로 ptl_org 를 별도 조회하여 매핑한다.</p>
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
public class WebhookSendLogManService extends BaseService {
|
||||
|
||||
/** 모니터링 공통코드(tseairm28) 상의 웹훅 이벤트유형 코드 그룹. */
|
||||
public static final String CODE_GROUP_EVENT_TYPE = "EVENT_TYPE";
|
||||
|
||||
private static final DateTimeFormatter FMT_DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final DateTimeFormatter FMT_YYYYMMDD = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
private final WebhookSendLogRepository webhookSendLogRepository;
|
||||
private final MonitoringCodeRepository monitoringCodeRepository;
|
||||
|
||||
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
|
||||
private EntityManager entityManager;
|
||||
|
||||
public Page<WebhookSendLogUI> selectList(Pageable pageable, WebhookSendLogUISearch search) {
|
||||
QWebhookSendLog log = QWebhookSendLog.webhookSendLog;
|
||||
BooleanBuilder predicate = new BooleanBuilder();
|
||||
|
||||
if (StringUtils.isNotBlank(search.getSearchOrgName())) {
|
||||
List<String> orgIds = findOrgIdsByName(search.getSearchOrgName());
|
||||
if (orgIds.isEmpty()) {
|
||||
// 일치하는 기관이 없으면 쿼리를 실행할 필요 없이 빈 결과를 바로 반환한다.
|
||||
// (QueryDSL의 where false 는 이 프로젝트의 Hibernate HQL 파서에서 지원하지 않음)
|
||||
return Page.empty(pageable);
|
||||
}
|
||||
predicate.and(log.orgId.in(orgIds));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchEventType())) {
|
||||
predicate.and(log.eventType.eq(search.getSearchEventType()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchTargetUrl())) {
|
||||
predicate.and(log.targetUrl.containsIgnoreCase(search.getSearchTargetUrl()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchSuccess())) {
|
||||
predicate.and(log.success.eq(search.getSearchSuccess()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchStartDate())) {
|
||||
predicate.and(log.sentAt.goe(parseYYYYMMDD(search.getSearchStartDate()).atStartOfDay()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search.getSearchEndDate())) {
|
||||
predicate.and(log.sentAt.loe(parseYYYYMMDD(search.getSearchEndDate()).atTime(23, 59, 59)));
|
||||
}
|
||||
|
||||
Page<WebhookSendLog> page = webhookSendLogRepository.findAll(predicate, pageable);
|
||||
|
||||
Map<String, String> orgNameMap = findOrgNames(page.getContent());
|
||||
// 목록의 이벤트유형 코드명은 화면에서 콤보(LIST_INIT_COMBO) 기준으로 변환하므로 서버에서는 조회하지 않는다(N+1 방지).
|
||||
return page.map(e -> convertToUI(e, orgNameMap.get(e.getOrgId()), null));
|
||||
}
|
||||
|
||||
public WebhookSendLogUI selectDetail(Long id) {
|
||||
WebhookSendLog entity = webhookSendLogRepository.findById(id)
|
||||
.orElseThrow(() -> new BizException("존재하지 않는 웹훅 발송 로그입니다."));
|
||||
|
||||
Map<String, String> orgNameMap = findOrgNames(Collections.singletonList(entity));
|
||||
String eventTypeName = resolveEventTypeName(entity.getEventType());
|
||||
return convertToUI(entity, orgNameMap.get(entity.getOrgId()), eventTypeName);
|
||||
}
|
||||
|
||||
private static LocalDate parseYYYYMMDD(String yyyyMMdd) {
|
||||
return LocalDate.parse(yyyyMMdd, FMT_YYYYMMDD);
|
||||
}
|
||||
|
||||
/** 모니터링 공통코드(EVENT_TYPE)에서 코드명을 조회한다. 코드가 없으면 원본 코드를 그대로 반환한다. */
|
||||
private String resolveEventTypeName(String eventType) {
|
||||
if (StringUtils.isBlank(eventType)) {
|
||||
return eventType;
|
||||
}
|
||||
return monitoringCodeRepository.findById(new MonitoringCodeId(CODE_GROUP_EVENT_TYPE, eventType))
|
||||
.map(MonitoringCode::getCodeName)
|
||||
.orElse(eventType);
|
||||
}
|
||||
|
||||
/** 기관명 LIKE 검색으로 매칭되는 orgId 목록. */
|
||||
private List<String> findOrgIdsByName(String orgName) {
|
||||
QPortalOrg org = QPortalOrg.portalOrg;
|
||||
return new JPAQueryFactory(entityManager)
|
||||
.select(org.id)
|
||||
.from(org)
|
||||
.where(org.orgName.containsIgnoreCase(orgName))
|
||||
.fetch();
|
||||
}
|
||||
|
||||
/** 목록/상세에 필요한 orgId → orgName 매핑 (일괄 조회로 N+1 방지). */
|
||||
private Map<String, String> findOrgNames(List<WebhookSendLog> logs) {
|
||||
Set<String> orgIds = logs.stream()
|
||||
.map(WebhookSendLog::getOrgId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.collect(Collectors.toSet());
|
||||
if (orgIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
QPortalOrg org = QPortalOrg.portalOrg;
|
||||
return new JPAQueryFactory(entityManager)
|
||||
.select(org.id, org.orgName)
|
||||
.from(org)
|
||||
.where(org.id.in(orgIds))
|
||||
.fetch()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(t -> t.get(org.id), t -> t.get(org.orgName)));
|
||||
}
|
||||
|
||||
private WebhookSendLogUI convertToUI(WebhookSendLog e, String orgName, String eventTypeName) {
|
||||
WebhookSendLogUI ui = new WebhookSendLogUI();
|
||||
ui.setId(e.getId());
|
||||
ui.setOrgId(e.getOrgId());
|
||||
ui.setOrgName(orgName);
|
||||
ui.setEventType(e.getEventType());
|
||||
ui.setEventTypeName(eventTypeName);
|
||||
ui.setTargetUrl(e.getTargetUrl());
|
||||
ui.setProxyUrl(e.getProxyUrl());
|
||||
ui.setPayload(e.getPayload());
|
||||
ui.setSignature(e.getSignature());
|
||||
ui.setStatusCode(e.getStatusCode());
|
||||
ui.setResponseBody(e.getResponseBody());
|
||||
ui.setSuccess(e.getSuccess());
|
||||
ui.setSendBy(e.getSendBy());
|
||||
ui.setErrorMessage(e.getErrorMessage());
|
||||
ui.setRetryCount(e.getRetryCount());
|
||||
ui.setCreatedAt(fmt(e.getCreatedAt(), FMT_DATETIME));
|
||||
ui.setSentAt(fmt(e.getSentAt(), FMT_DATETIME));
|
||||
return ui;
|
||||
}
|
||||
|
||||
private static String fmt(LocalDateTime t, DateTimeFormatter f) {
|
||||
return t == null ? "" : t.format(f);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,9 @@ import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.HttpServerErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.eactive.apim.portal.portalorg.entity.QPortalOrg;
|
||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||
import com.eactive.eai.rms.common.util.ContainerUtil;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqApi;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReqEvent;
|
||||
@@ -60,17 +62,21 @@ public class WebhookService {
|
||||
QWebhookReq req = QWebhookReq.webhookReq;
|
||||
QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent;
|
||||
QWebhookReqApi api = QWebhookReqApi.webhookReqApi;
|
||||
QPortalOrg org = QPortalOrg.portalOrg;
|
||||
|
||||
String reverseProxyUrl = monitoringContext.getProperty(MonitoringContext.RMS_WEBHOOK_REVERSE_PROXY_URL);
|
||||
|
||||
List<Tuple> tuples = new JPAQueryFactory(entityManager)
|
||||
.select(req.orgId, req.targetUrl, req.secret, api.id.apiId)
|
||||
.select(req.orgId, req.targetUrl, req.secret, org.reverseProxyPath, api.id.apiId)
|
||||
.from(req)
|
||||
.join(evt).on(req.id.eq(evt.id.webhookReqId))
|
||||
.join(api).on(req.id.eq(api.id.webhookReqId))
|
||||
.join(org).on(req.orgId.eq(org.id))
|
||||
.where(evt.id.eventType.eq(event)
|
||||
.and(api.id.apiId.in(apiIds)))
|
||||
.fetch();
|
||||
|
||||
// orgId + targetUrl + secret 기준으로 그룹핑, apiId를 data(List)로 집계
|
||||
// orgId + targetUrl + secret 기준으로 그룹핑, apiIds를 data(List)로 집계
|
||||
Map<String, WebhookSendRequest> resultMap = new LinkedHashMap<>();
|
||||
for (Tuple t : tuples) {
|
||||
String key = t.get(req.orgId) + "|" + t.get(req.targetUrl);
|
||||
@@ -78,6 +84,7 @@ public class WebhookService {
|
||||
WebhookSendRequest wr = new WebhookSendRequest();
|
||||
wr.setOrgId(t.get(req.orgId));
|
||||
wr.setTargetUrl(t.get(req.targetUrl));
|
||||
wr.setProxyUrl(this.getProxyUrl(reverseProxyUrl, t.get(org.reverseProxyPath), t.get(req.targetUrl)));
|
||||
wr.setSecret(t.get(req.secret));
|
||||
wr.setEventType(event);
|
||||
wr.setData(new ArrayList<String>());
|
||||
@@ -88,6 +95,46 @@ public class WebhookService {
|
||||
|
||||
return new ArrayList<>(resultMap.values());
|
||||
}
|
||||
|
||||
//reverse proxy 서버로 발송하기 위한 url 만든다
|
||||
private String getProxyUrl(String proxyUrl, String path, String targetUrl) {
|
||||
String base = stripTrailingSlash(proxyUrl);
|
||||
String normalizedPath = stripSlashes(path);
|
||||
// scheme://host[:port] 부분만 제거하고 이후 path/query/fragment는 그대로 유지
|
||||
String targetPath = targetUrl.replaceFirst("^[a-zA-Z][a-zA-Z0-9+.-]*://[^/]+", "");
|
||||
|
||||
StringBuilder url = new StringBuilder(base);
|
||||
if (!normalizedPath.isEmpty()) {
|
||||
url.append('/').append(normalizedPath);
|
||||
}
|
||||
url.append(targetPath);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private String stripTrailingSlash(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
int end = value.length();
|
||||
while (end > 0 && value.charAt(end - 1) == '/') {
|
||||
end--;
|
||||
}
|
||||
return value.substring(0, end);
|
||||
}
|
||||
|
||||
private String stripSlashes(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
String result = value.trim();
|
||||
while (result.startsWith("/")) {
|
||||
result = result.substring(1);
|
||||
}
|
||||
while (result.endsWith("/")) {
|
||||
result = result.substring(0, result.length() - 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 웹훅 발송 메인 메서드
|
||||
@@ -96,6 +143,7 @@ public class WebhookService {
|
||||
public void send(WebhookSendRequest req) {
|
||||
|
||||
String targetUrl = req.getTargetUrl();
|
||||
String proxyUrl = req.getProxyUrl();
|
||||
String secret = req.getSecret();
|
||||
String eventType = req.getEventType();
|
||||
Object data = req.getData();
|
||||
@@ -103,7 +151,9 @@ public class WebhookService {
|
||||
WebhookSendLog sendLog = new WebhookSendLog();
|
||||
sendLog.setOrgId(req.getOrgId());
|
||||
sendLog.setTargetUrl(targetUrl);
|
||||
sendLog.setProxyUrl(proxyUrl);
|
||||
sendLog.setEventType(eventType);
|
||||
sendLog.setSendBy(ContainerUtil.getInstanceName());
|
||||
|
||||
try {
|
||||
// 1. Payload 생성
|
||||
@@ -123,11 +173,11 @@ public class WebhookService {
|
||||
headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp()));
|
||||
|
||||
// 4. 발송 (재시도 포함)
|
||||
int retryCount = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_COUNT, DEFAULT_RETRY_COUNT);
|
||||
int retryTime = monitoringContext.getIntProperty(MonitoringContext.API_WEBHOOK_RETRY_TIME, DEFAULT_RETRY_TIME);
|
||||
int retryCount = monitoringContext.getIntProperty(MonitoringContext.RMS_WEBHOOK_RETRY_COUNT, DEFAULT_RETRY_COUNT);
|
||||
int retryTime = monitoringContext.getIntProperty(MonitoringContext.RMS_WEBHOOK_RETRY_TIME, DEFAULT_RETRY_TIME);
|
||||
sendLog.setSentAt(LocalDateTime.now());
|
||||
HttpEntity<String> httpRequest = new HttpEntity<>(payloadJson, headers);
|
||||
ResponseEntity<String> response = sendWithRetry(targetUrl, httpRequest, sendLog, retryCount, retryTime);
|
||||
ResponseEntity<String> response = sendWithRetry(proxyUrl, httpRequest, sendLog, retryCount, retryTime);
|
||||
|
||||
// 5. 성공 로그
|
||||
sendLog.setStatusCode(response.getStatusCode().value());
|
||||
@@ -135,7 +185,7 @@ public class WebhookService {
|
||||
sendLog.setSuccess(response.getStatusCode().is2xxSuccessful());
|
||||
|
||||
log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}, retryCount: {}",
|
||||
eventType, targetUrl, response.getStatusCode(), sendLog.getRetryCount());
|
||||
eventType, proxyUrl, response.getStatusCode(), sendLog.getRetryCount());
|
||||
|
||||
} catch (HttpClientErrorException | HttpServerErrorException e) {
|
||||
sendLog.setStatusCode(e.getStatusCode().value());
|
||||
@@ -143,13 +193,13 @@ public class WebhookService {
|
||||
sendLog.setSuccess(false);
|
||||
sendLog.setErrorMessage(e.getMessage());
|
||||
log.error("[Webhook] HTTP 오류 - eventType: {}, url: {}, status: {}",
|
||||
eventType, targetUrl, e.getStatusCode());
|
||||
eventType, proxyUrl, e.getStatusCode());
|
||||
|
||||
} catch (Exception e) {
|
||||
sendLog.setSuccess(false);
|
||||
sendLog.setErrorMessage(e.getMessage());
|
||||
log.error("[Webhook] 발송 실패 - eventType: {}, url: {}, error: {}",
|
||||
eventType, targetUrl, e.getMessage());
|
||||
eventType, proxyUrl, e.getMessage());
|
||||
}
|
||||
|
||||
sendLogRepository.save(sendLog);
|
||||
@@ -161,24 +211,25 @@ public class WebhookService {
|
||||
/**
|
||||
* 재시도 포함 HTTP 발송
|
||||
* - 4xx: 클라이언트 오류이므로 즉시 throw (재시도 불필요)
|
||||
* - 5xx / 네트워크 오류: 최대 RETRY_COUNT회까지 exponential backoff 재시도 (1s → 2s → 4s)
|
||||
* - 5xx / 네트워크 오류: 최대 RETRY_COUNT회까지 선형 증가 backoff 재시도 (1s → 2s → 3s → 4s → 5s)
|
||||
*/
|
||||
private ResponseEntity<String> sendWithRetry(String targetUrl, HttpEntity<String> request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception {
|
||||
private ResponseEntity<String> sendWithRetry(String proxyUrl, HttpEntity<String> request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception {
|
||||
Exception lastException = null;
|
||||
for (int attempt = 0; attempt <= retryCount; attempt++) {
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
long delayMs = retryTime * (1L << (attempt - 1));
|
||||
log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, targetUrl, delayMs);
|
||||
Thread.sleep(delayMs);
|
||||
long delayMs = (long) retryTime * attempt;
|
||||
log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, proxyUrl, delayMs);
|
||||
sendLog.setRetryCount(attempt);
|
||||
Thread.sleep(delayMs);
|
||||
}
|
||||
return restTemplate.postForEntity(targetUrl, request, String.class);
|
||||
return restTemplate.postForEntity(proxyUrl, request, String.class);
|
||||
} catch (HttpClientErrorException e) {
|
||||
throw e; // 4xx는 재시도 불필요
|
||||
//throw e; // 4xx는 재시도 불필요
|
||||
lastException = e; //TODO: 재시도 테스트
|
||||
} catch (Exception e) {
|
||||
lastException = e;
|
||||
log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, targetUrl, e.getMessage());
|
||||
log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, proxyUrl, e.getMessage());
|
||||
}
|
||||
}
|
||||
throw lastException;
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.eactive.eai.rms.onl.apim.portalorg.PortalOrgManService;
|
||||
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserManService;
|
||||
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserUI;
|
||||
import com.eactive.eai.rms.common.acl.user.ui.UserUI;
|
||||
import com.eactive.eai.common.util.SystemUtil;
|
||||
import com.eactive.eai.rms.onl.common.exception.BizException;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.ApiInterfaceService;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.mapping.ApiSpecUIMapper;
|
||||
@@ -359,6 +360,10 @@ public class PortalApprovalManService extends BaseService {
|
||||
}
|
||||
|
||||
public boolean isHardDeleteEnabled() {
|
||||
// 승인 요청 완전삭제는 dev(개발) 환경에서만 허용
|
||||
if (!SystemUtil.isDevServer()) {
|
||||
return false;
|
||||
}
|
||||
Map<String, String> properties = portalPropertyService.getPortalPropertiesAsMap(PORTAL_PROPERTY_GROUP);
|
||||
return Boolean.parseBoolean(properties.getOrDefault(HARD_DELETE_FLAG_KEY, "false"));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.eactive.eai.rms.onl.apim.authserver;
|
||||
|
||||
import com.eactive.eai.agent.command.CommonCommand;
|
||||
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
|
||||
import com.eactive.eai.rms.common.datasource.DataSourceType;
|
||||
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
||||
import com.eactive.eai.rms.onl.apim.common.BaseRestController;
|
||||
import com.eactive.eai.rms.onl.common.service.AgentUtilService;
|
||||
import com.eactive.eai.rms.onl.manage.authserver.client.ClientManService;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
/**
|
||||
* 포털 등 내부 시스템이 호출하는 인증서버(GW) 클라이언트 제어용 내부 API.
|
||||
*
|
||||
* <p>{@link BaseRestController} 를 상속하여 ① 세션 인증을 skip(InterceptorSkipController)하고
|
||||
* ② {@code IpWhitelistInterceptor} 를 통해 {@code kjb.api.allow_ip_list} 기준 IP 화이트리스트
|
||||
* 인증을 적용받는다. (호출 측 포털 서버 IP를 반드시 허용목록에 등록해야 함)</p>
|
||||
*
|
||||
* @see com.eactive.eai.rms.common.interceptor.IpWhitelistInterceptor
|
||||
*/
|
||||
@Controller
|
||||
public class ClientBlockApiController extends BaseRestController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ClientBlockApiController.class);
|
||||
|
||||
private final ClientManService clientManService;
|
||||
|
||||
@Qualifier("onlAgentUtilService")
|
||||
@Autowired
|
||||
private AgentUtilService agentUtilService;
|
||||
|
||||
@Autowired
|
||||
public ClientBlockApiController(ClientManService clientManService) {
|
||||
this.clientManService = clientManService;
|
||||
}
|
||||
|
||||
/**
|
||||
* clientId 의 GW 인증 클라이언트를 차단(TSEAIAU01.appstatus = '0')한 뒤,
|
||||
* 승인 경로와 동일하게 GW 인증서버로 RELOAD_CLIENT 명령을 broadcast 하여 캐시를 갱신한다.
|
||||
*
|
||||
* @param clientId 차단할 클라이언트 ID
|
||||
* @return 처리 결과 JSON ({@code {"success":true,"clientId":...}} 또는 {@code {"success":false,"msg":...}})
|
||||
*/
|
||||
@RequestMapping(value = "/onl/admin/authserver/clientBlock.json", method = RequestMethod.POST, produces = JSON_CONTENT_TYPE)
|
||||
@ResponseBody
|
||||
public String blockClient(@RequestParam String clientId) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
// TSEAIAU01(ClientEntity)은 APIGW 데이터소스에 있으므로 라우팅을 먼저 설정한다.
|
||||
DataSourceType type = DataSourceTypeManager.getDataSourceType("APIGW");
|
||||
DataSourceContextHolder.setDataSourceType(type);
|
||||
|
||||
// 상태 차단 (appstatus = '0')
|
||||
clientManService.blockClient(clientId);
|
||||
|
||||
// GW 인증서버 캐시 리로드 (승인 경로 PortalAppApprovalListener 와 동일)
|
||||
CommonCommand.builder()
|
||||
.name(CommonCommand.RELOAD_CLIENT_COMMAND)
|
||||
.args(clientId)
|
||||
.build().broadcast(agentUtilService);
|
||||
|
||||
logger.warn("GW client 차단(appstatus=0)+리로드 완료 - clientId={}", clientId);
|
||||
result.put("success", true);
|
||||
result.put("clientId", clientId);
|
||||
} catch (Exception e) {
|
||||
logger.error("GW client 차단 실패 - clientId={}", clientId, e);
|
||||
result.put("success", false);
|
||||
result.put("msg", e.getMessage());
|
||||
}
|
||||
return toJson(result);
|
||||
}
|
||||
}
|
||||
+12
-4
@@ -41,6 +41,7 @@ public class PortalInquiryManService extends BaseService {
|
||||
private final FileService fileService;
|
||||
|
||||
public static final String PENDING = "PENDING";
|
||||
public static final String REVIEWING = "REVIEWING";
|
||||
public static final String RESPONDED = "RESPONDED";
|
||||
public static final String CLOSED = "CLOSED";
|
||||
|
||||
@@ -116,12 +117,19 @@ public class PortalInquiryManService extends BaseService {
|
||||
|
||||
public PortalInquiryUI selectDetail(String id) {
|
||||
Inquiry inquiry = portalInquiryService.getById(id);
|
||||
|
||||
// 답변 대기중 상태에서 상세 조회 시 검토중 상태로 변경
|
||||
if (PENDING.equals(inquiry.getInquiryStatus())) {
|
||||
inquiry.setInquiryStatus(REVIEWING);
|
||||
portalInquiryService.save(inquiry);
|
||||
}
|
||||
|
||||
PortalInquiryUI portalInquiryUI = portalInquiryUIMapper.toVo(inquiry);
|
||||
|
||||
// 문의자 정보 마스킹처리 _ 이름, 이메일, 로그인id, 핸드폰번호
|
||||
if (portalInquiryUI.getInquirer() != null) {
|
||||
maskPortalInquiryUI(portalInquiryUI);
|
||||
}
|
||||
//if (portalInquiryUI.getInquirer() != null) {
|
||||
// maskPortalInquiryUI(portalInquiryUI);
|
||||
//}
|
||||
|
||||
if (StringUtils.isNotBlank(inquiry.getAttachFile())) {
|
||||
try {
|
||||
@@ -159,7 +167,7 @@ public class PortalInquiryManService extends BaseService {
|
||||
Inquiry inquiry = portalInquiryService.getById(portalInquiryUI.getId());
|
||||
validateNotClosed(inquiry);
|
||||
// 답변 대기중 상태일 경우만 답변완료로 상태 변경
|
||||
if(inquiry.getInquiryStatus().equals(PENDING)) {
|
||||
if(inquiry.getInquiryStatus().equals(PENDING) || inquiry.getInquiryStatus().equals(REVIEWING)) {
|
||||
inquiry.setInquiryStatus(RESPONDED);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.springframework.data.web.SortDefault;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -37,8 +38,8 @@ public class PortalOrgManController extends BaseAnnotationController {
|
||||
private final ComboService comboService;
|
||||
|
||||
@GetMapping(value = "/onl/apim/portalorg/portalOrgMan.view")
|
||||
public void view() {
|
||||
// view
|
||||
public void view(Model model) {
|
||||
model.addAttribute("hardDeleteEnabled", portalOrgManService.isHardDeleteEnabled());
|
||||
}
|
||||
|
||||
@GetMapping(value = "/onl/apim/portalorg/portalOrgMan.view", params = "cmd=DETAIL")
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.eactive.eai.rms.onl.apim.masking.MaskingUtils;
|
||||
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserCorpManagerUI;
|
||||
import com.eactive.eai.rms.onl.apim.portaluser.PortalUserCorpManagerUIMapper;
|
||||
import com.eactive.eai.rms.onl.common.exception.BizException;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -44,15 +45,27 @@ import java.util.Optional;
|
||||
@Slf4j
|
||||
public class PortalOrgManService extends BaseService {
|
||||
|
||||
/** PortalProperty 그룹명 */
|
||||
private static final String PORTAL_PROPERTY_GROUP = "Portal";
|
||||
/** 법인 완전삭제(hard delete) 허용 프로퍼티 키 (true:허용, 기본 false) */
|
||||
private static final String HARD_DELETE_FLAG_KEY = "org.hard-delete.enabled";
|
||||
|
||||
private final PortalOrgService portalOrgService;
|
||||
private final PortalOrgUIMapper portalOrgUIMapper;
|
||||
private final PortalUserService portalUserService;
|
||||
private final PortalUserCorpManagerUIMapper portalUserCorpManagerUIMapper;
|
||||
private final FileService fileService;
|
||||
private final PortalPropertyService portalPropertyService;
|
||||
|
||||
@PersistenceContext(unitName = "entityManagerFactoryForEMS")
|
||||
private EntityManager entityManager;
|
||||
|
||||
/** 법인 목록 '선택 삭제'(완전삭제) 버튼 노출 여부 */
|
||||
public boolean isHardDeleteEnabled() {
|
||||
Map<String, String> properties = portalPropertyService.getPortalPropertiesAsMap(PORTAL_PROPERTY_GROUP);
|
||||
return Boolean.parseBoolean(properties.getOrDefault(HARD_DELETE_FLAG_KEY, "false"));
|
||||
}
|
||||
|
||||
public Page<PortalOrgUI> selectList(Pageable pageable, PortalOrgUISearch portalOrgUISearch) {
|
||||
Page<PortalOrg> portalOrg = portalOrgService.findAll(pageable, portalOrgUISearch);
|
||||
return portalOrg.map(entity -> convertToUIWithMaskOption(entity, true));
|
||||
|
||||
@@ -14,7 +14,6 @@ public interface PortalPropertyMapper extends GenericMapper<PropertyUI, PortalPr
|
||||
@Mapping(source = "prptyGroupName", target = "id.propertyGroupName")
|
||||
@Mapping(source = "prptyName", target = "id.propertyName")
|
||||
@Mapping(source = "prpty2Val", target = "propertyValue")
|
||||
@Mapping(source = "prptyDesc", target = "propertyDesc")
|
||||
@Override
|
||||
PortalProperty toEntity(PropertyUI vo);
|
||||
|
||||
@@ -22,7 +21,6 @@ public interface PortalPropertyMapper extends GenericMapper<PropertyUI, PortalPr
|
||||
@Mapping(source ="id.propertyGroupName", target = "prptyGroupName")
|
||||
@Mapping(source ="id.propertyName", target = "prptyName")
|
||||
@Mapping(source ="propertyValue", target = "prpty2Val")
|
||||
@Mapping(source ="propertyDesc", target = "prptyDesc")
|
||||
@InheritInverseConfiguration
|
||||
@Override
|
||||
PropertyUI toVo(PortalProperty entity);
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.web.SortDefault;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
@@ -32,8 +33,8 @@ public class PortalUserManController extends BaseAnnotationController {
|
||||
private final ComboService comboService;
|
||||
|
||||
@GetMapping(value = "/onl/apim/portaluser/portalUserMan.view")
|
||||
public void view() {
|
||||
// view
|
||||
public void view(Model model) {
|
||||
model.addAttribute("hardDeleteEnabled", portalUserManService.isHardDeleteEnabled());
|
||||
}
|
||||
|
||||
@GetMapping(value = "/onl/apim/portaluser/portalUserMan.view", params = "cmd=DETAIL")
|
||||
|
||||
@@ -46,6 +46,8 @@ public class PortalUserManService extends BaseService {
|
||||
private static final String PORTAL_PROPERTY_GROUP = "Portal";
|
||||
/** 사용자 별 휴대폰 번호 중복 허용 프로퍼티 키 (true:허용, false:허용안함, 기본 false) */
|
||||
private static final String PROP_MOBILE_DUPLICATE_ALLOW = "user.mobile.duplicate.allow";
|
||||
/** 가입자 완전삭제(hard delete) 허용 프로퍼티 키 (true:허용, 기본 false) */
|
||||
private static final String HARD_DELETE_FLAG_KEY = "user.hard-delete.enabled";
|
||||
|
||||
private final PortalUserService portalUserService;
|
||||
private final PortalUserUIMapper portalUserUIMapper;
|
||||
@@ -60,6 +62,11 @@ public class PortalUserManService extends BaseService {
|
||||
private final PortalInquiryService portalInquiryService;
|
||||
private final PortalPropertyService portalPropertyService;
|
||||
|
||||
/** 가입자 목록 '선택 삭제'(완전삭제) 버튼 노출 여부 */
|
||||
public boolean isHardDeleteEnabled() {
|
||||
Map<String, String> properties = portalPropertyService.getPortalPropertiesAsMap(PORTAL_PROPERTY_GROUP);
|
||||
return Boolean.parseBoolean(properties.getOrDefault(HARD_DELETE_FLAG_KEY, "false"));
|
||||
}
|
||||
|
||||
public Page<PortalUserUI> selectList(Pageable pageable, PortalUserUISearch portalUserUISearch) {
|
||||
Page<PortalUser> portalUser = portalUserService.findAll(pageable, portalUserUISearch);
|
||||
|
||||
@@ -103,6 +103,18 @@ public class ClientManService extends OnlBaseService {
|
||||
clientEntityService.deleteById(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 클라이언트를 차단합니다. (appstatus = '0')
|
||||
* 삭제하지 않고 상태만 차단으로 변경합니다.
|
||||
*
|
||||
* @param clientId 차단할 클라이언트 ID
|
||||
*/
|
||||
public void blockClient(String clientId) {
|
||||
ClientEntity clientEntity = clientEntityService.getById(clientId);
|
||||
clientEntity.setAppstatus("0"); // 0: 차단, 1: 정상
|
||||
clientEntityService.save(clientEntity);
|
||||
}
|
||||
|
||||
|
||||
public List<String> findAll() {
|
||||
List<ClientEntity> clientList = clientEntityService.findAll();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.eactive.eai.rms.onl.manage.authserver.scope;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -8,21 +10,29 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import com.eactive.eai.agent.authserver.ReloadApiScopeCommand;
|
||||
import com.eactive.eai.agent.command.CommonCommand;
|
||||
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
|
||||
import com.eactive.eai.rms.common.vo.GridResponse;
|
||||
import com.eactive.eai.rms.data.entity.onl.authserver.ScopeSearch;
|
||||
import com.eactive.eai.rms.data.entity.onl.eaimsg.ApiScopeSearch;
|
||||
import com.eactive.eai.rms.onl.common.exception.BizException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@Controller
|
||||
public class ScopeController extends OnlBaseAnnotationController {
|
||||
|
||||
private ScopeManService scopeManService;
|
||||
|
||||
private ApiScopeManService apiScopeManService;
|
||||
|
||||
@Autowired
|
||||
public ScopeController(ScopeManService scopeManService) {
|
||||
public ScopeController(ScopeManService scopeManService, ApiScopeManService apiScopeManService) {
|
||||
this.scopeManService = scopeManService;
|
||||
this.apiScopeManService = apiScopeManService;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/onl/admin/authserver/scopeMan.view")
|
||||
@@ -41,6 +51,12 @@ public class ScopeController extends OnlBaseAnnotationController {
|
||||
return ResponseEntity.ok(new GridResponse<>(page));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/admin/authserver/scopeMan.json", params = "cmd=API_LIST")
|
||||
public ResponseEntity<GridResponse<ApiScopeUI>> selectApiList(Pageable pageable, ApiScopeSearch apiScopeSearch) {
|
||||
Page<ApiScopeUI> page = apiScopeManService.selectApiScopeRelationsList(pageable, apiScopeSearch);
|
||||
return ResponseEntity.ok(new GridResponse<>(page));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/admin/authserver/scopeMan.json", params = "cmd=DETAIL")
|
||||
public ResponseEntity<ScopeUI> selectDetail(String scopeId) {
|
||||
ScopeUI scopeUI = scopeManService.selectDetail(scopeId);
|
||||
@@ -48,14 +64,16 @@ public class ScopeController extends OnlBaseAnnotationController {
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/admin/authserver/scopeMan.json", params = "cmd=INSERT")
|
||||
public ResponseEntity<Void> insert(ScopeUI scopeUI) {
|
||||
public ResponseEntity<Void> insert(ScopeUI scopeUI, @RequestParam(value = "apiList", required = false) String apiListJson) {
|
||||
scopeManService.insert(scopeUI);
|
||||
saveApiScopeRelations(scopeUI.getScopeId(), apiListJson);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/admin/authserver/scopeMan.json", params = "cmd=UPDATE")
|
||||
public ResponseEntity<Void> update(ScopeUI scopeUI) {
|
||||
public ResponseEntity<Void> update(ScopeUI scopeUI, @RequestParam(value = "apiList", required = false) String apiListJson) {
|
||||
scopeManService.update(scopeUI);
|
||||
saveApiScopeRelations(scopeUI.getScopeId(), apiListJson);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -69,8 +87,29 @@ public class ScopeController extends OnlBaseAnnotationController {
|
||||
.args(new String[] {ReloadApiScopeCommand.COMMAND_TYPE_SCOPE, "", scopeId})
|
||||
.build()
|
||||
.broadcast(agentUtilService);
|
||||
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
|
||||
private void saveApiScopeRelations(String scopeId, String apiListJson) {
|
||||
if (apiListJson == null || apiListJson.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<ApiScopeUI> apiScopeList;
|
||||
try {
|
||||
apiScopeList = new ObjectMapper().readValue(apiListJson, new TypeReference<List<ApiScopeUI>>() {});
|
||||
} catch (Exception e) {
|
||||
throw new BizException("데이터 변환 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
scopeManService.updateApiScopeRelations(apiScopeList, scopeId);
|
||||
|
||||
CommonCommand.builder()
|
||||
.name(CommonCommand.RELOAD_API_SCOPE_COMMAND)
|
||||
.args(new String[] {ReloadApiScopeCommand.COMMAND_TYPE_SCOPE, "", scopeId})
|
||||
.build()
|
||||
.broadcast(agentUtilService);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.eactive.eai.rms.onl.manage.authserver.scope;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -21,17 +23,20 @@ public class ScopeManService extends OnlBaseService {
|
||||
private ScopeInfoService scopeInfoService;
|
||||
|
||||
private ScopeEntityService scopeEntityService;
|
||||
|
||||
|
||||
private ScopeUIMapper scopeUIMapper;
|
||||
|
||||
private ApiScopeUIMapper apiScopeUIMapper;
|
||||
|
||||
private LocaleMessage localeMessage;
|
||||
|
||||
|
||||
@Autowired
|
||||
public ScopeManService(ScopeInfoService scopeInfoService, ScopeEntityService scopeEntityService,
|
||||
ScopeUIMapper scopeUIMapper, LocaleMessage localeMessage ) {
|
||||
public ScopeManService(ScopeInfoService scopeInfoService, ScopeEntityService scopeEntityService,
|
||||
ScopeUIMapper scopeUIMapper, ApiScopeUIMapper apiScopeUIMapper, LocaleMessage localeMessage ) {
|
||||
this.scopeInfoService = scopeInfoService;
|
||||
this.scopeEntityService = scopeEntityService;
|
||||
this.scopeUIMapper = scopeUIMapper;
|
||||
this.apiScopeUIMapper = apiScopeUIMapper;
|
||||
this.localeMessage = localeMessage;
|
||||
}
|
||||
|
||||
@@ -65,5 +70,23 @@ public class ScopeManService extends OnlBaseService {
|
||||
public void deleteApiScopeRelationsByScopeId(String scopeId) {
|
||||
scopeEntityService.deleteByIdScopeId(scopeId);
|
||||
}
|
||||
|
||||
|
||||
public void updateApiScopeRelations(List<ApiScopeUI> apiScopeList, String scopeId) {
|
||||
if (scopeId == null || scopeId.isEmpty()) {
|
||||
throw new BizException("ScopeId 가 존재하지 않습니다.");
|
||||
}
|
||||
|
||||
// ScopeId 기준 전체 삭제 후 LIST 기준으로 재저장.
|
||||
scopeEntityService.deleteByIdScopeId(scopeId);
|
||||
|
||||
if (apiScopeList == null || apiScopeList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ApiScopeUI ui : apiScopeList) {
|
||||
ui.setScopeId(scopeId);
|
||||
scopeEntityService.save(apiScopeUIMapper.toEntity(ui));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,4 @@ public class PropertyUI {
|
||||
|
||||
@JsonProperty("PRPTY2VAL")
|
||||
private String prpty2Val;
|
||||
|
||||
@JsonProperty("PRPTYDESC")
|
||||
private String prptyDesc;
|
||||
}
|
||||
|
||||
@@ -352,7 +352,7 @@ public class LayoutManService extends OnlBaseService {
|
||||
}
|
||||
|
||||
if (max == 0 && min == 0) {
|
||||
if ("GRID".equalsIgnoreCase(loutItemType)) {
|
||||
if ("GRID".equalsIgnoreCase(loutItemType) || "ARRAY".equalsIgnoreCase(loutItemType)) {
|
||||
min = 1;
|
||||
max = -1;
|
||||
} else {
|
||||
@@ -371,7 +371,8 @@ public class LayoutManService extends OnlBaseService {
|
||||
}
|
||||
|
||||
private String determineOccurNoItem(LayoutItemUI layoutItemUI, String loutItemType) {
|
||||
if ("GRID".equalsIgnoreCase(loutItemType) && NumberUtils.toInt(layoutItemUI.getLoutItemOccCnt()) == 0) {
|
||||
if (("GRID".equalsIgnoreCase(loutItemType) || "ARRAY".equalsIgnoreCase(loutItemType))
|
||||
&& NumberUtils.toInt(layoutItemUI.getLoutItemOccCnt()) == 0) {
|
||||
return "*";
|
||||
}
|
||||
return String.valueOf(layoutItemUI.getLoutItemOccCnt());
|
||||
@@ -385,6 +386,8 @@ public class LayoutManService extends OnlBaseService {
|
||||
node = Item.NODE_GROUP;
|
||||
} else if ("ATTR".equalsIgnoreCase(el.getLoutItemType())) {
|
||||
node = Item.NODE_ATTR;
|
||||
} else if ("ARRAY".equalsIgnoreCase(el.getLoutItemType())) {
|
||||
node = Item.NODE_FIELD;
|
||||
} else {
|
||||
node = Item.NODE_FIELD;
|
||||
}
|
||||
|
||||
+26
-6
@@ -71,13 +71,33 @@ public interface LayoutItemUIMapper extends GenericMapper<LayoutItemUI, LayoutIt
|
||||
itemType = isGridCondition ? "GRID" : "GROUP";
|
||||
|
||||
// isGridCondition이 참이고, 참조 정보2가 비어 있는 경우, itemLoopCount 설정
|
||||
if (isGridCondition && StringUtils.isBlank(item.getLoutitemrefinfo2())
|
||||
&& NumberUtils.isCreatable(item.getLoutitemoccurptrndstcd())
|
||||
&& Integer.parseInt(item.getLoutitemoccurptrndstcd()) > -1 ) {
|
||||
itemLoopCount = item.getLoutitemoccurptrndstcd();
|
||||
if (isGridCondition && StringUtils.isBlank(item.getLoutitemrefinfo2())) {
|
||||
if ("*".equals(item.getLoutitemoccurptrndstcd())) {
|
||||
// 반복횟수로 '*'(무제한)이 저장된 경우, 화면 그리드에도 '*'을 그대로 표시
|
||||
itemLoopCount = "*";
|
||||
} else if (NumberUtils.isCreatable(item.getLoutitemoccurptrndstcd())
|
||||
&& Integer.parseInt(item.getLoutitemoccurptrndstcd()) > -1) {
|
||||
itemLoopCount = item.getLoutitemoccurptrndstcd();
|
||||
}
|
||||
}
|
||||
} else if (ItemNodeType.FIELD.getNumber() == item.getLoutitemnodeptrnidname()) {
|
||||
// 아이템 발생 포인터 명이 '*'이거나 숫자로 생성 가능한 경우
|
||||
boolean isArrayCondition = "*".equals(item.getLoutitemoccurptrndstcd()) || NumberUtils.isCreatable(item.getLoutitemoccurptrndstcd());
|
||||
// itemType을 ARRAY 또는 FIELD 로 설정
|
||||
itemType = isArrayCondition ? "ARRAY" : "FIELD";
|
||||
|
||||
// isArrayCondition이 참이고, 참조 정보2가 비어 있는 경우, itemLoopCount 설정
|
||||
if (isArrayCondition && StringUtils.isBlank(item.getLoutitemrefinfo2())) {
|
||||
if ("*".equals(item.getLoutitemoccurptrndstcd())) {
|
||||
// 반복횟수로 '*'(무제한)이 저장된 경우, 화면 그리드에도 '*'을 그대로 표시
|
||||
itemLoopCount = "*";
|
||||
} else if (NumberUtils.isCreatable(item.getLoutitemoccurptrndstcd())
|
||||
&& Integer.parseInt(item.getLoutitemoccurptrndstcd()) > -1) {
|
||||
itemLoopCount = item.getLoutitemoccurptrndstcd();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 아이템 노드 타입이 GROUP이 아닌 경우, 해당 노드 타입으로 itemType 설정
|
||||
// 아이템 노드 타입이 GROUP/FIELD가 아닌 경우, 해당 노드 타입으로 itemType 설정
|
||||
itemType = ItemNodeType.fromNumber(item.getLoutitemnodeptrnidname()).toString();
|
||||
}
|
||||
|
||||
@@ -85,7 +105,7 @@ public interface LayoutItemUIMapper extends GenericMapper<LayoutItemUI, LayoutIt
|
||||
|
||||
itemUi.setLoutItemType(itemType);
|
||||
|
||||
if(NumberUtils.toInt(itemLoopCount) == 0){
|
||||
if (!"*".equals(itemLoopCount) && NumberUtils.toInt(itemLoopCount) == 0) {
|
||||
itemLoopCount = "";
|
||||
}
|
||||
itemUi.setLoutItemOccCnt(itemLoopCount);
|
||||
|
||||
+25
-5
@@ -71,10 +71,30 @@ public interface LayoutPopupUIMapper extends GenericMapper<LayoutPopupUI, Layout
|
||||
itemType = isGridCondition ? "GRID" : "GROUP";
|
||||
|
||||
// isGridCondition이 참이고, 참조 정보2가 비어 있는 경우, itemLoopCount 설정
|
||||
if (isGridCondition && StringUtils.isBlank(item.getLoutitemrefinfo2())
|
||||
&& NumberUtils.isCreatable(item.getLoutitemoccurptrndstcd())
|
||||
&& Integer.parseInt(item.getLoutitemoccurptrndstcd()) > -1 ) {
|
||||
itemLoopCount = item.getLoutitemoccurptrndstcd();
|
||||
if (isGridCondition && StringUtils.isBlank(item.getLoutitemrefinfo2())) {
|
||||
if ("*".equals(item.getLoutitemoccurptrndstcd())) {
|
||||
// 반복횟수로 '*'(무제한)이 저장된 경우, 화면 그리드에도 '*'을 그대로 표시
|
||||
itemLoopCount = "*";
|
||||
} else if (NumberUtils.isCreatable(item.getLoutitemoccurptrndstcd())
|
||||
&& Integer.parseInt(item.getLoutitemoccurptrndstcd()) > -1) {
|
||||
itemLoopCount = item.getLoutitemoccurptrndstcd();
|
||||
}
|
||||
}
|
||||
} else if (ItemNodeType.FIELD.getNumber() == item.getLoutitemnodeptrnidname()) {
|
||||
// 아이템 발생 포인터 명이 '*'이거나 숫자로 생성 가능한 경우
|
||||
boolean isArrayCondition = "*".equals(item.getLoutitemoccurptrndstcd()) || NumberUtils.isCreatable(item.getLoutitemoccurptrndstcd());
|
||||
// itemType을 ARRAY 또는 FIELD 로 설정
|
||||
itemType = isArrayCondition ? "ARRAY" : "FIELD";
|
||||
|
||||
// isArrayCondition이 참이고, 참조 정보2가 비어 있는 경우, itemLoopCount 설정
|
||||
if (isArrayCondition && StringUtils.isBlank(item.getLoutitemrefinfo2())) {
|
||||
if ("*".equals(item.getLoutitemoccurptrndstcd())) {
|
||||
// 반복횟수로 '*'(무제한)이 저장된 경우, 화면 그리드에도 '*'을 그대로 표시
|
||||
itemLoopCount = "*";
|
||||
} else if (NumberUtils.isCreatable(item.getLoutitemoccurptrndstcd())
|
||||
&& Integer.parseInt(item.getLoutitemoccurptrndstcd()) > -1) {
|
||||
itemLoopCount = item.getLoutitemoccurptrndstcd();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 아이템 노드 타입이 GROUP이 아닌 경우, 해당 노드 타입으로 itemType 설정
|
||||
@@ -85,7 +105,7 @@ public interface LayoutPopupUIMapper extends GenericMapper<LayoutPopupUI, Layout
|
||||
|
||||
popupUI.setLoutItemType(itemType);
|
||||
|
||||
if(NumberUtils.toInt(itemLoopCount) == 0){
|
||||
if (!"*".equals(itemLoopCount) && NumberUtils.toInt(itemLoopCount) == 0) {
|
||||
itemLoopCount = "";
|
||||
}
|
||||
popupUI.setLoutItemOccCnt(itemLoopCount);
|
||||
|
||||
@@ -555,7 +555,7 @@ public class ApiInterfaceService extends OnlBaseService {
|
||||
restOption.setStandardCommonFields(standardCommonFields);
|
||||
return objectMapper.writeValueAsString(restOption);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to create standard common option", e);
|
||||
throw new RuntimeException("표준전문 공통부 생성을 실패했습니다", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1117,13 +1117,14 @@ public class ApiInterfaceService extends OnlBaseService {
|
||||
String replacementBetweenUnderscores = newApiInterfaceId;
|
||||
|
||||
// 정규 표현식 패턴: 첫 번째 언더바 전의 단어와 두 번째 언더바 사이의 단어를 찾기 위한 패턴
|
||||
String regex = "(^[^_]+)|(?<=_)\\w+(?=_)";
|
||||
// (서비스ID 등에 '-'가 포함될 수 있으므로 \w+ 대신 [^_]+ 사용 - transform 치환 로직과 동일)
|
||||
String regex = "(^[^_]+)|(?<=_)[^_]+(?=_)";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(loutName);
|
||||
|
||||
// 첫 번째 매칭된 부분(언더바 전의 단어)과 두 번째 매칭된 부분(언더바 사이의 단어)을 대체
|
||||
String updatedString = matcher.replaceFirst(replacementBeforeUnderscore); // 첫 번째 매칭된 부분 대체
|
||||
updatedString = updatedString.replaceFirst("(?<=_)\\w+(?=_)", replacementBetweenUnderscores); // 두 번째 매칭된 부분 대체
|
||||
updatedString = updatedString.replaceFirst("(?<=_)[^_]+(?=_)", replacementBetweenUnderscores); // 두 번째 매칭된 부분 대체
|
||||
|
||||
layoutUI.setLoutName(updatedString);
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public class ApiSpecController extends OnlBaseAnnotationController {
|
||||
return "/onl/transaction/apim/apiSpecManPopup";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/onl/transaction/apim/apiSpecMan.view", params = "cmd=POPUP_LIST")
|
||||
@GetMapping(value = "/onl/transaction/apim/apiSpecManPopup.view", params = "cmd=POPUP_LIST")
|
||||
public String detailOrgPopupView() {
|
||||
return "/onl/transaction/apim/apiSpecManDisplayOrgPopup";
|
||||
}
|
||||
|
||||
@@ -1,541 +0,0 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
|
||||
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
|
||||
import com.eactive.eai.rms.data.entity.onl.eaimsg.EAIMessageService;
|
||||
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutUI;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import com.eactive.eai.rms.data.entity.onl.apim.apigroup.ApiGroupService;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
/**
|
||||
* 신규 OpenAPI Spec 6단계 편집 마법사 컨트롤러.
|
||||
*
|
||||
* apiInterfaceMan 상세의 "OpenAPI Spec" 버튼이 새 탭(_blank)으로 여는 페이지 및 마법사가 호출하는
|
||||
* cmd API 를 제공한다. 저장은 기존 컬럼 + testbedSpec(OpenAPI JSON)에 매핑(스키마 무변경, 경량).
|
||||
*
|
||||
* (MVC 컨트롤러는 springapp-servlet.xml 의 com.eactive.eai.rms 스캔 범위에서만 등록되므로 본 컨트롤러는
|
||||
* 해당 패키지에 둔다. 기존 apiSpecMan.view(v1)와 URL 을 분리해 공존한다.)
|
||||
*/
|
||||
@Controller
|
||||
public class DjbApiSpecController extends OnlBaseAnnotationController {
|
||||
|
||||
@Autowired
|
||||
private ApiSpecManService apiSpecManService;
|
||||
|
||||
@Autowired
|
||||
private ApiInterfaceService apiInterfaceService;
|
||||
|
||||
@Autowired
|
||||
private EAIMessageService eaiMessageService;
|
||||
|
||||
@Autowired
|
||||
private DjbApiSpecSwaggerEnricher swaggerEnricher;
|
||||
|
||||
@Autowired
|
||||
private DjbApiSpecLayoutMergeService layoutMergeService;
|
||||
|
||||
@Autowired
|
||||
private PortalPropertyService portalPropertyService;
|
||||
|
||||
@Autowired
|
||||
private ApiGroupService apiGroupService;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// GW base URL 단일 기준 = 포탈 DjbTestbedGatewayProperty 의 djb.gateway.base-url (PTL_PROPERTY 그룹 'Portal').
|
||||
// 별도 swagger.gw.address 프로퍼티는 두지 않는다.
|
||||
private static final String GW_BASE_URL_KEY = "djb.gateway.base-url";
|
||||
private static final String GW_BASE_URL_DEFAULT = "PortalMock";
|
||||
// "PortalMock" 은 포탈이 요청 origin 으로 치환하는 sentinel 이라 admin 미리보기용 서버 주소로는 부적합 → 대체값 사용.
|
||||
private static final String GW_PREVIEW_FALLBACK = "http://127.0.0.1:39310";
|
||||
|
||||
// ── 진입 ────────────────────────────────────────────────
|
||||
@GetMapping(value = "/onl/transaction/apim/djbApiSpecMan.view", params = "cmd=DETAIL")
|
||||
public String viewDetail(Model model) {
|
||||
// GW base URL (djb.gateway.base-url, 그룹 'Portal') — 없으면 기본값으로 생성. DjbTestbedGatewayProperty 와 동일 키.
|
||||
String gw = portalPropertyService.getOrCreateProperty(
|
||||
"Portal", GW_BASE_URL_KEY, GW_BASE_URL_DEFAULT, "GW base URL (테스트베드 공통)");
|
||||
if (StringUtils.isBlank(gw) || GW_BASE_URL_DEFAULT.equalsIgnoreCase(gw.trim())) {
|
||||
gw = GW_PREVIEW_FALLBACK; // PortalMock → admin 미리보기용 구체 주소로 대체
|
||||
}
|
||||
model.addAttribute("gwAddress", gw.trim());
|
||||
return "/onl/transaction/apim/djbApiSpecManPopup";
|
||||
}
|
||||
|
||||
// API 그룹 선택 팝업(iframe 모달 대상). 그룹 목록 그리드는 apiGroupMan.json?cmd=LIST 재사용.
|
||||
@GetMapping(value = "/onl/transaction/apim/djbApiSpecMan.view", params = "cmd=GROUP_POPUP")
|
||||
public String viewGroupPopup() {
|
||||
return "/onl/transaction/apim/djbApiGroupPopup";
|
||||
}
|
||||
|
||||
// ── 조회: 저장 spec 우선 + authtype securityScheme 주입 ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=DETAIL_SPEC")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> detailSpec(String eaiSvcName, String regen) throws Exception {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (StringUtils.isBlank(eaiSvcName)) {
|
||||
result.put("status", "fail");
|
||||
result.put("errorMsg", "eaiSvcName 이 없습니다.");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
|
||||
// regen=true 이면 저장본 무시하고 레이아웃 기반으로 강제 재생성 (자동 생성/초기화)
|
||||
boolean force = "true".equalsIgnoreCase(regen);
|
||||
String spec;
|
||||
String source;
|
||||
ApiSpecInfoUI saved = force ? null : apiSpecManService.selectDetail(eaiSvcName);
|
||||
if (!force && saved != null && StringUtils.isNotBlank(saved.getTestbedSpec())) {
|
||||
spec = saved.getTestbedSpec();
|
||||
source = "saved";
|
||||
} else {
|
||||
spec = generateSpec(eaiSvcName);
|
||||
source = force ? "regenerated" : "generated";
|
||||
}
|
||||
|
||||
String authtype = eaiMessageService.findById(eaiSvcName)
|
||||
.map(EAIMessageEntity::getAuthtype)
|
||||
.orElse(null);
|
||||
String enriched = swaggerEnricher.enrich(spec, authtype);
|
||||
|
||||
result.put("testbedSpec", enriched);
|
||||
result.put("source", source);
|
||||
result.put("authType", authtype == null ? "" : authtype);
|
||||
// API 그룹 소속(조인테이블 ApiGroupApi) — 저장 여부와 무관하게 현재 소속 반환
|
||||
try {
|
||||
ArrayNode groups = objectMapper.createArrayNode();
|
||||
for (ApiGroup g : apiGroupService.findByApiId(eaiSvcName)) {
|
||||
ObjectNode gn = objectMapper.createObjectNode();
|
||||
gn.put("id", g.getId());
|
||||
gn.put("groupName", g.getGroupName());
|
||||
groups.add(gn);
|
||||
}
|
||||
result.put("apiGroups", objectMapper.writeValueAsString(groups));
|
||||
} catch (Exception e) {
|
||||
logger.warn("API 그룹 조회 실패: " + eaiSvcName, e);
|
||||
}
|
||||
// 저장본이면 폼 복원용 기본정보/공개설정도 함께 반환
|
||||
if (saved != null) {
|
||||
putIfNotNull(result, "apiName", saved.getApiName());
|
||||
putIfNotNull(result, "apiSimpleDescription", saved.getApiSimpleDescription());
|
||||
putIfNotNull(result, "apiUrl", saved.getApiUrl());
|
||||
putIfNotNull(result, "apiMethod", saved.getApiMethod());
|
||||
putIfNotNull(result, "apiContentType", saved.getApiContentType());
|
||||
putIfNotNull(result, "description", saved.getDescription());
|
||||
putIfNotNull(result, "apiRequestSpec", saved.getApiRequestSpec());
|
||||
putIfNotNull(result, "apiResponseSpec", saved.getApiResponseSpec());
|
||||
putIfNotNull(result, "sampleRequest", saved.getSampleRequest());
|
||||
putIfNotNull(result, "sampleResponse", saved.getSampleResponse());
|
||||
putIfNotNull(result, "responseType", saved.getResponseType());
|
||||
putIfNotNull(result, "mockUrl", saved.getMockUrl());
|
||||
putIfNotNull(result, "displayYn", saved.getDisplayYn());
|
||||
putIfNotNull(result, "displayRoleCode", saved.getDisplayRoleCode());
|
||||
putIfNotNull(result, "displayOrg", saved.getDisplayOrg());
|
||||
}
|
||||
// 예제/설명자료(요청·응답 메시지 스펙 HTML)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치.
|
||||
// 저장본이면 위 saved 블록에서 저장값을 이미 반환.
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 저장: 마법사 폼 → ApiSpecInfoUI → apiSpecManService.save (기존 재사용) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=SAVE")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> save(ApiSpecInfoUI apiSpecInfoUI, String apiGroupIds) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (apiSpecInfoUI == null || StringUtils.isBlank(apiSpecInfoUI.getApiId())) {
|
||||
result.put("status", "fail");
|
||||
result.put("errorMsg", "API ID 가 없습니다.");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
// CrossScriptingFilter(RequestWrapper.cleanXSS)가 요청 파라미터의 특수문자를 엔티티로 치환
|
||||
// ('('→( ')'→) '"'→" 등)하므로, 저장 전 모든 자유텍스트/JSON 필드를 언이스케이프해 원복한다.
|
||||
// (JSON 필드는 특히 '"'→" 로 깨지므로 필수)
|
||||
apiSpecInfoUI.setApiName(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiName()));
|
||||
apiSpecInfoUI.setApiSimpleDescription(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiSimpleDescription()));
|
||||
apiSpecInfoUI.setApiUrl(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiUrl()));
|
||||
apiSpecInfoUI.setMockUrl(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getMockUrl()));
|
||||
apiSpecInfoUI.setApiRequestSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiRequestSpec()));
|
||||
apiSpecInfoUI.setApiResponseSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiResponseSpec()));
|
||||
apiSpecInfoUI.setSampleRequest(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getSampleRequest()));
|
||||
apiSpecInfoUI.setSampleResponse(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getSampleResponse()));
|
||||
apiSpecInfoUI.setDescription(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getDescription()));
|
||||
apiSpecInfoUI.setTestbedSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getTestbedSpec()));
|
||||
|
||||
apiSpecManService.save(apiSpecInfoUI);
|
||||
|
||||
// API 그룹 소속(조인테이블) 동기화 — apiGroupIds(CSV) 로 재설정
|
||||
try {
|
||||
java.util.List<String> gids = new ArrayList<>();
|
||||
if (StringUtils.isNotBlank(apiGroupIds)) {
|
||||
for (String g : apiGroupIds.split(",")) {
|
||||
if (StringUtils.isNotBlank(g)) {
|
||||
gids.add(g.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
apiGroupService.setApiGroupsForApi(apiSpecInfoUI.getApiId(), gids);
|
||||
} catch (Exception e) {
|
||||
logger.warn("API 그룹 동기화 실패: " + apiSpecInfoUI.getApiId(), e);
|
||||
}
|
||||
|
||||
result.put("status", "success");
|
||||
result.put("apiId", apiSpecInfoUI.getApiId());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 레이아웃 불러오기 (GW) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=LOAD_REQUEST_LAYOUT")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> loadRequestLayout(String eaiSvcName) throws Exception {
|
||||
return loadLayout(eaiSvcName, true);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=LOAD_RESPONSE_LAYOUT")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> loadResponseLayout(String eaiSvcName) throws Exception {
|
||||
return loadLayout(eaiSvcName, false);
|
||||
}
|
||||
|
||||
// ── HTML 테이블 생성 (기존 generateSpecTableFromLayout 재사용) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=GENERATE_HTML")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> generateHtml(String eaiSvcName, String layoutType) throws Exception {
|
||||
LayoutUI layout = resolveLayout(eaiSvcName, !"RESPONSE".equalsIgnoreCase(layoutType));
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("html", apiSpecManService.generateSpecTableFromLayout(layout));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 메시지 예제 생성 (기존 generateSampleDataFromLayout 재사용) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=GENERATE_SAMPLE")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> generateSample(String eaiSvcName, String layoutType) throws Exception {
|
||||
LayoutUI layout = resolveLayout(eaiSvcName, !"RESPONSE".equalsIgnoreCase(layoutType));
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("json", apiSpecManService.generateSampleDataFromLayout(layout));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 내부 헬퍼 ──────────────────────────────────────────
|
||||
private ResponseEntity<Map<String, Object>> loadLayout(String eaiSvcName, boolean request) throws Exception {
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
if (StringUtils.isBlank(eaiSvcName)) {
|
||||
res.put("layoutType", request ? "REQUEST" : "RESPONSE");
|
||||
res.put("fields", new ArrayList<>());
|
||||
warnings.add("eaiSvcName 이 없습니다.");
|
||||
res.put("warnings", warnings);
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
LayoutUI layout = resolveLayout(eaiSvcName, request);
|
||||
if (layout == null || layout.getLayoutItems() == null || layout.getLayoutItems().isEmpty()) {
|
||||
warnings.add((request ? "요청" : "응답") + " 레이아웃이 없습니다.");
|
||||
}
|
||||
res.put("layoutType", request ? "REQUEST" : "RESPONSE");
|
||||
res.put("fields", layoutMergeService.toFields(layout, request ? "REQUEST" : "RESPONSE"));
|
||||
res.put("warnings", warnings);
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
|
||||
private LayoutUI resolveLayout(String eaiSvcName, boolean request) throws Exception {
|
||||
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
||||
if (apiInterfaceUI == null) {
|
||||
return null;
|
||||
}
|
||||
String layoutName = request ? apiInterfaceUI.getInboundRequestLayout() : apiInterfaceUI.getInboundResponseLayout();
|
||||
return StringUtils.isNotEmpty(layoutName) ? apiSpecManService.selectLayoutUI(layoutName) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 기반 자동 생성 + 자동생성 규칙(백엔드 단일 소스).
|
||||
* 규칙: title/tag/summary = API명(eaiSvcDesc), operationId = 인터페이스ID,
|
||||
* servers[0].url 을 path 앞에 붙이고 servers 제거(서버 선택 제거),
|
||||
* 요청/200 응답 예제를 레이아웃 샘플로 임베드.
|
||||
*/
|
||||
private String generateSpec(String eaiSvcName) throws Exception {
|
||||
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
||||
if (apiInterfaceUI == null) {
|
||||
throw new IllegalStateException("API 인터페이스 정보를 찾을 수 없습니다: " + eaiSvcName);
|
||||
}
|
||||
Map<String, String> inboundAdapterSpec = apiInterfaceService.getHttpAdapterInfo(apiInterfaceUI.getFromAdapter());
|
||||
if (inboundAdapterSpec == null) {
|
||||
inboundAdapterSpec = new HashMap<>();
|
||||
}
|
||||
String requestLayoutName = apiInterfaceUI.getInboundRequestLayout();
|
||||
String responseLayoutName = apiInterfaceUI.getInboundResponseLayout();
|
||||
LayoutUI requestLayoutUI = StringUtils.isNotEmpty(requestLayoutName)
|
||||
? apiSpecManService.selectLayoutUI(requestLayoutName) : null;
|
||||
LayoutUI responseLayoutUI = StringUtils.isNotEmpty(responseLayoutName)
|
||||
? apiSpecManService.selectLayoutUI(responseLayoutName) : null;
|
||||
|
||||
String baseSpec = apiSpecManService.generateSwaggerSpec(requestLayoutUI, responseLayoutUI, inboundAdapterSpec, apiInterfaceUI);
|
||||
|
||||
String apiName = StringUtils.isNotBlank(apiInterfaceUI.getEaiSvcDesc()) ? apiInterfaceUI.getEaiSvcDesc() : eaiSvcName;
|
||||
String reqSample = apiSpecManService.generateSampleDataFromLayout(requestLayoutUI);
|
||||
String resSample = apiSpecManService.generateSampleDataFromLayout(responseLayoutUI);
|
||||
String contentType = StringUtils.isNotBlank(apiInterfaceUI.getRestContentType()) ? apiInterfaceUI.getRestContentType() : "application/json";
|
||||
return applyAutoRules(baseSpec, apiName, eaiSvcName, reqSample, resSample, contentType);
|
||||
}
|
||||
|
||||
/** 자동생성 규칙 후처리(Jackson 트리 조작). 실패 시 원본 유지. */
|
||||
private String applyAutoRules(String specJson, String apiName, String eaiSvcName, String reqSample, String resSample, String contentType) {
|
||||
try {
|
||||
JsonNode parsed = objectMapper.readTree(specJson);
|
||||
if (!parsed.isObject()) {
|
||||
return specJson;
|
||||
}
|
||||
ObjectNode root = (ObjectNode) parsed;
|
||||
|
||||
root.put("openapi", "3.1.0"); // OpenAPI 3.1 방출(Swagger UI 5.x oas31 렌더 지원)
|
||||
getOrCreateObject(root, "info").put("title", apiName);
|
||||
|
||||
String tagName = "DJBank"; // 자동생성 태그명 고정
|
||||
ArrayNode tags = objectMapper.createArrayNode();
|
||||
ObjectNode tag = objectMapper.createObjectNode();
|
||||
tag.put("name", tagName);
|
||||
tag.put("description", apiName); // 태그 설명 = 간단설명(API명)
|
||||
tags.add(tag);
|
||||
root.set("tags", tags);
|
||||
|
||||
// 어댑터경로(servers[0].url = getHttpAdapterInfo urlPath)를 path 앞에 baking 하고 servers 제거.
|
||||
// 호스트(gw=djb.gateway.base-url / mock=Mock URL / sample=없음)는 프론트가 응답유형에 따라 서버로 붙인다.
|
||||
String adapterPath = "";
|
||||
JsonNode serversNode = root.get("servers");
|
||||
if (serversNode != null && serversNode.isArray() && serversNode.size() > 0 && serversNode.get(0).get("url") != null) {
|
||||
adapterPath = serversNode.get(0).get("url").asText("");
|
||||
}
|
||||
root.remove("servers");
|
||||
|
||||
JsonNode pathsNode = root.get("paths");
|
||||
if (pathsNode != null && pathsNode.isObject()) {
|
||||
ObjectNode paths = (ObjectNode) pathsNode;
|
||||
List<String> pathKeys = new ArrayList<>();
|
||||
Iterator<String> pit = paths.fieldNames();
|
||||
while (pit.hasNext()) {
|
||||
pathKeys.add(pit.next());
|
||||
}
|
||||
for (String pk : pathKeys) {
|
||||
JsonNode pathItem = paths.get(pk);
|
||||
if (pathItem != null && pathItem.isObject()) {
|
||||
List<String> methods = new ArrayList<>();
|
||||
Iterator<String> mit = pathItem.fieldNames();
|
||||
while (mit.hasNext()) {
|
||||
methods.add(mit.next());
|
||||
}
|
||||
for (String mk : methods) {
|
||||
JsonNode opNode = pathItem.get(mk);
|
||||
if (opNode != null && opNode.isObject()) {
|
||||
ObjectNode op = (ObjectNode) opNode;
|
||||
op.put("operationId", eaiSvcName);
|
||||
op.put("summary", apiName);
|
||||
ArrayNode opTags = objectMapper.createArrayNode();
|
||||
opTags.add(tagName);
|
||||
op.set("tags", opTags);
|
||||
// 예제(요청/응답 본문)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치. 여기선 임베드하지 않음.
|
||||
markGwOnOperation(op);
|
||||
addResponseHeader(op, "200", "Content-Type", contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
String newKey = adapterPath.isEmpty() ? pk
|
||||
: (adapterPath.replaceAll("/+$", "") + (pk.startsWith("/") ? "" : "/") + pk);
|
||||
if (!newKey.equals(pk)) {
|
||||
paths.set(newKey, pathItem);
|
||||
paths.remove(pk);
|
||||
}
|
||||
}
|
||||
}
|
||||
return objectMapper.writeValueAsString(root);
|
||||
} catch (Exception e) {
|
||||
logger.error("applyAutoRules 실패, 원본 spec 유지", e);
|
||||
return specJson;
|
||||
}
|
||||
}
|
||||
|
||||
private void setContentExample(ObjectNode op, String sampleJson) {
|
||||
if (StringUtils.isBlank(sampleJson)) {
|
||||
return;
|
||||
}
|
||||
JsonNode body = op.get("requestBody");
|
||||
if (body == null || !body.isObject()) {
|
||||
return;
|
||||
}
|
||||
JsonNode content = body.get("content");
|
||||
if (content == null || !content.isObject()) {
|
||||
return;
|
||||
}
|
||||
Iterator<String> mts = content.fieldNames();
|
||||
if (!mts.hasNext()) {
|
||||
return;
|
||||
}
|
||||
JsonNode mtNode = content.get(mts.next());
|
||||
if (mtNode != null && mtNode.isObject()) {
|
||||
((ObjectNode) mtNode).set("example", parseJsonOrText(sampleJson));
|
||||
}
|
||||
}
|
||||
|
||||
private void setResponseExample(ObjectNode op, String code, String sampleJson) {
|
||||
if (StringUtils.isBlank(sampleJson)) {
|
||||
return;
|
||||
}
|
||||
JsonNode resps = op.get("responses");
|
||||
if (resps == null || !resps.isObject()) {
|
||||
return;
|
||||
}
|
||||
JsonNode r = resps.get(code);
|
||||
if (r == null || !r.isObject()) {
|
||||
return;
|
||||
}
|
||||
ObjectNode content = getOrCreateObject((ObjectNode) r, "content");
|
||||
ObjectNode appjson = getOrCreateObject(content, "application/json");
|
||||
appjson.set("example", parseJsonOrText(sampleJson));
|
||||
}
|
||||
|
||||
/** GW 유래 스키마 필드에 x-djb-gw 마커 부여(프론트에서 잠금 표시). */
|
||||
private void markGwOnOperation(ObjectNode op) {
|
||||
JsonNode body = op.get("requestBody");
|
||||
if (body != null && body.isObject()) {
|
||||
markGwInContent(body.get("content"));
|
||||
}
|
||||
JsonNode resps = op.get("responses");
|
||||
if (resps != null && resps.isObject()) {
|
||||
List<String> codes = new ArrayList<>();
|
||||
Iterator<String> it = resps.fieldNames();
|
||||
while (it.hasNext()) {
|
||||
codes.add(it.next());
|
||||
}
|
||||
for (String c : codes) {
|
||||
JsonNode r = resps.get(c);
|
||||
if (r != null && r.isObject()) {
|
||||
markGwInContent(r.get("content"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void markGwInContent(JsonNode content) {
|
||||
if (content == null || !content.isObject()) {
|
||||
return;
|
||||
}
|
||||
List<String> mts = new ArrayList<>();
|
||||
Iterator<String> it = content.fieldNames();
|
||||
while (it.hasNext()) {
|
||||
mts.add(it.next());
|
||||
}
|
||||
for (String mt : mts) {
|
||||
JsonNode m = content.get(mt);
|
||||
if (m != null && m.isObject()) {
|
||||
markGwSchema(m.get("schema"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void markGwSchema(JsonNode schema) {
|
||||
if (schema == null || !schema.isObject()) {
|
||||
return;
|
||||
}
|
||||
ObjectNode s = (ObjectNode) schema;
|
||||
JsonNode props = s.get("properties");
|
||||
if (props != null && props.isObject()) {
|
||||
ArrayNode required = (s.get("required") != null && s.get("required").isArray())
|
||||
? (ArrayNode) s.get("required") : objectMapper.createArrayNode();
|
||||
java.util.Set<String> existing = new java.util.HashSet<>();
|
||||
required.forEach(n -> existing.add(n.asText()));
|
||||
List<String> keys = new ArrayList<>();
|
||||
Iterator<String> it = props.fieldNames();
|
||||
while (it.hasNext()) {
|
||||
keys.add(it.next());
|
||||
}
|
||||
for (String k : keys) {
|
||||
JsonNode p = props.get(k);
|
||||
if (p != null && p.isObject()) {
|
||||
((ObjectNode) p).put("x-djb-gw", true);
|
||||
if (!existing.contains(k)) { // GW 필드는 기본 '필수'
|
||||
required.add(k);
|
||||
existing.add(k);
|
||||
}
|
||||
markGwSchema(p);
|
||||
}
|
||||
}
|
||||
if (required.size() > 0) {
|
||||
s.set("required", required);
|
||||
}
|
||||
}
|
||||
JsonNode items = s.get("items");
|
||||
if (items != null && items.isObject()) {
|
||||
markGwSchema(items);
|
||||
}
|
||||
}
|
||||
|
||||
/** 응답 헤더 미리 등록(중복 시 스킵). */
|
||||
private void addResponseHeader(ObjectNode op, String code, String headerName, String example) {
|
||||
JsonNode resps = op.get("responses");
|
||||
if (resps == null || !resps.isObject()) {
|
||||
return;
|
||||
}
|
||||
JsonNode r = resps.get(code);
|
||||
if (r == null || !r.isObject()) {
|
||||
return;
|
||||
}
|
||||
ObjectNode headers = getOrCreateObject((ObjectNode) r, "headers");
|
||||
if (headers.has(headerName)) {
|
||||
return;
|
||||
}
|
||||
ObjectNode h = objectMapper.createObjectNode();
|
||||
h.put("description", headerName);
|
||||
ObjectNode sc = objectMapper.createObjectNode();
|
||||
sc.put("type", "string");
|
||||
h.set("schema", sc);
|
||||
h.put("example", example);
|
||||
headers.set(headerName, h);
|
||||
}
|
||||
|
||||
private JsonNode parseJsonOrText(String s) {
|
||||
try {
|
||||
return objectMapper.readTree(s);
|
||||
} catch (Exception e) {
|
||||
return TextNode.valueOf(s);
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectNode getOrCreateObject(ObjectNode parent, String field) {
|
||||
JsonNode node = parent.get(field);
|
||||
if (node != null && node.isObject()) {
|
||||
return (ObjectNode) node;
|
||||
}
|
||||
ObjectNode created = objectMapper.createObjectNode();
|
||||
parent.set(field, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private void putIfNotNull(Map<String, String> map, String key, String value) {
|
||||
if (value != null) {
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutItemUI;
|
||||
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutUI;
|
||||
|
||||
/**
|
||||
* D9 레이아웃 머지: GW LayoutUI 를 마법사 그리드용 필드 목록으로 변환한다.
|
||||
*
|
||||
* <p>GW 에서 온 필드는 {@code sourceType=GW}(항목명/데이터타입 잠금)로 표기하고, 예제값은
|
||||
* {@link DjbApiSpecSampleGenerator} 규칙으로 자동 채운다. 저장된 spec 의 부가정보(설명/예제/제약)
|
||||
* 병합은 클라이언트(그리드)에서 사용자 편집분과 합치므로, 서버는 GW 원본 필드 + 자동 예제까지 제공한다.
|
||||
*/
|
||||
@Service
|
||||
public class DjbApiSpecLayoutMergeService {
|
||||
|
||||
@Autowired
|
||||
private DjbApiSpecSampleGenerator sampleGenerator;
|
||||
|
||||
/** LayoutUI → 그리드 필드 행 목록(flat, depth/parentIndex 로 트리 복원 가능). */
|
||||
public List<Map<String, Object>> toFields(LayoutUI layout, String layoutType) {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
if (layout == null || layout.getLayoutItems() == null) {
|
||||
return rows;
|
||||
}
|
||||
for (LayoutItemUI it : layout.getLayoutItems()) {
|
||||
// serno 0 은 root 컨테이너 → 스킵 (ApiSpecManService.generateSpecTableRows 규칙과 동일)
|
||||
if (it.getLoutItemSerno() != null && it.getLoutItemSerno() == 0) {
|
||||
continue;
|
||||
}
|
||||
String dataType = it.getLoutItemDataType();
|
||||
int len = it.getLoutItemLength();
|
||||
int dec = it.getLoutItemDecimal();
|
||||
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
r.put("serno", it.getLoutItemSerno());
|
||||
r.put("name", it.getLoutItemName());
|
||||
r.put("desc", it.getLoutItemDesc());
|
||||
r.put("itemType", it.getLoutItemType()); // FIELD / GROUP / GRID
|
||||
r.put("dataType", dataType);
|
||||
r.put("category", sampleGenerator.categoryOf(dataType));
|
||||
r.put("length", len);
|
||||
r.put("decimal", dec);
|
||||
r.put("depth", it.getLoutItemDepth());
|
||||
r.put("parentIndex", it.getParentLoutItemIndex());
|
||||
r.put("required", false);
|
||||
r.put("sourceType", "GW");
|
||||
r.put("example", sampleGenerator.exampleFor(dataType, it.getLoutItemName(), len, dec));
|
||||
rows.add(r);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* D10 예제값 자동 생성 규칙.
|
||||
* <ul>
|
||||
* <li>string → {@code sample_} + 항목명. 최대길이 초과 시 left-cut(앞에서 자름).</li>
|
||||
* <li>integer → {@code 1}</li>
|
||||
* <li>number → {@code 1.1} (소수 자릿수 지정 시 {@code 1.1000} 형태)</li>
|
||||
* <li>boolean → {@code false}</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Service
|
||||
public class DjbApiSpecSampleGenerator {
|
||||
|
||||
/** 원시 데이터타입(레이아웃 값)을 OpenAPI 계열 카테고리로 분류. */
|
||||
public String categoryOf(String rawDataType) {
|
||||
if (rawDataType == null) {
|
||||
return "string";
|
||||
}
|
||||
String t = rawDataType.trim().toLowerCase();
|
||||
if (t.contains("bool")) {
|
||||
return "boolean";
|
||||
}
|
||||
if (t.contains("decimal") || t.contains("double") || t.contains("float") || t.contains("number")) {
|
||||
return "number";
|
||||
}
|
||||
if (t.contains("int") || t.contains("long")) {
|
||||
return "integer";
|
||||
}
|
||||
if (t.contains("object") || t.contains("group")) {
|
||||
return "object";
|
||||
}
|
||||
if (t.contains("array") || t.contains("grid") || t.contains("list")) {
|
||||
return "array";
|
||||
}
|
||||
return "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* 예제값 생성.
|
||||
* @param rawDataType 레이아웃 데이터타입(원시)
|
||||
* @param fieldName 항목명
|
||||
* @param maxLen 최대 길이(0/음수면 무제한)
|
||||
* @param decimalLen 소수 자릿수(0이면 미적용)
|
||||
*/
|
||||
public String exampleFor(String rawDataType, String fieldName, int maxLen, int decimalLen) {
|
||||
switch (categoryOf(rawDataType)) {
|
||||
case "integer":
|
||||
return "1";
|
||||
case "number":
|
||||
if (decimalLen > 0) {
|
||||
StringBuilder sb = new StringBuilder("1.");
|
||||
for (int i = 0; i < decimalLen; i++) {
|
||||
sb.append(i == 0 ? '1' : '0');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
return "1.1";
|
||||
case "boolean":
|
||||
return "false";
|
||||
case "object":
|
||||
case "array":
|
||||
return ""; // 컨테이너는 예제값 없음(자식으로 표현)
|
||||
default:
|
||||
String v = "sample_" + StringUtils.defaultString(fieldName);
|
||||
if (maxLen > 0 && v.length() > maxLen) {
|
||||
v = v.substring(0, maxLen); // left-cut
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
/**
|
||||
* 생성/저장된 OpenAPI(3.0) spec JSON 에 인증 체계(components.securitySchemes + 글로벌 security)를 주입한다.
|
||||
*
|
||||
* <p>포털 {@code DjbSwaggerSpecEnricher} 와 동일 모델링:
|
||||
* 게이트웨이가 토큰/키를 표준 Authorization 이 아니라 커스텀 헤더로 받으므로 OAuth·API_KEY 모두
|
||||
* <b>apiKey-in-header</b> 스킴으로 표현(헤더명만 상이).
|
||||
*
|
||||
* <p>authtype(TSEAIHE01.AUTHTYPE, 소문자 저장) 매핑:
|
||||
* {@code oauth/oauth2/ca → OAUTH}, {@code api_key/apikey → API_KEY}, 그 외 → 주입 없음.
|
||||
* 원본 트리를 보존하며 조작한다(문자열 치환 금지). 매핑 불가/빈 스펙/파싱 실패 시 원본 유지(멱등).
|
||||
*/
|
||||
@Service
|
||||
public class DjbApiSpecSwaggerEnricher {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public static final String OAUTH_SCHEME = "djbOAuth";
|
||||
public static final String API_KEY_SCHEME = "djbApiKey";
|
||||
// 게이트웨이 헤더명 (포털 DjbTestbedGatewayProperty 기본값과 동일)
|
||||
public static final String OAUTH_HEADER = "X-AUTH-TOKEN";
|
||||
public static final String API_KEY_HEADER = "X-DJB-API-Key";
|
||||
|
||||
/** authtype → 스킴명. 매핑 불가면 null. */
|
||||
public String schemeNameOf(String authtype) {
|
||||
if (authtype == null) {
|
||||
return null;
|
||||
}
|
||||
switch (authtype.trim().toLowerCase()) {
|
||||
case "oauth":
|
||||
case "oauth2":
|
||||
case "ca":
|
||||
return OAUTH_SCHEME;
|
||||
case "api_key":
|
||||
case "apikey":
|
||||
return API_KEY_SCHEME;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* spec JSON 에 authtype 기반 securityScheme 주입. 매핑 불가/빈 스펙/파싱 실패 시 원본 그대로 반환.
|
||||
*/
|
||||
public String enrich(String specJson, String authtype) {
|
||||
String schemeName = schemeNameOf(authtype);
|
||||
if (StringUtils.isBlank(specJson) || schemeName == null) {
|
||||
return specJson;
|
||||
}
|
||||
try {
|
||||
JsonNode parsed = objectMapper.readTree(specJson);
|
||||
if (!parsed.isObject()) {
|
||||
return specJson;
|
||||
}
|
||||
ObjectNode root = (ObjectNode) parsed;
|
||||
|
||||
ObjectNode scheme = objectMapper.createObjectNode();
|
||||
scheme.put("type", "apiKey");
|
||||
scheme.put("in", "header");
|
||||
scheme.put("name", OAUTH_SCHEME.equals(schemeName) ? OAUTH_HEADER : API_KEY_HEADER);
|
||||
|
||||
boolean swagger2 = root.has("swagger") && !root.has("openapi");
|
||||
if (swagger2) {
|
||||
getOrCreateObject(root, "securityDefinitions").set(schemeName, scheme);
|
||||
} else {
|
||||
ObjectNode components = getOrCreateObject(root, "components");
|
||||
getOrCreateObject(components, "securitySchemes").set(schemeName, scheme);
|
||||
}
|
||||
|
||||
// 글로벌 security 요구사항 (apiKey 스킴은 빈 스코프 배열)
|
||||
ArrayNode security = objectMapper.createArrayNode();
|
||||
ObjectNode requirement = objectMapper.createObjectNode();
|
||||
requirement.set(schemeName, objectMapper.createArrayNode());
|
||||
security.add(requirement);
|
||||
root.set("security", security);
|
||||
|
||||
return objectMapper.writeValueAsString(root);
|
||||
} catch (Exception e) {
|
||||
return specJson; // 파싱/직렬화 실패 → 원본 유지(멱등)
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectNode getOrCreateObject(ObjectNode parent, String field) {
|
||||
JsonNode node = parent.get(field);
|
||||
if (node != null && node.isObject()) {
|
||||
return (ObjectNode) node;
|
||||
}
|
||||
ObjectNode created = objectMapper.createObjectNode();
|
||||
parent.set(field, created);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<configuration scan="true" scanPeriod="10 seconds">
|
||||
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
|
||||
|
||||
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
|
||||
<property name="PREFIX" value="${inst.Type:-ems}" />
|
||||
<property name="LOG_HOME" value="/Users/rinjae/eactive/djb-eapim/djb-eapim/eapim-admin/logs" />
|
||||
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
|
||||
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-DEBUG}" />
|
||||
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="STDOUT"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_stdout.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_stdout.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="ACCESS_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_access.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_access.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="SMS_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/sms.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/sms.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="QUERY_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_query.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_query.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="DYNAMIC_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_dynamic.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_dynamic.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="HIBERNATE_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_hibernate.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/${PREFIX}_hibernate.log-%d{yyyy-MM-dd}.%i</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="HOTSWAP_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_hotswap.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/${PREFIX}_hotswap.log-%d{yyyy-MM-dd}.%i</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="com.eactive.eai.rms.onl.server" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.onl.dashboard" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.ext.djb.job.ApiStatusMonitorJob" level="ERROR" />
|
||||
|
||||
<logger name="org.springframework.orm.jpa.JpaTransactionManager" level="ERROR" additivity="false" />
|
||||
|
||||
<logger name="org.springframework" level="INFO" />
|
||||
<logger name="net.sf.ehcache" level="INFO" />
|
||||
|
||||
|
||||
<logger name="ACCESS_LOGGER" level="DEBUG" >
|
||||
<appender-ref ref="ACCESS_APPENDER" />
|
||||
</logger>
|
||||
<logger name="SMS_LOGGER" level="DEBUG" >
|
||||
<appender-ref ref="SMS_APPENDER" />
|
||||
</logger>
|
||||
<logger name="DYNAMIC_LOGGER" level="DEBUG" >
|
||||
<appender-ref ref="DYNAMIC_APPENDER" />
|
||||
</logger>
|
||||
|
||||
|
||||
<!-- QUERY_APPENDER start -->
|
||||
<!-- Hibernate SQL & 파라미터 바인딩 -->
|
||||
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="TRACE" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<!-- Hibernate 일반 로그 (DDL/init/connection 등) -->
|
||||
<logger name="org.hibernate" level="INFO" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
<appender-ref ref="STDOUT" />
|
||||
</logger>
|
||||
|
||||
<!-- iBATIS 2.3.4 (commons-logging 또는 log4j-over-slf4j 통해 SLF4J 도달) -->
|
||||
<logger name="com.ibatis" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
</logger>
|
||||
<logger name="com.eactive.eai.rms.common.base.SchemaIdConfiguredSqlMapTemplate" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
</logger>
|
||||
<!-- QUERY_APPENDER end -->
|
||||
|
||||
<!-- HotswapAgent는 자체 logger를 사용하므로 logback에 안 잡힘. hotswap-agent.properties의 LOGFILE로 직접 파일 출력 -->
|
||||
|
||||
<root>
|
||||
<level value="INFO" />
|
||||
<appender-ref ref="STDOUT" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -2490,6 +2490,7 @@ deployResourceSearchMan.deleteBtn=delete
|
||||
|
||||
auditLogMan.command = Command
|
||||
auditLogMan.logMsg = Message
|
||||
auditLogMan.message = Reason
|
||||
auditLogMan.logSeqNo = Number
|
||||
auditLogMan.logSubMsg = Sub Message
|
||||
auditLogMan.logType.access = ACCESS
|
||||
|
||||
@@ -1674,6 +1674,7 @@ screen.lastLogin = Last LoginTime
|
||||
screen.logout = Logout
|
||||
screen.logoutMsg = Are you sure you want to log out of EMS System?
|
||||
screen.title = EAI Monitoring System
|
||||
screen.sitemap = Sitemap
|
||||
|
||||
search.period = Inquiry Period
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@ apiScpMan.title = API-SCOPE \uB9F5\uD551
|
||||
|
||||
auditLogMan.command = Command
|
||||
auditLogMan.logMsg = \uBA54\uC2DC\uC9C0
|
||||
auditLogMan.message = \uC0AC\uC720
|
||||
auditLogMan.logSeqNo = \uBC88\uD638
|
||||
auditLogMan.logSubMsg = \uC11C\uBE0C \uBA54\uC2DC\uC9C0
|
||||
auditLogMan.logType.access = \uC811\uC18D
|
||||
@@ -1770,6 +1771,7 @@ screen.lastLogin = \uB9C8\uC9C0\uB9C9 \uB85C\uADF8\uC778
|
||||
screen.logout = \uB85C\uADF8\uC544\uC6C3
|
||||
screen.logoutMsg = EMS Monitoring System \uC5D0\uC11C \uB85C\uADF8\uC544\uC6C3 \uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C ?
|
||||
screen.title = \uC778\uD130\uD398\uC774\uC2A4 \uD1B5\uD569\uBAA8\uB2C8\uD130\uB9C1
|
||||
screen.sitemap = \uC0AC\uC774\uD2B8\uB9F5
|
||||
|
||||
search.period = \uC870\uD68C\uAE30\uAC04
|
||||
|
||||
|
||||
Reference in New Issue
Block a user