30 Commits

Author SHA1 Message Date
eastargh 481e82fec7 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-07-10 11:37:43 +09:00
eastargh 1b382f5859 ums 설정 추가 2026-07-10 11:37:33 +09:00
Rinjae 608cb4188e "Jenkinsfile 개선:
eapim-admin CI / build (push) Has been cancelled
- Tomcat 배포 구조 → WebLogic 기반으로 변경
- stage별 agent 명시 및 불필요한 환경변수 제거
- Webhook 알림 로직 추가"
2026-07-09 16:20:54 +09:00
eastargh 534947637d qna 상세 회원이름 표시
eapim-admin CI / build (push) Has been cancelled
2026-07-09 14:55:07 +09:00
eastargh 153c68c6ab 사이트맵 위치 변경
eapim-admin CI / build (push) Has been cancelled
2026-07-09 11:47:23 +09:00
eastargh 80ef41f4fc QnA 문의검토중 상태 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-09 09:26:24 +09:00
Rinjae 8aebabe005 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-08 18:31:45 +09:00
Rinjae 6d33b69bed 인증 서버 클라이언트 차단 API 추가:
- `ClientBlockApiController` 신규 생성 및 차단 로직 구현
- `ClientManService`에 클라이언트 차단 메서드 추가
- 승인 요청 삭제 로직 개발 환경 제한 문구 수정
2026-07-08 18:31:39 +09:00
eastargh 49ebdce4c3 전문레이아웃 그리드 항목명 순서 변경(김태호이사 요청)
eapim-admin CI / build (push) Has been cancelled
2026-07-08 11:13:43 +09:00
eastargh 979e43e382 - 포함된 인터페이스명 복제 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-07-08 10:37:03 +09:00
eastargh 53b075967c 스타일시트 정리
eapim-admin CI / build (push) Has been cancelled
2026-07-07 16:53:22 +09:00
eastargh 724117860b 탑메뉴 오버레이 높이 조정
eapim-admin CI / build (push) Has been cancelled
2026-07-07 14:47:02 +09:00
eastargh 0ee52392aa 조회기간 기본설정 변경
eapim-admin CI / build (push) Has been cancelled
2026-07-07 14:09:58 +09:00
eastargh f0af06da88 사이트맵
eapim-admin CI / build (push) Has been cancelled
2026-07-07 13:38:44 +09:00
eastargh b16d03517a 사이트맵 신규 개발
eapim-admin CI / build (push) Has been cancelled
2026-07-07 13:19:33 +09:00
Rinjae edf024f0a3 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-07 10:35:26 +09:00
Rinjae a1a1f27498 승인 관리: 승인 상태별 행 배경색 강조 로직 추가 2026-07-07 10:34:41 +09:00
eastargh c62f395983 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-07-06 16:34:55 +09:00
eastargh c3955cefd8 Scope관리 Api Reload 버그 수정 2026-07-06 16:26:40 +09:00
Rinjae 5c378eac0d Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-07-06 15:33:39 +09:00
Rinjae e51cdbc42a 감사로그 및 삭제 프로세스 개선:
- 감사 사유 입력 기능 추가 및 UI 수정
- 사용/법인 삭제 시 '완전삭제' 여부 확인 및 로직 적용
- 감사로그에 사유(message) 저장 필드 추가
2026-07-06 15:33:32 +09:00
eastargh 142d402bad 레이아웃관리 - Array 아이템유형 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-06 11:25:16 +09:00
eastargh f23cb02b1c 웹훅 발송 내역 2026-07-03 17:15:57 +09:00
eastargh eca120812c 관리자포탈 비밀번호 규칙 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-02 16:34:45 +09:00
eastargh 71b0fc7009 오류변환만 있는 경우, 전문변환 ON으로 설정 2026-07-02 16:34:20 +09:00
eastargh 7b12ef2fb5 웹훅 발송 내역 신규
eapim-admin CI / build (push) Has been cancelled
2026-07-02 14:46:02 +09:00
eastargh 292e27e66e 에러메세지 수정
eapim-admin CI / build (push) Has been cancelled
2026-07-02 13:28:31 +09:00
eastargh 61d710dd8c 초기비밀번호로 로그인시 비밀번호변경 모달 오픈
eapim-admin CI / build (push) Has been cancelled
2026-07-01 14:23:55 +09:00
eastargh 8494758fcd 신규 비밀번호 및 초기화시 사번+@! 로 설정
eapim-admin CI / build (push) Has been cancelled
2026-07-01 10:27:18 +09:00
eastargh f2b25d2775 API명 검색조건 sql에 추가
eapim-admin CI / build (push) Has been cancelled
2026-07-01 10:06:56 +09:00
67 changed files with 1979 additions and 436 deletions
Vendored
+142 -148
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 */
+9 -9
View File
@@ -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:'';}
@@ -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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
}
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>
+82
View File
@@ -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 모달 스타일 -->
+45 -29
View File
@@ -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>
+27 -5
View File
@@ -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;
@@ -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);
}
});
}
});
});
@@ -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, '&quot;'); }
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();
@@ -486,6 +486,7 @@
}
if(data.errResponseTransform != null && data.errResponseTransform != '' && data.errTransformYn == 'Y') {
$('#toggleTransformYn').bootstrapToggle('on');
$('#toggleErrTransformYn').bootstrapToggle('on');
$('#rowErrResponseTransform').show();
} else {
@@ -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();
}
}
@@ -14,6 +14,8 @@ import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class MessageRequestService
@@ -53,6 +55,16 @@ public class MessageRequestService
return repository.findAll(predicate, fixed);
}
/** 특정 일자에 SMS/KAKAO 로 발송 요청된 건수 (UMS 일련번호 채번용). */
public long countSmsKakaoRequestsOnDate(LocalDate date) {
QMessageRequest q = QMessageRequest.messageRequest;
BooleanBuilder predicate = new BooleanBuilder();
predicate.and(q.messageType.in("SMS", "KAKAO"));
predicate.and(q.requestDate.goe(date.atStartOfDay()));
predicate.and(q.requestDate.loe(date.atTime(23, 59, 59)));
return repository.count(predicate);
}
/** PENDING 상태인 건만 FAILED 로 전환한다. */
public void updateStatusToFailedIfPending(String id) {
MessageRequest entity = getById(id);
@@ -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()));
}
@@ -8,6 +8,7 @@ import java.net.URL;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -18,8 +19,10 @@ import org.springframework.stereotype.Service;
import com.eactive.apim.portal.common.util.PhoneNumberUtil;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.eai.common.util.StringUtil;
import com.eactive.eai.common.util.SystemUtil;
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
import com.eactive.eai.rms.data.entity.onl.apim.messagerequest.MessageRequestService;
import com.eactive.eai.rms.ext.djb.ums.vo.EmailDataVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SmsDataVO;
import com.eactive.eai.rms.ext.djb.ums.vo.StdHeadVO;
@@ -39,12 +42,15 @@ public class UmsService {
private static final String SYS_DVCD = "API"; //시스템코드
private static final String BZWK_DVCD = "99"; //업무구분코드
private static final String SUB_BZWK_CD = "9019"; //세부구분코드
private static final String TEAM_DVCD = "00"; //팀구분코드 TODO: 변경해야함
private final MonitoringPropertyService propertyService;
private final MessageRequestService messageRequestService;
public UmsService(MonitoringPropertyService propertyService) {
public UmsService(MonitoringPropertyService propertyService, MessageRequestService messageRequestService) {
this.propertyService = propertyService;
this.messageRequestService = messageRequestService;
}
public boolean send(MessageRequest messageRequest) throws Exception {
@@ -168,17 +174,16 @@ public class UmsService {
// 스윙챗 알림 데이터
SwingDataVO swiDataVO = new SwingDataVO();
swiDataVO.setNotiCode("CNO_FEP_HMP_0002");
swiDataVO.setNotiCode("CNO_FEP_APIM_0002"); //0001:긴급, 0002:중요
swiDataVO.setNotiTitle("알림");
swiDataVO.setNotiContent(messageRequest.getMessage());
// 스윙챗 알림 데이터
List<SwingReceiverDataVO> swiReDataVOList = new ArrayList<SwingReceiverDataVO>();
SwingReceiverDataVO receiver = new SwingReceiverDataVO();
receiver.setType("");
receiver.setType("PSN");
receiver.setCompanyCode("CB");
receiver.setCode(messageRequest.getMessengerId());
receiver.setPrdNm("");
receiver.setCode(messageRequest.getMessengerId());
swiReDataVOList.add(receiver);
swiDataVO.setReceiverInfo(swiReDataVOList);
@@ -200,22 +205,22 @@ public class UmsService {
// Sms 데이터
SmsDataVO dataVO = new SmsDataVO();
dataVO.setSnd_dman_dt(jsonData[0]);
dataVO.setSnd_dman_id("");
dataVO.setSnd_dman_id(this.getSndDmanId(messageRequest.getMessageType()));
dataVO.setSnd_dvcd("EAI");
dataVO.setMsg_titl("");
dataVO.setMsg_titl(StringUtil.chunkString(messageRequest.getSubject(), 40, false)); // MS949 40byte 초과시 절단
dataVO.setMsg_ctnt(messageRequest.getMessage());
dataVO.setRecvr_no(PhoneNumberUtil.digitsOnly(messageRequest.getPhone()));
dataVO.setSndn_no("15880079"); //발신번호
dataVO.setSnd_empno(""); //요청자
dataVO.setSnd_emp_dprm_cd(""); //요청부서코드
dataVO.setSnd_empno("APIM"); //요청자
dataVO.setSnd_emp_dprm_cd("132"); //요청부서코드
if ("SMS".equals(messageRequest.getMessageType())) {
dataVO.setLtrs_snd_tycd("1"); // 1:SMS 2:LMS/MMS
dataVO.setMsg_snd_tycd("0000"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
} else if ("KAKAO".equals(messageRequest.getMessageType())) {
dataVO.setLtrs_snd_tycd("2"); // 1:SMS 2:LMS/MMS
dataVO.setMsg_snd_tycd("1008"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
dataVO.setAlmtk_snd_prfl_key(""); // 플러스친구아이디
dataVO.setAlmtk_tmplt_cd(""); // 템플릿코드
dataVO.setAlmtk_snd_prfl_key(propertyService.getPropertyValue("Monitoring", "djb.ums.sms.almtk_snd_prtl_key", "")); // 플러스친구아이디
dataVO.setAlmtk_tmplt_cd(BZWK_DVCD + TEAM_DVCD + SUB_BZWK_CD + "001"); // 템플릿코드
dataVO.setAlmtk_err_ltrs_snd_yn("Y"); // 오류자문자발송여부
dataVO.setAlmtk_btn_trnm_tycd("2"); // 카카오버튼전송방식 1-format string 2-JSON 3-XML
}
@@ -256,8 +261,8 @@ public class UmsService {
dataVO.setRecvr_nm(messageRequest.getUsername()); // 수신자명(필수)
dataVO.setTmplt_mpng_info(messageRequest.getMessage()); // 탬플릿매핑정보
dataVO.setTmplt_mpng_rptt_info(""); // 탬플릿매핑반복정보
dataVO.setSnd_empno(""); // 발신자ID(필수)
dataVO.setSnd_emp_dprm_cd(""); // 발신자부서코드(필수)
dataVO.setSnd_empno("APIM"); // 발신자ID(필수)
dataVO.setSnd_emp_dprm_cd("132"); // 발신자부서코드(필수)
dataVO.setBzwk_cd(BZWK_DVCD); // 업무구분코드
dataVO.setSub_bzwk_cd(SUB_BZWK_CD); // 업무세부코드
dataVO.setSys_dvcd(SYS_DVCD);
@@ -334,4 +339,24 @@ public class UmsService {
}
return buffer.toString();
}
private String getSndDmanId(String messageType) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String yyyymmdd = sdf.format(new Date());
String uuid = SYS_DVCD + BZWK_DVCD + SUB_BZWK_CD + yyyymmdd + "O" ;
if ("SMS".equals(messageType)) {
uuid += "S";
} else if ("LMS".equals(messageType)) {
uuid += "L";
} else if ("KAKAO".equals(messageType)) {
uuid += "K";
}
long seq = messageRequestService.countSmsKakaoRequestsOnDate(LocalDate.now()) + 1;
uuid += String.format("%014d", seq);
return uuid;
}
}
@@ -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;
}
@@ -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 " +
@@ -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);
}
}
@@ -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));
@@ -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));
}
}
}
@@ -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;
}
@@ -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);
@@ -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);
@@ -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