47 Commits

Author SHA1 Message Date
eastargh b8beeb7c3e Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-06-22 13:40:24 +09:00
eastargh 0bb00b4fb9 프라퍼티 암호화 기능 추가 2026-06-22 13:40:13 +09:00
Rinjae 655c35b1e1 - 메시지 발송 내역 화면 추가: 조회/상세/상태변경(PENDING→FAILED)
eapim-admin CI / build (push) Has been cancelled
- 본문·수신자 마스킹 유틸(MessagePatternMaskingUtils) 개발
- 안전 HTML 생성 마스킹/검증 테스트 및 JSP 바인딩
2026-06-19 19:40:15 +09:00
Rinjae efd1c748ed Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-06-19 17:17:50 +09:00
Rinjae 64a9c7408e - 사용자 조회 시 방어적 로직 추가: 삭제된 사용자 처리
- PortalFaq/PortalInquiry 컨벤션 동일 적용
2026-06-19 17:17:45 +09:00
eastargh 8f9411f990 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
2026-06-19 13:43:52 +09:00
eastargh ed1f55c534 유량제어 서버버킷 조회 port 변경 2026-06-19 13:43:36 +09:00
Rinjae d1392821ef Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-06-19 11:23:13 +09:00
Rinjae 6542b0c0f8 - 메인 화면 serviceType 설정 수정: session/localStorage 분리
- emergency.jsp JSTL core 태그 추가
2026-06-19 11:23:07 +09:00
Rinjae-gf63 f7de1b6885 Merge remote-tracking branch 'origin/master'
eapim-admin CI / build (push) Has been cancelled
2026-06-18 17:58:26 +09:00
Rinjae-gf63 3e32220061 포탈 공지사항 관리 - 콤보 로드 후 그리드 생성으로 경합 방지 2026-06-18 17:57:37 +09:00
Rinjae 10db8129eb 마스킹 표준화 적용 (eapim-admin/portal/JS)
eapim-admin CI / build (push) Has been cancelled
- admin: elink-online-core 표준 위임(e메일/이름/전화/IP)으로 규칙 일원화
- portal/JS: 표준 로직 직접 구현 + 기존 owner 비마스킹 유지
- 호출처 무수정/인라인 중복 제거/테스트 통과
2026-06-18 17:56:32 +09:00
Rinjae e34829437d 피드백/개선요청 삭제 로직 수정
eapim-admin CI / build (push) Has been cancelled
- 삭제 버튼 UI 개선 및 상세 페이지 삭제 기능 추가
- audit log에 DELETE 이벤트 추가
2026-06-18 16:45:28 +09:00
Rinjae-gf63 ef3f0e802a - 사용자 ID 검증 로직 추가 (PortalUserService)
eapim-admin CI / build (push) Has been cancelled
- 사업제휴 관리 > 삭제/다중 삭제 기능 추가 및 화면 수정
- 피드백/개선 요청으로 명칭 변경 (사업제휴 관리)
2026-06-18 16:31:22 +09:00
rinjae b741a7e563 Jenkinsfile 업데이트
eapim-admin CI / build (push) Has been cancelled
2026-06-18 12:17:49 +09:00
rinjae e8069ef371 Jenkinsfile 업데이트
eapim-admin CI / build (push) Has been cancelled
2026-06-18 12:10:31 +09:00
rinjae c0e218451e Jenkinsfile 추가
eapim-admin CI / build (push) Has been cancelled
2026-06-18 11:42:30 +09:00
eastargh af63b1a256 배치잡 기본에서 클러스터로 수정시 next fire time =null인 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-06-18 11:05:26 +09:00
eastargh 02072df973 api사용현황 수정
eapim-admin CI / build (push) Has been cancelled
2026-06-18 09:17:58 +09:00
eastargh bc478ad4ed 배치잡 수정시 clustered에 무조건 추가되는 버그 수정
eapim-admin CI / build (push) Has been cancelled
2026-06-17 10:58:00 +09:00
Rinjae 161a68a0a2 - Tomcat readiness probe 타임아웃 180s → 300s 연장
eapim-admin CI / build (push) Has been cancelled
- readiness probe URL /monitoring/login.do → /monitoring/loginForm.do 수정
- readiness 실패 시 로그 출력 내용 및 포트 점검 추가
2026-06-16 18:46:28 +09:00
Rinjae-gf63 3b59f7acd7 Merge branch 'master' of ssh://172.30.1.50:2222/djb-eapim/eapim-admin
eapim-admin CI / build (push) Has been cancelled
2026-06-16 16:44:16 +09:00
Rinjae-gf63 1e29d1d721 Merge remote-tracking branch 'origin/master' into master
Resolve conflict in PortalNoticeManService.java import block:
union both sides — keep Arrays (HEAD) and LocalDateTime/ArrayList/Collections (origin/master),
all of which are used in the merged body.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 19:39:01 +09:00
eastargh ac0d081a9a 인터페이스 아웃바운드 path에 {} 포함시 variableUrlRequest로 설정 2026-06-12 10:18:39 +09:00
eastargh 164ab92919 BAP 서버 목록조회 쿼리에 rownum=1 제거 2026-06-12 09:27:08 +09:00
eastargh a6e7d1adb3 아웃바운드 URL restoption type 판단시 {}안에 .도 포함되도록 변경 2026-06-11 11:38:38 +09:00
77 changed files with 2670 additions and 250 deletions
+140
View File
@@ -0,0 +1,140 @@
name: eapim-admin CI
on:
push:
branches:
- master
- develop
- stage
- feats/ci-test
jobs:
build:
runs-on: [self-hosted, linux, eapim-admin]
env:
JAVA_HOME: /apps/opts/jdk8
JAVA_HOME_TOMCAT: /apps/opts/jdk17
CATALINA_BASE: /prod/eapim/adminportal
CATALINA_HOME: /prod/eapim/apache-tomcat-9.0.116
CATALINA_PID: /prod/eapim/adminportal/temp/catalina.pid
DEPLOY_HTTP_PORT: "39120"
defaults:
run:
working-directory: eapim-admin
steps:
- name: Checkout eapim-admin (trigger SHA)
uses: http://172.30.1.50:3000/actions/checkout@v4
with:
path: eapim-admin
- name: Checkout sibling modules (master)
working-directory: ${{ github.workspace }}
run: |
set -e
mkdir -p eapim-online
# eapim-online/* — 5 modules referenced from settings.gradle
for REPO in elink-online-core elink-online-core-jpa elink-online-transformer elink-online-common elink-online-emsclient; do
rm -rf "eapim-online/$REPO"
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/$REPO.git \
"eapim-online/$REPO"
done
# standalone siblings
for REPO in elink-portal-common eapim-admin-djb; do
rm -rf "$REPO"
git clone --depth=1 --branch master \
http://oauth2:${{ secrets.CI_TOKEN }}@172.30.1.50:3000/djb-eapim/$REPO.git \
"$REPO"
done
- name: Verify toolchain
run: |
java -version
gradle --version
- name: Gradle build (no tests)
run: gradle clean build -x test --no-daemon
- name: Upload WAR artifact
uses: http://172.30.1.50:3000/actions/upload-artifact@v3
with:
name: eapim-admin-war-${{ github.sha }}
path: eapim-admin/build/libs/eapim-admin.war
if-no-files-found: error
- name: Stop tomcat (pre-clean)
working-directory: ${{ github.workspace }}
run: |
# 1) systemd 관리 인스턴스 정지 (이번 워크플로가 띄운 것)
systemctl --user stop eapim-admin 2>/dev/null || true
# 2) systemd 외부 detached 인스턴스 정리
if [ -f "$CATALINA_PID" ] && kill -0 "$(cat $CATALINA_PID)" 2>/dev/null; then
JAVA_HOME=$JAVA_HOME_TOMCAT "$CATALINA_HOME/bin/shutdown.sh" 30 -force || true
for i in $(seq 1 30); do
[ -f "$CATALINA_PID" ] && kill -0 "$(cat $CATALINA_PID)" 2>/dev/null || break
sleep 1
done
fi
rm -f "$CATALINA_PID"
- name: Deploy war as /monitoring
working-directory: ${{ github.workspace }}
run: |
DEPLOY_DIR="$CATALINA_BASE/webapps"
rm -rf "$DEPLOY_DIR/monitoring" "$DEPLOY_DIR/monitoring.war"
cp eapim-admin/build/libs/eapim-admin.war "$DEPLOY_DIR/monitoring.war"
ls -la "$DEPLOY_DIR"
- name: Start tomcat and readiness probe (timeout 300s)
working-directory: ${{ github.workspace }}
run: |
LOG="$CATALINA_BASE/logs/catalina.$(date +%Y-%m-%d).log"
BEFORE=0
[ -f "$LOG" ] && BEFORE=$(stat -c %s "$LOG")
# systemd 가 Tomcat 을 새 cgroup 으로 띄움 — runner step 종료와 무관하게 살아남음
systemctl --user start eapim-admin
# /loginForm.do 는 GET 매핑(MainController#loginForm) → tomcat·webapp 양쪽 살아있으면 200
# /login.do 는 POST 처리 — GET 으로 probe 시 405/잘못된 응답 가능성 있어 회피
PROBE_URL="http://localhost:$DEPLOY_HTTP_PORT/monitoring/loginForm.do"
DEADLINE=$(($(date +%s) + 300))
STATUS=000
while [ $(date +%s) -lt $DEADLINE ]; do
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
"$PROBE_URL" 2>/dev/null || echo "000")
case "$STATUS" in
200|302|401)
echo "Readiness OK ($PROBE_URL HTTP $STATUS)"
break
;;
esac
if [ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | grep -qE "Application listener .* Exception|SEVERE.*startup.Catalina"; then
echo "::error::Startup error detected in log"
tail -c +$((BEFORE+1)) "$LOG" | tail -80
exit 1
fi
sleep 2
done
case "$STATUS" in
200|302|401) ;;
*)
echo "::error::Readiness probe failed within 300s (last HTTP status: $STATUS)"
echo "--- last 300 lines of catalina log ---"
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -300
echo "--- systemd unit status ---"
systemctl --user status eapim-admin --no-pager || true
echo "--- listening ports ---"
ss -tlnp 2>/dev/null | grep -E ":$DEPLOY_HTTP_PORT\b" || echo "(port $DEPLOY_HTTP_PORT 미점유)"
exit 1
;;
esac
echo "--- key boot log lines ---"
tail -c +$((BEFORE+1)) "$LOG" | grep -E "Server startup|Deployment of web" | head -10 || true
Vendored
+179
View File
@@ -0,0 +1,179 @@
pipeline {
agent { label 'djb-vm' }
options {
timestamps()
disableConcurrentBuilds()
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'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Checkout dependencies') {
steps {
sh '''
set -eu
cd "$WORKSPACE/.."
mkdir -p eapim-online
for REPO in elink-online-core elink-online-core-jpa elink-online-transformer elink-online-common elink-online-emsclient; do
TARGET="eapim-online/$REPO"
if [ ! -d "$TARGET/.git" ]; then
rm -rf "$TARGET"
git clone --depth=1 --branch master \
"ssh://git@172.30.1.50:2222/djb-eapim/$REPO.git" \
"$TARGET"
else
git -C "$TARGET" fetch --depth=1 origin master
git -C "$TARGET" reset --hard origin/master
git -C "$TARGET" clean -fdx
fi
done
for REPO in elink-portal-common eapim-admin-djb; do
if [ ! -d "$REPO/.git" ]; then
rm -rf "$REPO"
git clone --depth=1 --branch master \
"ssh://git@172.30.1.50:2222/djb-eapim/$REPO.git" \
"$REPO"
else
git -C "$REPO" fetch --depth=1 origin master
git -C "$REPO" reset --hard origin/master
git -C "$REPO" clean -fdx
fi
done
'''
}
}
stage('Verify toolchain') {
steps {
sh '''
set -eu
java -version
gradle --version
'''
}
}
stage('Build WAR') {
steps {
sh 'gradle clean build -x test --no-daemon'
}
post {
success {
sh '''
set -eu
cd build/libs
sha1sum eapim-admin.war > SHA1SUMS
sha256sum eapim-admin.war > SHA256SUMS
'''
archiveArtifacts artifacts: 'build/libs/eapim-admin.war,build/libs/SHA1SUMS,build/libs/SHA256SUMS', fingerprint: true
}
}
}
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 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"
'''
}
}
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
'''
}
}
}
}
@@ -115,7 +115,7 @@
</bean>
<bean id="ioAcceptor" class="com.eactive.eai.rms.onl.server.UdpServer" init-method="init" destroy-method="destory">
<property name="port" value="10800" />
<property name="port" value="39126" />
<property name="handler" ref="handler"/>
</bean>
+3 -2
View File
@@ -12,6 +12,7 @@ PortalTermsManController_약관관리_APIGW_INSERT,UPDATE,DELETE
ProdClientManController_운영Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE
ApiSpecController_API스펙관리_APIGW_INSERT,DELETE
MessageTemplateManController_메세지템플릿관리_APIGW_INSERT,UPDATE,DELETE
PortalPartnershipManController_사업제휴신청관리_APIGW_UNMASK
PortalPartnershipManController_피드백/개선요청관리_APIGW_UNMASK,DELETE
PortalPropertyController_포탈 프로퍼티 관리_APIGW_UPDATE
PortalUserTermsManController_약관동의 이력_APIGW_UNMASK
PortalUserTermsManController_약관동의 이력_APIGW_UNMASK
MessageRequestManController_메세지발송내역_APIGW_UPDATE_STATUS
+1
View File
@@ -1,6 +1,7 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@page import="org.apache.commons.lang3.StringUtils"%>
<%@page import="com.eactive.eai.rms.common.login.SessionManager"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
+2 -2
View File
@@ -353,10 +353,10 @@
</div>
<!-- SSO 로그인 버튼 (비상모드 시 숨김) -->
<c:if test="${not emergencyMode}">
<div class="form-group">
<!-- <div class="form-group">
<input type="button" id="btn_sso_login" class="form-control btn btn-success rounded submit px-3"
value="SSO Login">
</div>
</div> -->
</c:if>
<p style="color:red;"><%=resultMsg %></p>
</form>
+17 -7
View File
@@ -52,6 +52,13 @@
var urlParam = window.document.location.search;
var mod = "";
var strServiceType = "<%=request.getParameter("serviceType")%>";
function getServiceType() {
var serviceType = sessionStorage.getItem("serviceType");
if (!serviceType || serviceType == "undefined" || serviceType == "null") {
serviceType = localStorage.getItem("serviceType") || "";
}
return serviceType;
}
if(urlParam != ""){
if(urlParam.indexOf("mod=") > -1){
mod = urlParam.substring((urlParam.indexOf("mod=") + 4), urlParam.lastIndexOf("&"));
@@ -67,15 +74,18 @@
}
}
$(document).ready(function() {
$(document).ready(function() {
var url = '<%=topPage%>';
if (url.indexOf("?")>=0){
url = url + "&serviceType="+ sessionStorage["serviceType"];
}else{
url = url + "?serviceType="+ sessionStorage["serviceType"];
var serviceType = getServiceType();
if (serviceType) {
if (url.indexOf("?")>=0){
url = url + "&serviceType="+ serviceType;
}else{
url = url + "?serviceType="+ serviceType;
}
}
$("#topFrame").attr('src',url);
$("#topFrame").attr('src',url);
$(".topMenu").load(function(){ // iframe이 모두 load된후 제어
$(".topMenu").contents().find('.gnb').on("mouseenter", function(e){
+2 -2
View File
@@ -551,9 +551,9 @@
</h1><!-- /monitoring/images/top_logo.png -->
<div class="topmenu_box">
<ul>
<li style="width:240px;" id="newDashboardShow">
<li style="width:240px;" id="newDashboardShow">
<a style="width:240px;"
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>)</span>
href="#"><span><%=SessionManager.getUserName(request) %>(<%=SessionManager.getUserId(request) %>-<%=System.getProperty("inst.Name") %>)</span>
<span onClick="javascript:openColorPopup();"><%=localeMessage.getString("screen.customer") %></span>
<% if (!"".equals(LastLoginYms.trim())) { %>
<span style="display:block;font-size:10px;"><%=localeMessage.getString("screen.lastLogin") %> : <%=LastLoginYms%> [ <%=LastLoginIp%> ]</span>
+40 -23
View File
@@ -1,5 +1,10 @@
/**
* 문자열 마스킹 처리를 위한 유틸리티
*
* 보안아키텍처 표준 마스킹 규칙(Java MaskingUtils)과 동일한 결과를 내도록 구현한다.
* 이름 2자: 뒤 1자리(가나→가*) / 3자 이상: 앞뒤 제외 가운데 전체(가나다라→가**라)
* 이메일 아이디 앞2·뒤2 마스킹, 4자 이하 전체(test→****, test123→**st1**)
* 전화 2·3번째 세그먼트 각각 뒤 2자리(010-1234-1234→010-12**-12**)
*/
const StringMaskingUtil = {
@@ -8,41 +13,51 @@ const StringMaskingUtil = {
return str != null && str !== '';
},
// 이름 마스킹 처리
// '*' 반복 생성
_stars: function(count) {
return count > 0 ? '*'.repeat(count) : '';
},
// 세그먼트 뒤 count 자리 마스킹
_maskSegmentTail: function(segment, count) {
if (segment.length <= count) {
return this._stars(segment.length);
}
return segment.substring(0, segment.length - count) + this._stars(count);
},
// 이름 마스킹 처리 (2자: 뒤 1자리, 3자 이상: 앞뒤 제외 가운데 전체)
maskName: function(name) {
if (!this.isValidString(name)) {
return name;
}
return name.length > 1 ?
name.substring(0, name.length - 1) + "*" :
name;
const len = name.length;
if (len === 1) {
return name;
}
if (len === 2) {
return name.charAt(0) + '*';
}
return name.charAt(0) + this._stars(len - 2) + name.charAt(len - 1);
},
// 이메일 마스킹 처리
// 이메일 마스킹 처리 (아이디 앞2·뒤2, 4자 이하 전체)
maskEmail: function(email) {
if (!this.isValidString(email) || !email.includes('@')) {
return email;
}
const parts = email.split('@');
if (parts.length !== 2) {
return email;
const atIdx = email.indexOf('@');
const local = email.substring(0, atIdx);
const domain = email.substring(atIdx);
if (local.length <= 4) {
return this._stars(local.length) + domain;
}
const localPart = parts[0];
let maskedLocal;
if (localPart.length <= 3) {
maskedLocal = localPart.substring(0, 1) +
'*'.repeat(localPart.length - 1);
} else {
maskedLocal = localPart.substring(0, localPart.length - 3) + '***';
}
return maskedLocal + '@' + parts[1];
return this._stars(2) + local.substring(2, local.length - 2) + this._stars(2) + domain;
},
// 휴대폰 번호 마스킹 처리
// 휴대폰/전화 번호 마스킹 처리 (2·3번째 세그먼트 각각 뒤 2자리)
maskMobileNumber: function(number) {
if (!this.isValidString(number)) {
return number;
@@ -50,8 +65,10 @@ const StringMaskingUtil = {
const parts = number.split('-');
if (parts.length === 3) {
return parts[0] + '-****-' + parts[2];
return parts[0] + '-' +
this._maskSegmentTail(parts[1], 2) + '-' +
this._maskSegmentTail(parts[2], 2);
}
return number;
}
};
};
@@ -250,6 +250,29 @@ var url_view = '<c:url value="/onl/admin/common/propertyMan.view" />';
});
});
$("#btn_pop_encrypt").click(function() {
var prpty2Val = $("input[name=prpty2Val]").val();
if (prpty2Val == '') {
alert("프라퍼티 값을 입력하세요");
return;
}
var postData = {
cmd : "ENCRYPT",
prpty2Val : prpty2Val,
};
$.ajax({
type : "POST",
url : url,
data : postData,
success : function(args) {
$("input[name=prpty2Val]").val(args);
},
error : function(e) {
alert(e.responseText);
}
});
});
buttonControl(key);
titleControl(key);
@@ -289,12 +312,17 @@ var url_view = '<c:url value="/onl/admin/common/propertyMan.view" />';
<table class="table_row" cellspacing="0">
<tr>
<th style="width:20%;"><%= localeMessage.getString("propertyDetail.propertyKey") %></th>
<td><input type="text" name="prptyName" style="width:calc(100% - 85px);" /> <!-- <img src="<c:url value="/img/btn_pop_input.png"/>" id="btn_pop_input" /></td> -->
<button type="button" class="cssbtn smallBtn2" id="btn_pop_input" style="vertical-align:middle; font-weight:bold;"><i class="material-icons">input</i><%= localeMessage.getString("button.input") %></button>
<td>
<input type="text" name="prptyName" style="width:calc(100% - 85px);" /> <!-- <img src="<c:url value="/img/btn_pop_input.png"/>" id="btn_pop_input" /></td> -->
<button type="button" class="cssbtn smallBtn2" id="btn_pop_input" style="vertical-align:middle; font-weight:bold;"><i class="material-icons">input</i><%= localeMessage.getString("button.input") %></button>
</td>
</tr>
<tr>
<th><%= localeMessage.getString("propertyDetail.propertyValue") %></th>
<td><input type="text" name="prpty2Val"/></td>
<th style="width:20%;"><%= localeMessage.getString("propertyDetail.propertyValue") %></th>
<td>
<input type="text" name="prpty2Val" style="width:calc(100% - 85px);"/>
<button type="button" class="cssbtn smallBtn2" id="btn_pop_encrypt" style="vertical-align:middle; font-weight:bold;"><i class="material-icons">lock</i><%= localeMessage.getString("button.encode") %></button>
</td>
</tr>
</table>
<!-- grid -->
@@ -34,7 +34,7 @@
, {id:'LLLVAR',name:'[ISO8583] LLLVAR'}, {id:'LLLLVAR',name:'[ISO8583] LLLLVAR'}, {id:'BINARY',name:'[ISO8583] BINARY'}, {id:'LLBIN',name:'[ISO8583] LLBIN'}
, {id:'LLLBIN',name:'[ISO8583] LLLBIN'}];
const basicDataTypes = [{id:'BigDecimal',name:'BigDecimal'},{id:'String',name:'String'}];
const basicDataTypes = [{id:'BigDecimal',name:'BigDecimal'},{id:'String',name:'String'},{id:'Boolean',name:'Boolean'}];
function gridRenderingInit() {
$('#effectGrid').jqGrid({
@@ -44,14 +44,16 @@
'<%=localeMessage.getString("stdDefaultMan.columDesc")%>',
'<%=localeMessage.getString("stdDefaultMan.columLen")%>',
'<%=localeMessage.getString("stdDefaultMan.keyOr")%>',
'<%=localeMessage.getString("stdDefaultMan.ifOr")%>'
'<%=localeMessage.getString("stdDefaultMan.ifOr")%>',
'<%=localeMessage.getString("stdDefaultMan.orderSeq")%>'
],
colModel:[
{ name : 'COLUMNNAME' , align:'left' , sortable:false },
{ name : 'COLUMNDESC' , align:'left' },
{ name : 'COLUMNLENGTH' , align:'right' },
{ name : 'ISKEY' , align:'center' },
{ name : 'ISINTERFACE' , align:'center' }
{ name : 'COLUMNNAME' , align:'left' , width: '200px', sortable:false },
{ name : 'COLUMNDESC' , align:'left' , width: '200px' },
{ name : 'COLUMNLENGTH' , align:'center' , width: '100px' },
{ name : 'ISKEY' , align:'center', width: '100px' },
{ name : 'ISINTERFACE' , align:'center', width: '100px' },
{ name : 'ORDERSEQ' , align:'center', width: '100px' }
],
jsonReader: {
repeatitems:false
@@ -0,0 +1,211 @@
<%@ page language="java" contentType="text/html; charset=utf-8" %>
<%@ page import="java.io.*" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<style>
.mask-email { color:#1a73e8; font-weight:bold; }
.mask-phone { color:#d93025; font-weight:bold; }
.mask-digits { color:#9334e6; font-weight:bold; }
.msg-cell { display:inline-block; max-width:380px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; vertical-align:middle; }
</style>
<script language="javascript">
var url = '<c:url value="/onl/apim/messagerequest/messageRequestMan.json" />';
var url_view = '<c:url value="/onl/apim/messagerequest/messageRequestMan.view" />';
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
function escapeAttr(s) { return escapeHtmlJs(s).replace(/"/g, '&quot;'); }
// 제목: 없으면 messageCode, 마우스오버에 코드명(코드) 표기
function formatSubject(cellvalue, options, rowObject) {
var code = rowObject.messageCode || '';
var subject = (cellvalue && cellvalue !== '') ? cellvalue : '';
var title = rowObject.messageCodeName ? (rowObject.messageCodeName + ' (' + code + ')') : code;
if (subject) {
// 제목이 있으면 2줄: 제목 + 하단에 메세지코드
var codeLine = code ? '<br><span style="color:#888;font-size:0.85em;">' + escapeHtmlJs(code) + '</span>' : '';
return '<span title="' + escapeAttr(title) + '">' + escapeHtmlJs(subject) + codeLine + '</span>';
}
// 제목이 없으면 메세지코드만
return '<span title="' + escapeAttr(title) + '">' + escapeHtmlJs(code) + '</span>';
}
// 본문: 서버에서 마스킹+escape 된 안전 HTML 그대로 렌더
function formatMessage(cellvalue, options, rowObject) {
return '<span class="msg-cell">' + (rowObject.messageHtml || '') + '</span>';
}
// 수신자 정보: 휴대폰 / 이메일 / 메신저ID (모두 마스킹됨)
function formatReceiverInfo(cellvalue, options, rowObject) {
var parts = [];
if (rowObject.phone) parts.push(escapeHtmlJs(rowObject.phone));
if (rowObject.email) parts.push(escapeHtmlJs(rowObject.email));
if (rowObject.messengerId) parts.push(escapeHtmlJs(rowObject.messengerId));
return parts.join('<br>');
}
function formatRequestDate(cellvalue, options, rowObject) {
return '<span title="' + escapeAttr(rowObject.requestDateFull || '') + '">' + escapeHtmlJs(rowObject.requestDateShort || '') + '</span>';
}
function formatSentDate(cellvalue, options, rowObject) {
return '<span title="' + escapeAttr(rowObject.sentDateFull || '') + '">' + escapeHtmlJs(rowObject.sentDateShort || '') + '</span>';
}
function init() {
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'LIST_INIT_COMBO'},
success: function (json) {
new makeOptions("CODE", "NAME").setObj($("select[name=searchRequestStatus]"))
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
.setData(json.statusList).rendering();
new makeOptions("CODE", "NAME").setObj($("select[name=searchMessageCode]"))
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
.setData(json.messageCodeList).rendering();
putSelectFromParam();
// 콤보 로드 후 그리드 생성 (경합 방지)
buildGrid();
},
error: function (e) { alert(e.responseText); }
});
}
function buildGrid() {
$('#grid').jqGrid({
datatype: "json", mtype: 'POST', url: url,
postData: getSearchForJqgrid("cmd", "LIST"),
colNames: ['id', 'No.', '제목', '메세지 내용', '수신자명', '수신자 정보', '유형', '상태', '요청시간', '발송시간'],
colModel: [
{name: 'id', align: 'center', key: true, hidden: true},
{name: 'rowNum', align: 'center', width: 45, sortable: false},
{name: 'subject', align: 'left', width: 160, formatter: formatSubject},
{name: 'messageHtml', align: 'left', width: 300, formatter: formatMessage},
{name: 'username', align: 'center', width: 80},
{name: 'receiverInfo', align: 'left', width: 150, formatter: formatReceiverInfo},
{name: 'messageType', align: 'center', width: 60},
{name: 'requestStatus', align: 'center', width: 70},
{name: 'requestDateShort', align: 'center', width: 110, formatter: formatRequestDate},
{name: 'sentDateShort', align: 'center', width: 110, formatter: formatSentDate}
],
jsonReader: {repeatitems: false},
pager: $('#pager'),
page: '${param.page}',
rowNum: '${rmsDefaultRowNum}',
autoheight: true,
height: $("#container").height(),
autowidth: true,
viewrecords: true,
rowList: eval('[${rmsDefaultRowList}]'),
ondblClickRow: function (rowId) {
var rowData = $(this).getRowData(rowId);
var id = rowData['id'];
var url2 = url_view + '?cmd=DETAIL';
url2 += '&page=' + $(this).getGridParam("page");
url2 += '&returnUrl=' + getReturnUrl();
url2 += '&menuId=' + '${param.menuId}';
url2 += '&id=' + id;
goNav(url2);
},
loadComplete: function () {
var page = $(this).getGridParam('page');
var rowNum = $(this).getGridParam('rowNum');
var rows = $(this).getDataIDs();
var colModel = $(this).getGridParam("colModel");
for (var i = 0; i < rows.length; i++) {
var number = ((page - 1) * rowNum + i) + 1;
$(this).setCell(rows[i], 'rowNum', number);
}
// 서버 고정 정렬(요청일 역순) — 컬럼 정렬 비활성
for (var i = 0; i < colModel.length; i++) {
$(this).setColProp(colModel[i].name, {sortable: false});
}
},
loadError: function (jqXHR, textStatus, errorThrown) {
var location = '<%=request.getContextPath()%>/';
comloadError(jqXHR, textStatus, errorThrown, location);
}
});
resizeJqGridWidth('grid', 'content_middle', '1000');
}
$(document).ready(function () {
init();
$("#btn_search").click(function () {
var postData = getSearchForJqgrid("cmd", "LIST");
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
});
$("select[name^=search]").change(function () { $("#btn_search").click(); });
$("input[name^=search]").keydown(function (key) {
if (key.keyCode == 13) { $("#btn_search").click(); }
});
buttonControl();
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
</div><!-- end content_top -->
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
</div>
<div class="title">메세지 발송 내역<span class="tooltip">발송 요청 이력을 조회합니다. 개인정보는 마스킹되어 표시됩니다.</span></div>
<form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing=0;>
<colgroup>
<col style="width:120px;"><col style="width:200px;">
<col style="width:120px;"><col style="width:200px;">
<col style="width:120px;"><col style="width:200px;">
</colgroup>
<tbody>
<tr>
<th>상태</th>
<td><div class="select-style"><select name="searchRequestStatus"></select></div></td>
<th>메세지 코드</th>
<td><div class="select-style"><select name="searchMessageCode"></select></div></td>
<th>수신자명</th>
<td><input type="text" name="searchRecipient" autocomplete="off"></td>
</tr>
<tr>
<th>유형</th>
<td>
<div class="select-style">
<select name="searchMessageType">
<option value="">전체</option>
<option value="EMAIL">EMAIL</option>
<option value="SMS">SMS</option>
</select>
</div>
</td>
<th>요청기간</th>
<td colspan="3">
<input type="date" name="searchFromDate"> ~ <input type="date" name="searchToDate">
</td>
</tr>
</tbody>
</table>
</form>
<table id="grid"></table>
<div id="pager"></div>
</div><!-- end content_middle -->
</div><!-- end right_box -->
</body>
</html>
@@ -0,0 +1,149 @@
<%@ page language="java" contentType="text/html; charset=utf-8" %>
<%@ page import="java.io.*" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ include file="/jsp/common/include/localemessage.jsp" %>
<%
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
%>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<jsp:include page="/jsp/common/include/css.jsp"/>
<jsp:include page="/jsp/common/include/script.jsp"/>
<style>
.mask-email { color:#1a73e8; font-weight:bold; }
.mask-phone { color:#d93025; font-weight:bold; }
.mask-digits { color:#9334e6; font-weight:bold; }
.msg-detail { white-space:pre-wrap; word-break:break-all; min-height:80px; line-height:1.6; }
</style>
<script language="javascript">
var url = '<c:url value="/onl/apim/messagerequest/messageRequestMan.json" />';
var url_view = '<c:url value="/onl/apim/messagerequest/messageRequestMan.view" />';
var key = '${param.id}';
function detail() {
if (!key) return;
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'DETAIL', id: key},
success: function (data) {
var codeText = (data.messageCodeName || '') + (data.messageCode ? ' (' + data.messageCode + ')' : '');
$('#messageCodeName').text(codeText);
$('#subject').text((data.subject && data.subject !== '') ? data.subject : (data.messageCode || ''));
$('#messageType').text(data.messageType || '');
$('#requestStatus').text(data.requestStatus || '');
$('#username').text(data.username || '');
$('#email').text(data.email || '');
$('#phone').text(data.phone || '');
$('#messengerId').text(data.messengerId || '');
$('#umsUid').text(data.umsUid || '');
$('#eaiInterfaceId').text(data.eaiInterfaceId || '');
$('#serviceId').text(data.serviceId || '');
$('#requestDate').text(data.requestDateFull || '');
$('#sentDate').text(data.sentDateFull || '');
// 서버에서 마스킹+escape 된 안전 HTML
$('#messageHtml').html(data.messageHtml || '');
// PENDING 건만 '실패 처리' 노출 (권한 제어는 buttonControl 이 담당)
if (data.requestStatus !== 'PENDING') {
$('#btn_fail').hide();
}
},
error: function (e) { alert(e.responseText); }
});
}
$(document).ready(function () {
var returnUrl = getReturnUrlForReturn();
buttonControl();
detail();
$('#btn_fail').click(function () {
if (!confirm('이 건을 FAILED(발송 실패) 상태로 변경하시겠습니까?')) return;
$.ajax({
type: "POST", url: url, dataType: "json",
data: {cmd: 'UPDATE_STATUS', id: key},
success: function () {
alert("변경되었습니다.");
detail();
},
error: function (e) { alert(e.responseText); }
});
});
$('#btn_previous').click(function () { goNav(returnUrl); });
});
</script>
</head>
<body>
<div class="right_box">
<div class="content_top">
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
</div>
<div class="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_fail" level="W" status="DETAIL" style="background-color:#dc3545;border-color:#dc3545;color:#fff;"><i class="material-icons">report</i> 실패 처리</button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title" id="title">메세지 발송 내역 상세<span class="tooltip">개인정보는 마스킹되어 표시됩니다.</span></div>
<table class="table_row" cellspacing="0">
<colgroup>
<col style="width:12%"/><col style="width:38%"/>
<col style="width:12%"/><col style="width:38%"/>
</colgroup>
<tr>
<th>메세지 코드</th>
<td colspan="3"><span id="messageCodeName"></span></td>
</tr>
<tr>
<th>제목</th>
<td colspan="3"><span id="subject"></span></td>
</tr>
<tr>
<th>유형</th>
<td><span id="messageType"></span></td>
<th>상태</th>
<td><span id="requestStatus"></span></td>
</tr>
<tr>
<th>수신자명</th>
<td><span id="username"></span></td>
<th>메신저 ID</th>
<td><span id="messengerId"></span></td>
</tr>
<tr>
<th>이메일</th>
<td><span id="email"></span></td>
<th>휴대폰</th>
<td><span id="phone"></span></td>
</tr>
<tr>
<th>요청 시간</th>
<td><span id="requestDate"></span></td>
<th>발송 시간</th>
<td><span id="sentDate"></span></td>
</tr>
<tr>
<th>EAI 인터페이스 ID</th>
<td><span id="eaiInterfaceId"></span></td>
<th>서비스 ID</th>
<td><span id="serviceId"></span></td>
</tr>
<tr>
<th>UMS UID</th>
<td colspan="3"><span id="umsUid"></span></td>
</tr>
<tr>
<th>메세지 내용</th>
<td colspan="3"><div id="messageHtml" class="msg-detail"></div></td>
</tr>
</table>
</div>
</div>
</body>
</html>
@@ -20,7 +20,9 @@
var combo;
function formatNoticeType(cellvalue, options, rowObject) {
var name = "";
if (!combo || !combo.noticeTypeList) {
return cellvalue;
}
for (var i = 0; i < combo.noticeTypeList.length; i++) {
if (combo.noticeTypeList[i].CODE == cellvalue) {
return combo.noticeTypeList[i].NAME;
@@ -46,8 +48,39 @@
var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : '';
return cellvalue + icon;
}
function deleteSelectedRows() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
if (selectedIds.length === 0) {
alert('항목을 선택하세요.');
return;
}
if (!confirm('선택된 ' + selectedIds.length + '개 항목을 삭제하시겠습니까?')) {
return;
}
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {
cmd: 'DELETE_BULK',
ids: selectedIds.join(',')
},
success: function(data) {
alert('삭제되었습니다.');
$("#grid").jqGrid('clearGridData', true);
$("#grid").trigger("reloadGrid");
},
error: function(e) {
alert('삭제 중 오류가 발생했습니다.');
}
});
}
function init(){
$.ajax({
type : "POST",
@@ -58,8 +91,11 @@
console.log('init', json);
combo = json;
new makeOptions("CODE","NAME").setObj($("select[name=searchNoticeType]")).setNoValueInclude(true).setNoValue('','<%=localeMessage.getString("combo.all")%>').setData(json.noticeTypeList).rendering();
putSelectFromParam();
// 콤보(combo)가 채워진 뒤에 그리드를 생성해야 formatNoticeType 경합이 발생하지 않음
buildGrid();
},
error:function(e){
alert(e.responseText);
@@ -67,8 +103,8 @@
});
}
$(document).ready(function() {
function buildGrid(){
$('#grid').jqGrid({
datatype:"json",
mtype: 'POST',
@@ -103,6 +139,8 @@
autowidth: true,
viewrecords: true,
rowList : eval('[${rmsDefaultRowList}]'),
multiselect: true,
multiboxonly: true,
ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId);
var id = rowData['id'];
@@ -139,9 +177,13 @@
}
});
init();
resizeJqGridWidth('grid','content_middle','1000');
}
$(document).ready(function() {
// 콤보 로드 → 콤보 성공 콜백에서 buildGrid() 호출 (경합 방지)
init();
$("#btn_search").click(function(){
var postData = getSearchForJqgrid("cmd","LIST");
@@ -157,6 +199,10 @@
goNav(url2);
});
$("#btn_delete_selected").click(function(){
deleteSelectedRows();
});
$("select[name=searchUseYn], input[name^=search]").keydown(function(key){
if (key.keyCode == 13){
$("#btn_search").click();
@@ -179,6 +225,7 @@
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> <%= localeMessage.getString("button.new") %></button>
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
</div>
<div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div>
<form id="ajaxForm" onsubmit="return false;">
@@ -26,7 +26,38 @@ function formatFile(cellvalue, options, rowObject) {
return cellvalue + icon;
}
$(document).ready(function() {
function deleteSelectedRows() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
if (selectedIds.length === 0) {
alert('항목을 선택하세요.');
return;
}
if (!confirm('선택된 ' + selectedIds.length + '개 항목을 삭제하시겠습니까?')) {
return;
}
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {
cmd: 'DELETE_BULK',
ids: selectedIds.join(',')
},
success: function(data) {
alert('삭제되었습니다.');
$("#grid").jqGrid('clearGridData', true);
$("#grid").trigger("reloadGrid");
},
error: function(e) {
alert('삭제 중 오류가 발생했습니다.');
}
});
}
$(document).ready(function() {
$('#grid').jqGrid({
datatype:"json",
mtype: 'POST',
@@ -45,7 +76,7 @@ $(document).ready(function() {
],
jsonReader: {
repeatitems:false
},
},
pager : $('#pager'),
page : '${param.page}',
rowNum : '${rmsDefaultRowNum}',
@@ -54,6 +85,8 @@ $(document).ready(function() {
autowidth: true,
viewrecords: true,
rowList : eval('[${rmsDefaultRowList}]'),
multiselect: true,
multiboxonly: true,
ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId);
var id = rowData['id'];
@@ -94,10 +127,14 @@ $(document).ready(function() {
resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
$("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid");
});
$("#btn_delete_selected").click(function(){
deleteSelectedRows();
});
$("input[name^=search]").keydown(function(key){
if (key.keyCode == 13){
$("#btn_search").click();
@@ -120,8 +157,9 @@ $(document).ready(function() {
<div class="content_middle" id="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_search" level="R" ><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
</div>
<div class="title">사업제휴 신청 목록</div>
<div class="title">피드백/개선요청 신청 목록</div>
<form id="ajaxForm" onsubmit="return false;">
<table class="search_condition" cellspacing=0;>
<tbody>
@@ -92,6 +92,27 @@ $(document).ready(function() {
goNav(returnUrl);//LIST로 이동
});
$("#btn_delete").click(function(){
if (!confirm('선택한 항목을 삭제하시겠습니까?')) {
return;
}
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: { cmd: 'DELETE', id: key },
success: function(data) {
alert('삭제되었습니다.');
var returnUrl = getReturnUrlForReturn();
goNav(returnUrl);//LIST로 이동
},
error: function(e) {
alert('삭제 중 오류가 발생했습니다.');
}
});
});
$("#btn_unmask").click(function () {
const reason = prompt("마스킹 해제 사유를 입력하세요:");
@@ -134,9 +155,10 @@ $(document).ready(function() {
<div class="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_unmask" level="W" status="DETAIL"><i class="material-icons">lock_open</i> 마스킹해제</button>
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 삭제</button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title">사업제휴 신청 상세</div>
<div class="title">피드백/개선요청 신청 상세</div>
<table id="grid" ></table>
<div id="pager"></div>
@@ -222,7 +222,7 @@
$("input[name=searchStartDate], input[name=searchEndDate]").datepicker();
var today = getToday();
var startDate = today;
var startDate = today.substring(0,6)+"01";
var endDate = today;
@@ -234,6 +234,7 @@
}
list();
search();
resizeJqGridWidth('grid', 'content_middle', '1200');
@@ -1686,7 +1686,7 @@
<option value="none" selected>NONE</option>
<option value="oauth">OAUTH</option>
<option value="api_key">API_KEY</option>
<option value="ca">CA</option>
<!-- <option value="ca">CA</option> -->
</select>
</div>
</div>
+51
View File
@@ -0,0 +1,51 @@
# DJBank EAPIM Admin — user-scope systemd unit
#
# Install (one-time, on the host as `djb`):
# sudo loginctl enable-linger djb # allow services to outlive ssh logout
# mkdir -p ~/.config/systemd/user
# # workflow copies this file in on each deploy; for first boot you can also do:
# # cp /prod/.../deploy/systemd/eapim-admin.service ~/.config/systemd/user/
# systemctl --user daemon-reload
# systemctl --user enable eapim-admin
# systemctl --user start eapim-admin
#
# Why this exists:
# - `catalina.sh start` + `nohup &` does NOT escape the Gitea Actions runner
# process group. When the workflow step finishes, the runner sends SIGTERM
# to the process tree and Tomcat dies seconds after boot.
# - Running Tomcat as a systemd user service detaches lifecycle from the
# runner entirely. The workflow just calls `systemctl --user restart` and
# systemd owns the process.
[Unit]
Description=DJBank EAPIM Admin (Tomcat 9)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=JAVA_HOME=/apps/opts/jdk17
Environment=CATALINA_BASE=/prod/eapim/adminportal
Environment=CATALINA_HOME=/prod/eapim/apache-tomcat-9.0.116
Environment=CATALINA_PID=/prod/eapim/adminportal/temp/catalina.pid
# `catalina.sh run` keeps Tomcat in the foreground so systemd can supervise it.
ExecStart=/prod/eapim/apache-tomcat-9.0.116/bin/catalina.sh run
# Graceful shutdown via Tomcat's shutdown port, 30s window.
ExecStop=/prod/eapim/apache-tomcat-9.0.116/bin/catalina.sh stop 30
# Tomcat returns 143 on SIGTERM during normal stop — treat as success.
SuccessExitStatus=143
TimeoutStartSec=180
TimeoutStopSec=60
Restart=on-failure
RestartSec=10
# Memory / fd limits — adjust as needed.
LimitNOFILE=65535
[Install]
WantedBy=default.target
Binary file not shown.
@@ -35,6 +35,7 @@ public class MonitoringPropertyManService extends BaseService {
public MonitoringPropertyGroupUI selectDetail(String prptyGroupName) {
MonitoringPropertyGroup propertyGroup = propertyGroupService.getById(prptyGroupName);
propertyGroup.getMonitoringProperties().sort(Comparator.comparing(p -> p.getId().getPrptyName()));
return propertyGroupMapper.toVo(propertyGroup);
}
@@ -57,7 +57,7 @@ public class ClusteredSchedulerConfig {
quartzProperties.put("org.quartz.jobStore.misfireThreshold", "60000");
quartzProperties.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
quartzProperties.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate");
quartzProperties.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
quartzProperties.put("org.quartz.jobStore.useProperties", "false");
quartzProperties.put("org.quartz.jobStore.dataSource", monitoringSchema);
quartzProperties.put("org.quartz.jobStore.tablePrefix", monitoringSchema + ".QRTZ_");
@@ -15,4 +15,6 @@ public interface EMSScheduler {
void deleteAndAddJob(Scheduler scheduler, String jobName) throws SchedulerException, ClassNotFoundException;
void reloadJob(String jobName) throws SchedulerException, ClassNotFoundException;
}
@@ -120,12 +120,50 @@ public class EMSSchedulerImpl implements EMSScheduler {
}
}
@Override
public void reloadJob(String jobName) throws SchedulerException, ClassNotFoundException {
JobInfo jobInfo = jobInfoService.getById(jobName);
if (CLUSTERED.equals(jobInfo.getInstanceName())) {
deleteJob(scheduler, jobName);
reloadClusteredJob(jobInfo);
} else {
deleteAndAddJob(scheduler, jobInfo);
deleteJob(clusteredScheduler, jobName);
}
}
private void reloadClusteredJob(JobInfo jobInfo) throws SchedulerException, ClassNotFoundException {
String jobId = jobInfo.getId();
TriggerKey triggerKey = new TriggerKey(getTriggerName(jobId), Scheduler.DEFAULT_GROUP);
JobKey jobKey = new JobKey(jobId, Scheduler.DEFAULT_GROUP);
Trigger existing = clusteredScheduler.getTrigger(triggerKey);
JobDetail existingJob = clusteredScheduler.getJobDetail(jobKey);
if (existing != null && existingJob != null) {
CronTrigger newTrigger = newTrigger()
.withIdentity(triggerKey)
.withSchedule(cronSchedule(jobInfo.getCronExp()))
.build();
Date ft = clusteredScheduler.rescheduleJob(triggerKey, newTrigger);
if (ft != null) {
return;
}
log.warn("[reloadClusteredJob] {} rescheduleJob returned null, falling back to deleteAndAddJob", jobId);
} else if (existing != null) {
// orphaned trigger (job missing) — clean up before re-adding
log.warn("[reloadClusteredJob] {} has orphaned trigger without job, unscheduling", jobId);
clusteredScheduler.unscheduleJob(triggerKey);
}
deleteAndAddJob(clusteredScheduler, jobInfo);
}
public void deleteJob(Scheduler inScheduler, String jobNm) throws SchedulerException {
String schedulerType = (inScheduler == clusteredScheduler) ? "clusteredScheduler" : "scheduler";
JobKey key = new JobKey(jobNm, Scheduler.DEFAULT_GROUP);
JobDetail jobDetail = inScheduler.getJobDetail(key);
if (jobDetail != null) {
inScheduler.deleteJob(key);
log.info(jobNm + " has been scheduled to deleted");
}
}
@@ -151,10 +189,7 @@ public class EMSSchedulerImpl implements EMSScheduler {
.withSchedule(cronSchedule(info.getCronExp()))
.build();
Date ft = inScheduler.scheduleJob(job, trigger);
log
.info("{} has been scheduled to run at: {} and repeat based on expression: {}", job.getKey(), ft,
trigger.getCronExpression());
inScheduler.scheduleJob(job, trigger);
}
}
@@ -45,6 +45,9 @@ public class SchedulerManController extends BaseAnnotationController {
@Qualifier("monitoringSyncAgent")
private MonitoringSyncAgent monitoringSyncAgent;
@Autowired
private EMSScheduler emsScheduler;
@GetMapping(value = "/common/scheduler/schedulerMan.view")
public String viewList() {
return "/common/scheduler/schedulerMan";
@@ -96,6 +99,11 @@ public class SchedulerManController extends BaseAnnotationController {
@PostMapping(value = "/common/scheduler/schedulerMan.json", params = "cmd=UPDATE")
public ResponseEntity<Void> update(JobInfoUI jobInfoUI) {
service.update(jobInfoUI);
try {
emsScheduler.reloadJob(jobInfoUI.getId());
} catch (Exception e) {
logger.error("reloadJob error on current server", e);
}
syncReloadScheduler(jobInfoUI.getId());
return ResponseEntity.ok().build();
}
@@ -115,7 +123,7 @@ public class SchedulerManController extends BaseAnnotationController {
String url = "/monitoring/schedulerReload.do";
try {
ret = monitoringSyncAgent.sync(url, paramMap);
ret = monitoringSyncAgent.targetSync(url, paramMap);
} catch (Exception e) {
logger.error("syncReloadScheduler error", e);
} finally {
@@ -0,0 +1,17 @@
package com.eactive.eai.rms.data.entity.onl.apim.messagerequest;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
/**
* 발송 내역(PTL_MESSAGE_REQUEST) 조회용 admin 전용 리포지토리.
*
* <p>elink-portal-common 의 {@code template.repository.MessageRequestRepository}(JpaRepository) 와
* <b>단순 클래스명이 겹치면 Spring Data 빈 이름이 충돌</b>하므로(둘 다 admin 컨텍스트에 스캔됨)
* 이름을 분리한다. 또한 QueryDSL {@code findAll(predicate, pageable)} 를 쓰기 위해
* {@link BaseRepository} 를 상속한다.</p>
*/
@EMSDataSource
public interface MessageRequestEmsRepository extends BaseRepository<MessageRequest, String> {
}
@@ -0,0 +1,65 @@
package com.eactive.eai.rms.data.entity.onl.apim.messagerequest;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.apim.portal.template.entity.QMessageRequest;
import com.eactive.eai.rms.common.util.StringUtils;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import com.eactive.eai.rms.onl.common.exception.BizException;
import com.querydsl.core.BooleanBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class MessageRequestService
extends AbstractEMSDataSerivce<MessageRequest, String, MessageRequestEmsRepository> {
public Page<MessageRequest> findAll(Pageable pageable, MessageRequestUISearch search) {
QMessageRequest q = QMessageRequest.messageRequest;
BooleanBuilder predicate = new BooleanBuilder();
if (StringUtils.isNotBlank(search.getSearchRequestStatus())) {
predicate.and(q.requestStatus.eq(search.getSearchRequestStatus()));
}
if (StringUtils.isNotBlank(search.getSearchMessageCode())) {
MessageCode.findServiceId(search.getSearchMessageCode())
.ifPresent(code -> predicate.and(q.messageCode.eq(code)));
}
if (StringUtils.isNotBlank(search.getSearchMessageType())) {
predicate.and(q.messageType.eq(search.getSearchMessageType()));
}
if (StringUtils.isNotBlank(search.getSearchRecipient())) {
predicate.and(q.username.containsIgnoreCase(search.getSearchRecipient()));
}
// requestDate 는 yyyyMMddHHmmss 14자리 문자열로 저장(LocalDateTimeToStringConverter14)되므로
// 고정폭 비교로 기간 필터가 동작한다.
if (search.getSearchFromDate() != null) {
predicate.and(q.requestDate.goe(search.getSearchFromDate().atStartOfDay()));
}
if (search.getSearchToDate() != null) {
predicate.and(q.requestDate.loe(search.getSearchToDate().atTime(23, 59, 59)));
}
// 발송요청 역순 고정 (Pageable 에 정렬이 없을 때의 안전장치)
Sort base = Sort.by(Sort.Direction.DESC, "requestDate");
Sort sort = pageable.getSort().isSorted() ? pageable.getSort() : base;
Pageable fixed = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort);
return repository.findAll(predicate, fixed);
}
/** PENDING 상태인 건만 FAILED 로 전환한다. */
public void updateStatusToFailedIfPending(String id) {
MessageRequest entity = getById(id);
if (!"PENDING".equals(entity.getRequestStatus())) {
throw new BizException("PENDING 상태인 건만 변경할 수 있습니다.");
}
entity.setRequestStatus("FAILED");
save(entity);
}
}
@@ -0,0 +1,29 @@
package com.eactive.eai.rms.data.entity.onl.apim.messagerequest;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
@Data
public class MessageRequestUISearch {
/* 발송 상태 (PENDING/SENT/FAILED) */
private String searchRequestStatus;
/* 메세지 코드 (MessageCode enum name) */
private String searchMessageCode;
/* 메세지 유형 (EMAIL/SMS 등) */
private String searchMessageType;
/* 수신자 이름 (MessageRequest.username 평문 컬럼 대상, LIKE 검색) */
private String searchRecipient;
/* 요청일시 기간 검색 (email/phone 은 암호화 저장이라 검색 제외) */
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate searchFromDate;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate searchToDate;
}
@@ -13,10 +13,16 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import java.util.List;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, String, PortalNoticeRepository> {
public void deleteByIds(List<String> ids) {
repository.deleteAllById(ids);
}
public Page<PortalNotice> findAll(Pageable pageable, PortalNoticeUISearch portalNoticeUISearch) {
QPortalNotice qPortalNotice = QPortalNotice.portalNotice;
@@ -10,10 +10,16 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalPartnershipService extends AbstractEMSDataSerivce<PartnershipApplication, String, PortalPartnershipRepository> {
public void deleteByIds(List<String> ids) {
repository.deleteAllById(ids);
}
public Page<PartnershipApplication> findAll(Pageable pageable, String searchBizSubject, String searchBizDetail) {
QPartnershipApplication qPartnershipApplication = QPartnershipApplication.partnershipApplication;
@@ -77,6 +77,10 @@ public class PortalUserService extends AbstractEMSDataSerivce<PortalUser, String
}
public PortalUser findByUserId(String userId) {
if (StringUtils.isBlank(userId)) {
return null;
}
QPortalUser qPortalUser = QPortalUser.portalUser;
return getJPAQueryFactory()
@@ -5,7 +5,9 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -42,7 +44,12 @@ public class ExtendedColumnDefinitionService
predicate = predicate.and(qExtendedColumnDefinition.columnDesc.containsIgnoreCase(searchColumnDesc));
}
return repository.findAll(predicate, pageable);
Pageable orderedPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
Sort.by(Sort.Order.asc("orderSeq"), Sort.Order.asc("columnName")));
return repository.findAll(predicate, orderedPageable);
}
}
@@ -39,11 +39,10 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
+ " FROM ("
+ " SELECT A.EAISVCNAME"
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
//TODO: 게시판 임시로 생성하여 사용. 유차장이 완료하면 변경할것
+ " , NVL(( SELECT 'Y' FROM PTL_NOTICE_API X "
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.PTL_NOTICE Y WHERE X.NOTICE_ID = Y.ID)"
//+ " AND TO_CHAR(SYSDATE, 'YYYYMMDDHH24MI') BETWEEN START_TIME AND END_TIME) "
+ " AND A.EAISVCNAME = X.API_NAME),'N') AS CTRL_YN "
+ " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X"
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID "
+ " AND SYSTIMESTAMP BETWEEN STARTED_AT AND END_AT) "
+ " AND A.EAISVCNAME = X.API_ID),'N') AS CTRL_YN "
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
+ " ELSE 'N' END"
@@ -15,7 +15,7 @@ import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatus;
import com.eactive.eai.rms.data.entity.onl.djb.apistatus.ApiStatusEvent;
import com.eactive.eai.rms.ext.djb.util.UmsManager;
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
@Service
@@ -26,10 +26,10 @@ public class ApiStatusService {
@Autowired
private ApiStatusRepository apiStatusRepository;
@Autowired
private UmsManager ums;
private UmsManager ums;
@Autowired
private ApiStatusDetectionService apiStatusDectionService;
@@ -55,7 +55,8 @@ public class ApiStatusService {
apiStatus.setEaisvcname(event.getEaisvcname());
apiStatus.setStatusCode(newStatusCode);
apiStatusRepository.save(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT
apiStatusRepository.saveAndFlush(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT (즉시 flush)
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
@@ -11,7 +11,7 @@ import org.springframework.transaction.annotation.Transactional;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.eai.rms.data.entity.man.role.Role;
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
import com.eactive.eai.rms.ext.djb.util.UmsManager;
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
@Service
@@ -1,6 +1,9 @@
package com.eactive.eai.rms.ext.djb.job;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.quartz.DisallowConcurrentExecution;
@@ -68,10 +71,24 @@ public class ApiStatusMonitorJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
// execute.instances 가 지정된 경우 해당 인스턴스에서만 실행
String allowedInstances = jobDataMap.getString("execute.instances");
if (StringUtils.isNotEmpty(allowedInstances)) {
Set<String> instanceSet = Arrays.stream(allowedInstances.split(","))
.map(s -> s.trim().toLowerCase())
.collect(Collectors.toSet());
String currentInstance = StringUtils.defaultString(System.getProperty("inst.Name")).trim().toLowerCase();
if (!instanceSet.contains(currentInstance)) {
log.debug("Job 실행 인스턴스 제한 - 현재: {}, 허용: {}", currentInstance, allowedInstances);
return;
}
}
log.info("*** START ApiStatusUpdateJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
// Job 파라미터 로깅
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
logJobParameters(jobDataMap);
HashMap<String, String> param = this.checkParameters(jobDataMap);
@@ -1,8 +1,12 @@
package com.eactive.eai.rms.ext.djb.job;
import java.util.Arrays;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
@@ -48,6 +52,21 @@ public class InflowTokenMonitorJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
// execute.instances 가 지정된 경우 해당 인스턴스에서만 실행
String allowedInstances = jobDataMap.getString("execute.instances");
if (StringUtils.isNotEmpty(allowedInstances)) {
Set<String> instanceSet = Arrays.stream(allowedInstances.split(","))
.map(s -> s.trim().toLowerCase())
.collect(Collectors.toSet());
String currentInstance = StringUtils.defaultString(System.getProperty("inst.Name")).trim().toLowerCase();
if (!instanceSet.contains(currentInstance)) {
log.debug("Job 실행 인스턴스 제한 - 현재: {}, 허용: {}", currentInstance, allowedInstances);
return;
}
}
log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
long execMinute = 1; //default 배치 실행주기(분)
@@ -5,14 +5,20 @@ import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.onl.apim.portalInquiry.PortalInquiryClosingService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
import org.apache.commons.lang3.StringUtils;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 매일 00:05 실행 - RESPONDED 상태 문의글 자동 종료 배치
*
@@ -44,6 +50,21 @@ public class PortalInquiryClosingJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
// execute.instances 가 지정된 경우 해당 인스턴스에서만 실행
String allowedInstances = jobDataMap.getString("execute.instances");
if (StringUtils.isNotEmpty(allowedInstances)) {
Set<String> instanceSet = Arrays.stream(allowedInstances.split(","))
.map(s -> s.trim().toLowerCase())
.collect(Collectors.toSet());
String currentInstance = StringUtils.defaultString(System.getProperty("inst.Name")).trim().toLowerCase();
if (!instanceSet.contains(currentInstance)) {
log.debug("Job 실행 인스턴스 제한 - 현재: {}, 허용: {}", currentInstance, allowedInstances);
return;
}
}
log.info("*** START PortalInquiryCommentClosingService run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
int closingDays = DEFAULT_CLOSING_DAYS;
@@ -85,7 +85,7 @@ public class ApiUseStatsService
+ " ORDER BY ORGNAME";
} else if ("API".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", C.EAISVCDESC"
+ ", A.API_NAME"
+ ", SUM(A.TOTAL_CNT)"
+ ", SUM(A.SUCCESS_CNT)"
+ ", CASE WHEN SUM(A.TOTAL_CNT) > 0 THEN SUM(A.SUCCESS_CNT) * 100 / SUM(A.TOTAL_CNT) ELSE NULL END"
@@ -99,8 +99,8 @@ public class ApiUseStatsService
+ " LEFT OUTER JOIN TSEAIAU01 B ON A.CLIENT_ID = B.CLIENTID"
+ " LEFT OUTER JOIN TSEAIHE01 C ON A.API_NAME = C.EAISVCNAME "
+ where
+ " GROUP BY C.EAISVCDESC"
+ " ORDER BY EAISVCDESC";
+ " GROUP BY A.API_NAME"
+ " ORDER BY API_NAME";
} else if ("DATE".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME "
@@ -1,4 +1,4 @@
package com.eactive.eai.rms.ext.djb.util;
package com.eactive.eai.rms.ext.djb.ums;
import java.util.HashMap;
import java.util.List;
@@ -0,0 +1,43 @@
package com.eactive.eai.rms.ext.djb.ums.event;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageEventHandler;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.template.service.MessageSendEvent;
@Component
public class ApiStatusChangedEventHandler implements MessageEventHandler {
@Override
public MessageCode getKey() {
return MessageCode.API_STATUS_CHANGED;
}
@Override
public boolean allowAdditionalRecipients() {
return true;
}
@Override
public String getDisplayName() {
return "API 상태 변화";
}
@Override
public String getDescription() {
return "<div>사용 가능 변수: </div><br/>"
+ "<div> - message 알림 메시지 </div><br/>";
}
@Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> eventParams = new HashMap<>();
eventParams.put("message", (String) params.get("message"));
return new MessageSendEvent(source, MessageCode.API_STATUS_CHANGED, recipient, eventParams);
}
}
@@ -0,0 +1,43 @@
package com.eactive.eai.rms.ext.djb.ums.event;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.service.MessageEventHandler;
import com.eactive.apim.portal.template.service.MessageRecipient;
import com.eactive.apim.portal.template.service.MessageSendEvent;
@Component
public class InflowTokenFailedEventHandler implements MessageEventHandler {
@Override
public MessageCode getKey() {
return MessageCode.INFLOW_TOKEN_FAILED;
}
@Override
public boolean allowAdditionalRecipients() {
return true;
}
@Override
public String getDisplayName() {
return "유량제어 토큰 획득 실패";
}
@Override
public String getDescription() {
return "<div>사용 가능 변수: </div><br/>"
+ "<div> - message 알림 메시지 </div><br/>";
}
@Override
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
Map<String, String> eventParams = new HashMap<>();
eventParams.put("message", (String) params.get("message"));
return new MessageSendEvent(source, MessageCode.INFLOW_TOKEN_FAILED, recipient, eventParams);
}
}
@@ -0,0 +1,344 @@
package com.eactive.eai.rms.ext.djb.ums.service;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.eai.common.util.SystemUtil;
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
import com.eactive.eai.rms.ext.djb.ums.vo.EmailDataVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SmsDataVO;
import com.eactive.eai.rms.ext.djb.ums.vo.StdHeadVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SwingCommonVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SwingDataVO;
import com.eactive.eai.rms.ext.djb.ums.vo.SwingReceiverDataVO;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class UmsService {
private static final Gson gson = new Gson();
private static final String SYS_DVCD = "OPA";
private final MonitoringPropertyService propertyService;
public UmsService(MonitoringPropertyService propertyService) {
this.propertyService = propertyService;
}
public boolean send(MessageRequest messageRequest) throws Exception {
if (messageRequest == null) {
throw new IllegalArgumentException("messageRequest is null");
}
this.httpConnection(messageRequest);
return true;
}
private void httpConnection(MessageRequest messageRequest) throws Exception {
HttpURLConnection conn = null;
BufferedReader br = null;
log.debug("<<<HTTP Connection>>>:" + messageRequest.toString());
try {
String host = propertyService.getPropertyValue("Monitoring", "djb.ums."+messageRequest.getMessageType().toLowerCase()+".url", "");
URL url = new URL(host);
int timeoutValue = 5 * 1000; // 타임아웃 설정을 위한 값 (단위: ms)
// Charset charset = Charset.forName("UTF-8");
Charset charset = Charset.forName("MS949");
StringBuffer sb = null;
String bodyData = this.makeBodyData(messageRequest);
String responseData = "";
String method = bodyData.isEmpty() ? "GET" : "POST";
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(method); // 전송방식
// conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
conn.setRequestProperty("Content-Type", "application/json; charset=MS949");
conn.setConnectTimeout(timeoutValue); // 연결 타임아웃 설정(5초)
conn.setReadTimeout(timeoutValue); // 읽기 타임아웃 설정(5초)
conn.setDoOutput(true);
// POst 방식인 경우에만
if (method.equals("POST")) {
OutputStream os = conn.getOutputStream();
byte requestData[] = bodyData.getBytes(charset);
os.write(requestData);
os.close();
}
if (log.isDebugEnabled()) {
log.debug("getContentType();" + conn.getContentType()); // 응답 콘텐츠 유형 구하기
log.debug("getResponsecode();" + conn.getResponseCode()); // 응답 코드 구하기
log.debug("getResponseMessage():" + conn.getResponseMessage()); // 응답 메세지 구하기
}
// http 요청 후 응답 받은 데이타를 버퍼에 쌓는다
if (conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
br = new BufferedReader(new InputStreamReader(conn.getInputStream(), charset));
} else {
br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), charset));
}
String inputLine;
sb = new StringBuffer();
while ((inputLine = br.readLine()) != null) {
sb.append(inputLine);
}
responseData = sb.toString();
} finally {
// http 요청 및 응답 완료 후 BufferedReader를 닫는다
if (br != null) {
br.close();
}
if (conn != null) {
conn.disconnect();
}
}
}
private String makeBodyData(MessageRequest messageRequest) {
// 리턴값
String rtnVal = "";
// guID 및 날짜 시간 정보
String jsonData[] = this.makeStdGuidInfo();
// 표준 헤더
StdHeadVO stdHeadVO = new StdHeadVO();
stdHeadVO.setNxgn_stnd_idfr("JERA");
stdHeadVO.setGuid(jsonData[2]);
stdHeadVO.setGuid_prgs_no("1");
stdHeadVO.setStnd_mesg_ver("R10");
stdHeadVO.setOrtr_guid(jsonData[2]);
stdHeadVO.setSect_ecrpt_yn("N");
stdHeadVO.setFrst_trnm_ipad(propertyService.getPropertyValue("Monitoring", "djb.ums.was_ip_address", ""));
stdHeadVO.setFrst_trnm_ipv4_addr(propertyService.getPropertyValue("Monitoring", "djb.ums.was_ip_address", ""));
stdHeadVO.setFrst_trnm_mac(propertyService.getPropertyValue("Monitoring", "djb.ums.was_mac_address", ""));
stdHeadVO.setFrst_mesg_dman_dt(jsonData[0]);
stdHeadVO.setFrst_mesg_dman_time(jsonData[1]);
stdHeadVO.setFrst_trnm_sys_dvcd(SYS_DVCD);
stdHeadVO.setTrnm_sys_dvcd(SYS_DVCD);
stdHeadVO.setDman_rspn_dvcd("S"); // 요청응답구분코드 S:요청 R:응답
stdHeadVO.setSys_env_dvcd(SystemUtil.getSysOperEvirnDstcd()); // D:개발 T:테스트 P:운영
stdHeadVO.setTx_tycd("O"); // O:온라인 B:배치
stdHeadVO.setSynz_dvcd("S"); // S:동기 A:비동기
stdHeadVO.setChnl_tycd(""); // 채널유형코드
stdHeadVO.setHmab_dvcd("1"); // 내외구분코드 1:내부 2:외부(타발)
stdHeadVO.setIf_id(messageRequest.getEaiInterfaceId()); // 인터페이스ID
stdHeadVO.setTx_id(messageRequest.getServiceId()); // 거래ID
if ("MESSENGER".equals(messageRequest.getMessageType()) ) {
// 스윙챗 알림 공용
SwingCommonVO swiComVO = new SwingCommonVO();
swiComVO.setClientld(propertyService.getPropertyValue("Monitoring", "djb.ums.messenger.client_id", ""));
swiComVO.setClientSecret(propertyService.getPropertyValue("Monitoring", "djb.ums.messenger.client_secret", ""));
swiComVO.setCompanyCode("CB");
swiComVO.setRequestUniqueKey(this.genrateRandomStringNumber26());
// 스윙챗 알림 데이터
SwingDataVO swiDataVO = new SwingDataVO();
swiDataVO.setNotiCode("CNO_FEP_HMP_0002");
swiDataVO.setNotiTitle("알림");
swiDataVO.setNotiContent(messageRequest.getMessage());
// 스윙챗 알림 데이터
List<SwingReceiverDataVO> swiReDataVOList = new ArrayList<SwingReceiverDataVO>();
SwingReceiverDataVO receiver = new SwingReceiverDataVO();
receiver.setCode(messageRequest.getMessengerId());
swiReDataVOList.add(receiver);
swiDataVO.setReceiverInfo(swiReDataVOList);
String header = gson.toJson(stdHeadVO);
String common = gson.toJson(swiComVO);
String data = gson.toJson(swiDataVO);
// 헤더 + 데이터
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
+ " \"common\":\r\n" + common + ",\r\n" + " \"data\":" + data
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
}
else if ("SMS".equals(messageRequest.getMessageType())
|| "KAKAO".equals(messageRequest.getMessageType())) {
// Sms 데이터
SmsDataVO dataVO = new SmsDataVO();
dataVO.setSnd_dman_dt(jsonData[0]);
dataVO.setSnd_dman_id("");
dataVO.setSnd_dvcd("EAI");
dataVO.setMsg_titl("");
dataVO.setMsg_ctnt(messageRequest.getMessage());
dataVO.setRecvr_no(messageRequest.getPhone());
dataVO.setSndn_no("15880079"); //발신번호
dataVO.setSnd_empno(""); //요청자
dataVO.setSnd_emp_dprm_cd(""); //요청부서코드
if ("SMS".equals(messageRequest.getMessageType())) {
dataVO.setLtrs_snd_tycd("1"); // 1:SMS 2:LMS/MMS
dataVO.setMsg_snd_tycd("0000"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
} else if ("KAKAO".equals(messageRequest.getMessageType())) {
dataVO.setLtrs_snd_tycd("2"); // 1:SMS 2:LMS/MMS
dataVO.setMsg_snd_tycd("1008"); // 알림톡 메세지 종류 0000:SMS, 1000:LMS, 1008:알림톡, ....
dataVO.setAlmtk_snd_prfl_key(""); // 플러스친구아이디
dataVO.setAlmtk_tmplt_cd(""); // 템플릿코드
dataVO.setAlmtk_err_ltrs_snd_yn("Y"); // 오류자문자발송여부
dataVO.setAlmtk_btn_trnm_tycd("2"); // 카카오버튼전송방식 1-format string 2-JSON 3-XML
}
dataVO.setSms_trnm_bzwk_dvcd(""); //업무구분코드
dataVO.setSms_bzwk_dcls_dvcd(""); //업무세분코드
dataVO.setSms_sys_dvcd("83"); //시스템코드
dataVO.setTx_dt(jsonData[0]); //거래일자
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
List<SmsDataVO> dataList = new ArrayList<SmsDataVO>();
dataList.add(dataVO);
String header = gson.toJson(stdHeadVO);
String data = gson.toJson(dataList);
// 헤더 + 데이터
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
+ " \"ums_if_data_ncnt\":1, \r\n"
+ " \"ums_if_data\":" + data
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
}
else if ("EMAIL".equals(messageRequest.getMessageType())) {
// 이메일 데이터
EmailDataVO dataVO = new EmailDataVO();
dataVO.setUms_evnt_id(""); // UMS이벤트ID(필수)
dataVO.setSnd_dman_dt(jsonData[0]);
dataVO.setSnd_dman_id("");
dataVO.setSnd_dman_sno("1");
dataVO.setEmail_titl(messageRequest.getSubject()); // 메일제목
dataVO.setCstno(""); // 고객번호(필수)
dataVO.setSendr_emaddr(""); // 발신자 이메일
dataVO.setSendr_nm(""); // 발신자 이름
dataVO.setSndbk_emaddr(""); // 리턴 이메일
dataVO.setRecvr_emaddr(messageRequest.getEmail()); // 수신자 이메일(필수)
dataVO.setRecvr_nm(messageRequest.getUsername()); // 수신자명(필수)
dataVO.setTmplt_mpng_info(messageRequest.getMessage()); // 탬플릿매핑정보
dataVO.setTmplt_mpng_rptt_info(""); // 탬플릿매핑반복정보
dataVO.setSnd_empno(""); // 발신자ID(필수)
dataVO.setSnd_emp_dprm_cd(""); // 발신자부서코드(필수)
dataVO.setBzwk_cd(""); // 업무구분코드
dataVO.setSub_bzwk_cd(""); // 업무세부코드
dataVO.setSys_dvcd(SYS_DVCD);
dataVO.setTx_dt(jsonData[0]); //거래일자
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
List<EmailDataVO> dataList = new ArrayList<EmailDataVO>();
dataList.add(dataVO);
String header = gson.toJson(stdHeadVO);
String data = gson.toJson(dataList);
// 헤더 + 데이터
rtnVal = "{\"HEAD\":" + header + " ,\r\n" + "\"DATA\":\r\n" + " [" + "{\r\n"
+ " \"data_dvcd\":\"IO\", \r\n" + " \"BIZ_DATA\":\r\n" + " {\r\n"
+ " \"ums_if_data_ncnt\":1, \r\n"
+ " \"ums_if_data\":" + data
+ " }\r\n" + " }\r\n" + " ]\r\n" + "}";
}
log.debug(rtnVal);
return rtnVal;
}
private String[] makeStdGuidInfo() {
// 리턴값
String rtnVal[] = new String[3];
// 현재시간
Date currentDate = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
String formattedDate = sdf.format(currentDate);
rtnVal[0] = formattedDate.substring(0,8);
rtnVal[1] = formattedDate.substring(8);
// 랜던값
String random20 = this.genrateRandomStringNumber20();
rtnVal[2] = formattedDate + SYS_DVCD + random20;
return rtnVal;
}
private String genrateRandomStringNumber20() {
String rtnVal= "";
StringBuilder buffer= new StringBuilder(10);
// 랜덤값
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
// 랜덤객체 생성
SecureRandom random = new SecureRandom();
// 반복문
for (int i= 0;i< 10;i++) {
// 랜덤수
int randomIndex = random.nextInt(characters.length());
// 생성된 랜덤값
char randomChar = characters.charAt(randomIndex);
// 랜덤값 저장
buffer.append(randomChar);
}
// 랜덤객체 재생성
random = new SecureRandom();
// 난수 10개 생성
int random10 = (int) (Math.pow(10, 9) + random.nextDouble() * Math.pow(10, 9));
// 문자 숫자 조합(10) + 숫자(10)
rtnVal = buffer.toString() + Integer.toString(random10);
//리턴
return rtnVal;
}
private String genrateRandomStringNumber26() {
StringBuilder buffer = new StringBuilder(26);
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ0123456789";
SecureRandom random = new SecureRandom();
for (int i = 0; i < 26; i++) {
buffer.append(characters.charAt(random.nextInt(characters.length())));
}
return buffer.toString();
}
}
@@ -0,0 +1,67 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class EmailDataVO {
/** UMS이벤트ID */
private String ums_evnt_id;
/** 발송요청일자 */
private String snd_dman_dt;
/** UUID */
private String snd_dman_id;
/** 발송요청순번 */
private String snd_dman_sno;
/** 메일제목 */
private String email_titl;
/** 고객번호 */
private String cstno;
/** 발신자 이메일 */
private String sendr_emaddr;
/** 발신자 이름 */
private String sendr_nm;
/** 리턴 이메일 */
private String sndbk_emaddr;
/** 수신자 이메일 */
private String recvr_emaddr;
/** 수신자명 */
private String recvr_nm;
/** 탬플릿매핑정보 */
private String tmplt_mpng_info;
/** 탬플릿매핑반복정보 */
private String tmplt_mpng_rptt_info;
/** 발신자ID */
private String snd_empno;
/** 발신자부서코드 */
private String snd_emp_dprm_cd;
/** 업무구분코드 */
private String bzwk_cd;
/** 업무세부코드 */
private String sub_bzwk_cd;
/** 시스템코드 */
private String sys_dvcd;
/** 거래일자 */
private String tx_dt;
/** 거래시간 */
private String tx_tktm;
}
@@ -0,0 +1,67 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class SmsDataVO {
/** 발송요청일자 */
private String snd_dman_dt;
/** UUID */
private String snd_dman_id;
/** 발송구분 */
private String snd_dvcd;
/** 발송타입 */
private String ltrs_snd_tycd;
/** LMS 제목 */
private String msg_titl;
/** 메세지 */
private String msg_ctnt;
/** 수신자번호 */
private String recvr_no;
/** 발신번호 */
private String sndn_no;
/** 요청자(직원번호) */
private String snd_empno;
/** 요청부서코드 */
private String snd_emp_dprm_cd;
/** 알림톡 메시지 종류 */
private String msg_snd_tycd;
/** 플러스친구아이디 */
private String almtk_snd_prfl_key;
/** 템플릿코드 */
private String almtk_tmplt_cd;
/** 오류자문자발송여부 */
private String almtk_err_ltrs_snd_yn;
/** 카카오톡전송방식 */
private String almtk_btn_trnm_tycd;
/** 업무구분 코드 */
private String sms_trnm_bzwk_dvcd;
/** 업무세부코드 */
private String sms_bzwk_dcls_dvcd;
/** 시스템코드 */
private String sms_sys_dvcd;
/** 거래일자 */
private String tx_dt;
/** 거래시간 */
private String tx_tktm;
}
@@ -0,0 +1,61 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class StdHeadVO {
private String nxgn_stnd_idfr;
private String guid;
private String guid_prgs_no;
private String stnd_mesg_ver;
private String ortr_guid;
private String sect_ecrpt_yn;
private String frst_trnm_ipad;
private String frst_trnm_ipv4_addr;
private String frst_trnm_mac;
private String frst_mesg_dman_dt;
private String frst_mesg_dman_time;
private String frst_trnm_sys_dvcd;
private String trnm_sys_dvcd;
private String dman_rspn_dvcd;
private String sys_env_dvcd;
private String tx_tycd;
private String synz_dvcd;
private String tx_id;
private String osdch_dvcd;
private String chnl_tycd;
private String if_id;
private String hmab_dvcd;
private String tx_brcd;
private String tlrno;
private String insl_brcd;
private String blng_brcd;
private String mgmt_brcd;
}
@@ -0,0 +1,19 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class SwingCommonVO {
/** 클라이업트 D */
private String clientld;
/** 클라이언트 시크릿 */
private String clientSecret;
/** 회사코드 */
private String companyCode;
/** 요청고유키 */
private String requestUniqueKey;
}
@@ -0,0 +1,21 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import java.util.List;
import lombok.Data;
@Data
public class SwingDataVO {
/** 알림코드 */
private String notiCode;
/** 알림제목 */
private String notiTitle;
/** 알림내용 */
private String notiContent;
/** 수신자정보 */
private List<SwingReceiverDataVO> receiverInfo;
}
@@ -0,0 +1,22 @@
package com.eactive.eai.rms.ext.djb.ums.vo;
import lombok.Data;
@Data
public class SwingReceiverDataVO {
/** 알림코드 */
private String name;
/** 알림제목 */
private String type;
/** 알림내용 */
private String companyCode;
/** 수신자정보 */
private String code;
/** 상품명 */
private String prdNm;
}
@@ -267,17 +267,14 @@ public class SmsAuthService {
}
/**
* 전화번호 마스킹
* 전화번호 마스킹 (표준 규칙: 2·3번째 세그먼트 각각 뒤 2자리)
* 예: 010-1234-5678 → 010-12**-56**
*/
public String maskPhoneNumber(String phone) {
if (StringUtils.isEmpty(phone)) {
return "****";
}
String cleaned = phone.replaceAll("[^0-9]", "");
if (cleaned.length() < 7) {
return "****";
}
return cleaned.substring(0, 3) + "****" + cleaned.substring(cleaned.length() - 4);
return com.eactive.eai.common.util.MaskingUtils.maskPhoneNumber(phone);
}
/**
@@ -134,8 +134,8 @@ public class SsoController implements InterceptorSkipController {
log.debug("SSO 사용자 정보 - userId: {}, name: {}, phone: {}, email: {}",
userId,
MaskingUtils.maskName(name),
maskPhoneNumber(cellPhoneNo),
maskEmail(email));
MaskingUtils.maskPhoneNumber(cellPhoneNo),
MaskingUtils.maskEmailId(email));
// 사용자 생성 또는 업데이트
UserInfo userInfo = findOrCreateUser(info, extendedInfo);
@@ -341,33 +341,4 @@ public class SsoController implements InterceptorSkipController {
}
return phone.replaceAll("[^0-9]", "");
}
/**
* 휴대폰번호 마스킹 (로그용)
*/
private String maskPhoneNumber(String phone) {
if (StringUtils.isEmpty(phone) || phone.length() < 4) {
return "****";
}
// 010-1234-5678 → 010-****-5678
String cleaned = phone.replaceAll("[^0-9]", "");
if (cleaned.length() < 7) {
return "****";
}
return cleaned.substring(0, 3) + "****" + cleaned.substring(cleaned.length() - 4);
}
/**
* 이메일 마스킹 (로그용)
*/
private String maskEmail(String email) {
if (StringUtils.isEmpty(email) || !email.contains("@")) {
return "****";
}
int atIndex = email.indexOf("@");
if (atIndex <= 2) {
return "**" + email.substring(atIndex);
}
return email.substring(0, 2) + "**" + email.substring(atIndex);
}
}
@@ -1,83 +1,52 @@
package com.eactive.eai.rms.onl.apim.masking;
import com.eactive.eai.rms.common.util.StringUtils;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 개인정보 마스킹 유틸리티 (eapim-admin)
*
* <p>실제 마스킹 규칙은 보안아키텍처 표준 구현인
* {@link com.eactive.eai.common.util.MaskingUtils}(elink-online-core)에 위임한다.
* 기존 호출부 호환을 위해 메서드명/시그니처는 그대로 유지한다.</p>
*
* <pre>
* 이메일 eact***@gmail.com (구) → **ct1**@gmail.com (앞2·뒤2, 4자 이하 전체)
* 이름 홍길* (구) → 홍*동 (2자 뒤1, 3자↑ 가운데 전체)
* 전화 010-****-5678 (구) → 010-12**-56** (세그먼트별 뒤 2자리)
* IPv4 192.168.***.1 (3번째 옥텟) / IPv6 마지막 그룹, 콤마 다중 IP 지원
* </pre>
*/
public class MaskingUtils {
private MaskingUtils() {}
/**
* 이메일 주소의 ID 부분을 마스킹 처리
* 예: eactive@gmail.com => eact***@gmail.com
* 예: aajjbacc@gmail.com => **jjba**@gmail.com
*/
public static String maskEmailId(String email) {
if (email == null || email.isEmpty() || !email.contains("@")) {
return email;
}
String[] parts = email.split("@");
String id = parts[0];
String domain = parts[1];
if (id.length() <= 3) {
return "***" + "@" + domain;
}
String maskedId = id.substring(0, id.length() - 3) + "***";
return maskedId + "@" + domain;
return com.eactive.eai.common.util.MaskingUtils.maskEmail(email);
}
/**
* 이름지막 글자를 마스킹 처리
* 이름 마스킹 처리 (2자: 뒤 1자리, 3자 이상: 앞뒤 제외 가운데 전체)
* 예: 홍길동 => 홍*동
*/
public static String maskName(String name) {
if (name == null || name.isEmpty() || name.length() < 2) {
return name;
}
return name.substring(0, name.length() - 1) + "*";
return com.eactive.eai.common.util.MaskingUtils.maskKoreanName(name);
}
/**
* 전화번호 마스킹 처리
* 예: 010-1234-5678 => 010-****-5678
* 전화번호/휴대폰/FAX 마스킹 처리 (2·3번째 세그먼트 각각 뒤 2자리)
* 예: 010-1234-5678 => 010-12**-56**
*/
public static String maskPhoneNumber(String phoneNumber) {
if (phoneNumber == null || phoneNumber.isEmpty()) {
return phoneNumber;
}
String[] parts = phoneNumber.split("-");
if (parts.length != 3) {
return phoneNumber;
}
return parts[0] + "-****-" + parts[2];
return com.eactive.eai.common.util.MaskingUtils.maskPhoneNumber(phoneNumber);
}
/**
* IP 주소 마스킹 처리
* 예: 192.168.1.1 => 192.168.*.*
* IP 주소 마스킹 처리 (IPv4 3번째 옥텟 / IPv6 마지막 그룹, 콤마 다중 IP 지원)
* 예: 192.168.1.1 => 192.168.***.1
*/
public static String maskIpAddress(String ipAddresses) {
if (StringUtils.isBlank(ipAddresses)) {
return ipAddresses;
}
String[] ips = ipAddresses.split(",");
List<String> maskedIps = Arrays.stream(ips)
.map(ip -> {
String[] parts = ip.trim().split("\\.");
if (parts.length == 4) {
// 세 번째 octet을 ***로 마스킹
parts[2] = "***";
return String.join(".", parts);
}
return ip; // 유효하지 않은 IP 형식은 그대로 반환
})
.collect(Collectors.toList());
return String.join(",", maskedIps);
return com.eactive.eai.common.util.MaskingUtils.maskIpAddress(ipAddresses);
}
}
@@ -0,0 +1,117 @@
package com.eactive.eai.rms.onl.apim.masking;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 메세지 본문/메신저ID 패턴 기반 마스킹 유틸 (발송 내역 화면 전용).
*
* <p>본문(message) 안에 섞여 있는 개인정보/인증정보를 패턴으로 찾아 마스킹하고,
* 패턴 종류별로 색상 강조 {@code <span>} 으로 감싼 <b>안전한 HTML</b> 을 만든다.
* 매칭되지 않은 영역과 마스킹 결과 모두 HTML escape 하므로 본문에 스크립트가 있어도 무력화된다.</p>
*
* <pre>
* 이메일 test@gmail.com → &lt;span class="mask-email"&gt;**st@gmail.com&lt;/span&gt;
* 휴대폰(하이픈) 010-1234-5678 → &lt;span class="mask-phone"&gt;010-12**-56**&lt;/span&gt;
* 6자리+ 숫자 123456 → &lt;span class="mask-digits"&gt;123***&lt;/span&gt; (앞 3자리 제외)
* 메신저ID kakao_abcd → kakao_ab** (뒤 2자리)
* </pre>
*
* <p>실제 이메일/휴대폰 마스킹 규칙은 표준 구현
* {@link com.eactive.eai.common.util.MaskingUtils}(elink-online-core)에 위임한다.</p>
*/
public final class MessagePatternMaskingUtils {
private MessagePatternMaskingUtils() {}
private static final int DIGITS_KEEP_HEAD = 3;
/**
* 매칭 우선순위(이메일 &gt; 하이픈 휴대폰 &gt; 6자리+ 숫자)를 단일 패스로 보장하는 결합 정규식.
* 휴대폰 하이픈 형식이 "6자리 이상 숫자" 규칙에 이중 적용되지 않도록 한 Matcher 로 순회한다.
*/
private static final Pattern COMBINED = Pattern.compile(
"(?<email>[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})"
+ "|(?<phone>01[016789]-\\d{3,4}-\\d{4})"
+ "|(?<digits>\\d{6,})");
/** 본문 → escape + 패턴별 마스킹/색상강조 span 이 적용된 안전 HTML 반환. */
public static String maskAndHighlight(String raw) {
if (raw == null || raw.isEmpty()) {
return "";
}
Matcher m = COMBINED.matcher(raw);
StringBuilder sb = new StringBuilder();
int last = 0;
while (m.find()) {
sb.append(escapeHtml(raw.substring(last, m.start())));
String token = m.group();
if (m.group("email") != null) {
sb.append(span("mask-email", com.eactive.eai.common.util.MaskingUtils.maskEmail(token)));
} else if (m.group("phone") != null) {
sb.append(span("mask-phone", com.eactive.eai.common.util.MaskingUtils.maskPhoneNumber(token)));
} else {
sb.append(span("mask-digits", maskKeepHead(token, DIGITS_KEEP_HEAD)));
}
last = m.end();
}
sb.append(escapeHtml(raw.substring(last)));
return sb.toString();
}
/** 메신저 ID 마스킹: 뒤 2자리(2자 이하는 전체) 마스킹. */
public static String maskMessengerId(String id) {
if (id == null || id.isEmpty()) {
return id;
}
if (id.length() <= 2) {
return repeat('*', id.length());
}
return id.substring(0, id.length() - 2) + "**";
}
/** 앞 head 자리만 남기고 나머지를 '*' 로 마스킹. */
private static String maskKeepHead(String digits, int head) {
if (digits.length() <= head) {
return digits;
}
return digits.substring(0, head) + repeat('*', digits.length() - head);
}
private static String span(String cls, String inner) {
return "<span class=\"" + cls + "\">" + escapeHtml(inner) + "</span>";
}
/**
* 최소 HTML escape. {@code < > & " '} 만 변환하고 한글 등 비-ASCII 문자는 보존한다.
* (commons-lang {@code escapeHtml} 은 0x7F 초과 문자를 {@code &#NNN;} 로 변환해 한글이 깨지므로 직접 구현)
*/
private static String escapeHtml(String s) {
if (s == null || s.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '&': sb.append("&amp;"); break;
case '<': sb.append("&lt;"); break;
case '>': sb.append("&gt;"); break;
case '"': sb.append("&quot;"); break;
case '\'': sb.append("&#39;"); break;
default: sb.append(c);
}
}
return sb.toString();
}
private static String repeat(char c, int n) {
if (n <= 0) {
return "";
}
char[] a = new char[n];
Arrays.fill(a, c);
return new String(a);
}
}
@@ -0,0 +1,67 @@
package com.eactive.eai.rms.onl.apim.messagerequest;
import com.eactive.eai.rms.common.base.BaseAnnotationController;
import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.eai.rms.data.entity.onl.apim.messagerequest.MessageRequestUISearch;
import lombok.RequiredArgsConstructor;
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 java.util.HashMap;
import java.util.Map;
/**
* 발송 내역(PTL_MESSAGE_REQUEST) 조회 화면 컨트롤러.
*
* <p>조회 전용 + PENDING→FAILED 상태 전환만 제공한다. (등록/수정/삭제 없음)</p>
*/
@Controller
@RequiredArgsConstructor
public class MessageRequestManController extends BaseAnnotationController {
private final MessageRequestManService messageRequestManService;
@GetMapping(value = "/onl/apim/messagerequest/messageRequestMan.view")
public void view() {
// 목록 view
}
@GetMapping(value = "/onl/apim/messagerequest/messageRequestMan.view", params = "cmd=DETAIL")
public String detailView() {
return "/onl/apim/messagerequest/messageRequestManDetail";
}
@PostMapping(value = "/onl/apim/messagerequest/messageRequestMan.json", params = "cmd=LIST")
public ResponseEntity<GridResponse<MessageRequestUI>> selectList(
@SortDefault(sort = "requestDate", direction = Sort.Direction.DESC) Pageable pageable,
MessageRequestUISearch search) {
Page<MessageRequestUI> page = messageRequestManService.selectList(pageable, search);
return ResponseEntity.ok(new GridResponse<>(page));
}
@PostMapping(value = "/onl/apim/messagerequest/messageRequestMan.json", params = "cmd=LIST_INIT_COMBO")
public ResponseEntity<Map<String, Object>> initCombo() {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("statusList", messageRequestManService.statusCombo());
resultMap.put("messageCodeList", messageRequestManService.messageCodeCombo());
return ResponseEntity.ok(resultMap);
}
@PostMapping(value = "/onl/apim/messagerequest/messageRequestMan.json", params = "cmd=DETAIL")
public ResponseEntity<MessageRequestUI> selectDetail(String id) {
return ResponseEntity.ok(messageRequestManService.selectDetail(id));
}
/** PENDING 건을 FAILED 로 전환 (감사 포인트: UPDATE_STATUS). */
@PostMapping(value = "/onl/apim/messagerequest/messageRequestMan.json", params = "cmd=UPDATE_STATUS")
public ResponseEntity<Void> updateStatus(String id) {
messageRequestManService.updateStatusToFailed(id);
return ResponseEntity.ok().build();
}
}
@@ -0,0 +1,106 @@
package com.eactive.eai.rms.onl.apim.messagerequest;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.combo.ComboVo;
import com.eactive.eai.rms.data.entity.onl.apim.messagerequest.MessageRequestUISearch;
import com.eactive.eai.rms.onl.apim.masking.MaskingUtils;
import com.eactive.eai.rms.onl.apim.masking.MessagePatternMaskingUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class MessageRequestManService extends BaseService {
// KST 한글 요일 포맷 (표시용 / 툴팁용)
private static final DateTimeFormatter FMT_SHORT = DateTimeFormatter.ofPattern("MM-dd(E) HH:mm", Locale.KOREA);
private static final DateTimeFormatter FMT_FULL = DateTimeFormatter.ofPattern("yyyy-MM-dd (EEE) HH:mm:ss", Locale.KOREA);
private final com.eactive.eai.rms.data.entity.onl.apim.messagerequest.MessageRequestService messageRequestService;
@Autowired
public MessageRequestManService(com.eactive.eai.rms.data.entity.onl.apim.messagerequest.MessageRequestService messageRequestService) {
this.messageRequestService = messageRequestService;
}
public Page<MessageRequestUI> selectList(Pageable pageable, MessageRequestUISearch search) {
return messageRequestService.findAll(pageable, search).map(this::convertToUI);
}
public MessageRequestUI selectDetail(String id) {
return convertToUI(messageRequestService.getById(id));
}
/** PENDING → FAILED 전환. */
public void updateStatusToFailed(String id) {
messageRequestService.updateStatusToFailedIfPending(id);
}
/** 엔티티 → 마스킹/포맷 적용된 UI 변환 (목록·상세 공통). */
private MessageRequestUI convertToUI(MessageRequest e) {
MessageRequestUI ui = new MessageRequestUI();
ui.setId(e.getId());
ui.setMessageCode(e.getMessageCode() != null ? e.getMessageCode().name() : null);
ui.setMessageCodeName(e.getMessageCode() != null ? e.getMessageCode().getDescription() : null);
ui.setSubject(e.getSubject());
ui.setMessageType(e.getMessageType());
// 본문: 패턴 마스킹 + 색상강조 안전 HTML
ui.setMessageHtml(MessagePatternMaskingUtils.maskAndHighlight(e.getMessage()));
// 수신자 마스킹 (email/phone 은 @Convert 로 이미 복호화된 평문)
ui.setUsername(MaskingUtils.maskName(e.getUsername()));
ui.setEmail(MaskingUtils.maskEmailId(e.getEmail()));
ui.setPhone(MaskingUtils.maskPhoneNumber(e.getPhone()));
ui.setMessengerId(MessagePatternMaskingUtils.maskMessengerId(e.getMessengerId()));
ui.setUmsUid(e.getUmsUid());
ui.setRequestStatus(e.getRequestStatus());
ui.setEaiInterfaceId(e.getEaiInterfaceId());
ui.setServiceId(e.getServiceId());
ui.setRequestDateShort(fmt(e.getRequestDate(), FMT_SHORT));
ui.setRequestDateFull(fmt(e.getRequestDate(), FMT_FULL));
ui.setSentDateShort(fmt(e.getSentDate(), FMT_SHORT));
ui.setSentDateFull(fmt(e.getSentDate(), FMT_FULL));
return ui;
}
private static String fmt(LocalDateTime t, DateTimeFormatter f) {
return t == null ? "" : t.format(f);
}
/** 상태 검색 콤보 (PENDING/SENT/FAILED). */
public List<ComboVo> statusCombo() {
return Arrays.stream(new String[]{"PENDING", "SENT", "FAILED"})
.map(s -> {
ComboVo v = new ComboVo();
v.setCode(s);
v.setCodename(s);
return v;
}).collect(Collectors.toList());
}
/** 메세지 코드 검색 콤보 (enum name → 한글 설명). */
public List<ComboVo> messageCodeCombo() {
return Arrays.stream(MessageCode.values())
.map(c -> {
ComboVo v = new ComboVo();
v.setCode(c.name());
v.setCodename(c.getDescription());
return v;
}).collect(Collectors.toList());
}
}
@@ -0,0 +1,42 @@
package com.eactive.eai.rms.onl.apim.messagerequest;
import lombok.Data;
/**
* 발송 내역(PTL_MESSAGE_REQUEST) 화면 응답 DTO.
*
* <p>개인정보는 모두 마스킹된 값으로 채워지며, 본문(messageHtml)은 패턴 마스킹 + 색상강조가 적용된
* 안전한 HTML 이다. 날짜는 표시용(short)/툴팁용(full) 두 가지 포맷 문자열로 제공한다.</p>
*/
@Data
public class MessageRequestUI {
private String id;
/* 제목/코드 — subject 가 없으면 화면에서 messageCode 로 대체 표기 */
private String messageCode; // MessageCode enum name (제목 마우스오버/대체용)
private String messageCodeName; // MessageCode.getDescription() 한글명
private String subject;
private String messageType; // EMAIL/SMS 등
/* 본문 — 패턴 마스킹 + span 색상강조된 안전 HTML */
private String messageHtml;
/* 수신자 (마스킹) */
private String username;
private String email;
private String phone;
private String messengerId;
/* 발송 메타 */
private String umsUid;
private String requestStatus; // PENDING/SENT/FAILED
private String eaiInterfaceId;
private String serviceId;
/* 요청/발송 시간 — 표시용(short) / 툴팁용(full) */
private String requestDateShort; // MM-dd(E) HH:mm
private String requestDateFull; // yyyy-MM-dd (EEE) HH:mm:ss
private String sentDateShort;
private String sentDateFull;
}
@@ -80,4 +80,10 @@ public class PortalNoticeManController extends BaseAnnotationController {
return ResponseEntity.ok().build();
}
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=DELETE_BULK")
public ResponseEntity<String> deleteBulk(String ids) {
portalNoticeManService.deleteBulk(ids);
return ResponseEntity.ok().build();
}
}
@@ -33,6 +33,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -282,4 +283,20 @@ public class PortalNoticeManService extends BaseService {
portalNoticeService.deleteById(id);
}
public void deleteBulk(String ids) {
if (StringUtils.isBlank(ids)) {
return;
}
List<String> idList = Arrays.asList(ids.split(","));
// 첨부파일 정리 (단건 삭제와 동일한 처리)
idList.forEach(id -> {
PortalNotice portalNotice = portalNoticeService.getById(id);
Optional.ofNullable(portalNotice.getFileId()).ifPresent(fileService::deleteFile);
});
portalNoticeService.deleteByIds(idList);
}
}
@@ -64,12 +64,13 @@ public class PortalNoticeUI {
private String summary;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
// HTML <input type="datetime-local"> 는 'T' 구분자 형식 (예: 2026-06-16T10:29).
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm")
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm")
private LocalDateTime startedAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm")
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm")
private LocalDateTime endAt;
private String state; // INCIDENT 한정 (INVESTIGATING/IDENTIFIED/MONITORING/RESOLVED/CANCELED)
@@ -55,4 +55,16 @@ public class PortalPartnershipManController extends BaseAnnotationController {
return ResponseEntity.ok(portalPartnershipUI);
}
@PostMapping(value = "/onl/apim/portalpartnership/portalPartnershipMan.json", params = "cmd=DELETE")
public ResponseEntity<Void> delete(String id) {
portalPartnershipManService.delete(id);
return ResponseEntity.ok().build();
}
@PostMapping(value = "/onl/apim/portalpartnership/portalPartnershipMan.json", params = "cmd=DELETE_BULK")
public ResponseEntity<Void> deleteBulk(String ids) {
portalPartnershipManService.deleteBulk(ids);
return ResponseEntity.ok().build();
}
}
@@ -15,6 +15,10 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalPartnershipManService extends BaseService {
@@ -51,18 +55,48 @@ public class PortalPartnershipManService extends BaseService {
return convertToUIWithMaskOption(partnershipApplication, false);
}
public void delete(String id) {
PartnershipApplication partnership = partnershipService.getById(id);
Optional.ofNullable(partnership.getFileId())
.filter(StringUtils::isNotBlank)
.ifPresent(fileService::deleteFile);
partnershipService.deleteById(id);
}
public void deleteBulk(String ids) {
if (StringUtils.isBlank(ids)) {
return;
}
List<String> idList = Arrays.asList(ids.split(","));
// 첨부파일 정리 (단건 삭제와 동일한 처리)
idList.forEach(id -> {
PartnershipApplication partnership = partnershipService.getById(id);
Optional.ofNullable(partnership.getFileId())
.filter(StringUtils::isNotBlank)
.ifPresent(fileService::deleteFile);
});
partnershipService.deleteByIds(idList);
}
private PortalPartnershipUI convertToUIWithMaskOption(PartnershipApplication partnership, boolean isMasked) {
PortalPartnershipUI partnershipUI = portalPartnershipUIMapper.toVo(partnership);
PortalUser user = findUser(partnership.getCreatedBy());
// 작성자(createdBy)에 해당하는 포털 사용자가 없을 수 있음(탈퇴/삭제, 시스템 작성 등)
String userName = user != null ? user.getUserName() : null;
String loginId = user != null ? user.getLoginId() : null;
if (isMasked) {
// 마스킹 처리
partnershipUI.setCreatedByName(MaskingUtils.maskName(user.getUserName()));
partnershipUI.setCreatedById(MaskingUtils.maskEmailId(user.getLoginId()));
partnershipUI.setCreatedByName(MaskingUtils.maskName(userName));
partnershipUI.setCreatedById(MaskingUtils.maskEmailId(loginId));
} else {
// 마스킹 해제 상태
partnershipUI.setCreatedByName(user.getUserName());
partnershipUI.setCreatedById(user.getLoginId());
partnershipUI.setCreatedByName(userName);
partnershipUI.setCreatedById(loginId);
}
// 첨부파일 처리
@@ -7,6 +7,7 @@ import com.eactive.apim.portal.agreements.service.AgreementsService;
import com.eactive.apim.portal.file.entity.FileDetail;
import com.eactive.apim.portal.file.entity.FileInfo;
import com.eactive.apim.portal.file.service.FileService;
import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
import com.eactive.eai.rms.data.entity.onl.apim.portalterms.PortalTermsService;
@@ -49,7 +50,14 @@ public class PortalTermsManService extends BaseService {
}
private String findName(String userId) {
return userInfoService.getById(userId).getUsername();
if (StringUtils.isBlank(userId)) {
return "";
}
// 등록자/수정자 ID로 사용자를 찾지 못해도(삭제된 사용자, 레거시 시드 데이터 등)
// 화면 전체가 깨지지 않도록 방어적으로 조회한다. (PortalFaq/PortalInquiry 컨벤션과 동일)
return userInfoService.findById(userId)
.map(UserInfo::getUsername)
.orElse("Unknown");
}
public Page<PortalTermsUI> selectList(Pageable pageable, String searchAgreementsType) {
@@ -1,10 +1,10 @@
package com.eactive.eai.rms.onl.common.controller;
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
import com.eactive.eai.rms.common.scheduler.EMSScheduler;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterJobInfo;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterSchedulerUtil;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.SpcfTmAdapterDao;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.quartz.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,9 +15,11 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
import com.eactive.eai.rms.common.scheduler.EMSScheduler;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterJobInfo;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterSchedulerUtil;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.SpcfTmAdapterDao;
@Controller
public class SchedulerReloadController implements InterceptorSkipController {
@@ -34,19 +36,14 @@ public class SchedulerReloadController implements InterceptorSkipController {
private EMSScheduler emsScheduler;
@Autowired
@Qualifier("scheduler")
@Qualifier("scheduler")
private Scheduler scheduler;
@Autowired
@Qualifier("clusteredScheduler")
private Scheduler clusteredScheduler;
@RequestMapping("/schedulerReload.do")
public ResponseEntity<Map<String, Object>> rmsSchedulerReload(String jobName) {
Map<String, Object> result = new HashMap<>();
try {
emsScheduler.deleteAndAddJob(scheduler, jobName);
emsScheduler.deleteAndAddJob(clusteredScheduler, jobName);
emsScheduler.reloadJob(jobName);
result.put("status", "success");
} catch (Exception e) {
logger.error("", e);
@@ -19,9 +19,9 @@ import org.springframework.transaction.support.TransactionTemplate;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.apim.portal.template.repository.MessageRequestRepository;
import com.eactive.ext.djb.ums.DjbMessengerService;
import com.eactive.ext.kjb.ums.KjbEmailSendModule;
import com.eactive.ext.kjb.ums.KjbUmsService;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.ext.djb.ums.service.UmsService;
import lombok.extern.slf4j.Slf4j;
@@ -43,13 +43,17 @@ public class UmsDispatchService {
private EntityManager entityManager;
private final MessageRequestRepository messageRequestRepository;
private final UmsService umsService;
@Autowired
public UmsDispatchService(
@Qualifier("transactionManagerForEMS") PlatformTransactionManager transactionManager,
MessageRequestRepository messageRequestRepository
MessageRequestRepository messageRequestRepository,
UmsService umsService
) {
this.messageRequestRepository = messageRequestRepository;
this.umsService = umsService;
this.executorService = new ThreadPoolExecutor(
CORE_POOL_SIZE,
@@ -71,17 +75,6 @@ public class UmsDispatchService {
log.info("UmsDispatchService initialized - KjbUmsService and KjbEmailSendModule ready via singleton");
}
private DjbMessengerService getMessengerService() {
return DjbMessengerService.getInstance();
}
private KjbUmsService getUmsService() {
return KjbUmsService.getInstance();
}
private KjbEmailSendModule getEmailModule() {
return KjbEmailSendModule.getInstance();
}
@SuppressWarnings("unchecked")
@Transactional(transactionManager = "transactionManagerForEMS")
@@ -138,15 +131,7 @@ public class UmsDispatchService {
try {
transactionTemplate.execute(status -> {
try {
if (DjbMessengerService.isAllowChannel(message)) {
getMessengerService().send(message);
}
if (KjbUmsService.isAllowChannel(message)) {
getUmsService().send(message);
}
if (KjbEmailSendModule.isAllowChannel(message)) {
getEmailModule().sendEmail(message);
}
umsService.send(message);
updateMessageStatus(message, "SENT");
return null;
} catch (Exception e) {
@@ -174,5 +159,6 @@ public class UmsDispatchService {
message.setRequestStatus(status);
message.setSentDate(LocalDateTime.now());
entityManager.merge(message);
entityManager.flush();
}
}
@@ -19,6 +19,7 @@ import com.eactive.eai.agent.encryption.ReloadEncryptionYNCommand;
import com.eactive.eai.agent.property.ReloadPropertyCommand;
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
import com.eactive.eai.rms.common.vo.GridResponse;
import com.eactive.ext.djb.DamoManager;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -151,5 +152,12 @@ public class PropertyController extends OnlBaseAnnotationController {
resultMap.put("merge", lmerge);
return new ModelAndView("excelView", resultMap);
}
@PostMapping(value = "/onl/admin/common/propertyMan.json", params = "cmd=ENCRYPT")
public ResponseEntity<String> encrypt(PropertyUI propertyForm) throws Exception {
String encrypted = DamoManager.getInstance().encrypt(propertyForm.getPrpty2Val());
return ResponseEntity.ok(encrypted);
}
}
@@ -279,11 +279,7 @@ public class InflowGroupControlManService extends BaseService {
for (Map<String, String> server : serverList) {
String serverName = server.get("EAISVRINSTNM");
String serverIp = server.get("EAISVRIP");
String serverPort = server.get("WEBSERVERPORT");
if (StringUtils.isEmpty(serverPort)) {
serverPort = server.get("EAISVRLSNPORT");
}
String serverPort = server.get("EAISVRLSNPORT");
Map<String, Object> serverStatus = new HashMap<>();
serverStatus.put("serverName", serverName);
@@ -164,11 +164,7 @@ public class InflowControlManService extends BaseService {
for (Map<String, String> server : serverList) {
String serverName = server.get("EAISVRINSTNM");
String serverIp = server.get("EAISVRIP");
String serverPort = server.get("WEBSERVERPORT");
if (StringUtils.isEmpty(serverPort)) {
serverPort = server.get("EAISVRLSNPORT");
}
String serverPort = server.get("EAISVRLSNPORT");
Map<String, Object> serverStatus = new HashMap<>();
serverStatus.put("serverName", serverName);
@@ -565,7 +565,7 @@ public class ApiInterfaceService extends OnlBaseService {
restOption.setMethod(outboundHttpMethod);
restOption.setExtraPath(outboundRestPath);
String pathType = outboundRestPath.matches(".*\\{[_a-zA-Z]+\\}.*") ? "variableUrlRequest" : "simpleRequest";
String pathType = outboundRestPath.matches(".*\\{.+\\}.*") ? "variableUrlRequest" : "simpleRequest";
restOption.setType(pathType);
if (StringUtils.isNotBlank(contentType)) {
@@ -400,7 +400,7 @@ public class SimpleInterfaceService extends OnlBaseService {
jsonObject.put("method", outboundHttpMethod);
jsonObject.put("extraPath", outboundRestPath);
String pathType = outboundRestPath.matches(".*\\{[a-zA-Z]+\\}.*") ? "variableUrlRequest" : "simpleRequest";
String pathType = outboundRestPath.matches(".*\\{.+\\}.*") ? "variableUrlRequest" : "simpleRequest";
jsonObject.put("type", pathType);
restOption = jsonObject.toString();
return restOption;
@@ -23,7 +23,6 @@
FLOVRSEVRNAME AS "FAILOVERSVRNM",
HOSTNAME AS "HOSTNAME"
FROM $schemaId$.TSEAIBP03
WHERE ROWNUM = 1
</statement>
</sqlMap>
+10 -3
View File
@@ -5,13 +5,20 @@
# \uC790\uB3D9 \uD56B\uC2A4\uC651 \uD65C\uC131\uD654
autoHotswap=true
# \uB85C\uAE45 \uB808\uBCA8 (TRACE, DEBUG, INFO, WARNING, ERROR)
LOGGER.level=INFO
# \uB85C\uADF8 \uB808\uBCA8 (TRACE, DEBUG, INFO, WARNING, ERROR)
LOGGER=INFO
# \uB85C\uADF8 \uD30C\uC77C \uACBD\uB85C (\uC9C0\uC815 \uC2DC \uCF58\uC194 \uB300\uC2E0 \uD30C\uC77C\uB85C \uCD9C\uB825)
LOGFILE=/Users/rinjae/eactive/djb-eapim/djb-eapim/eapim-admin/logs/hotswap-agent.log
LOGFILE.append=true
# \uC2DC\uAC01 \uD3EC\uB9F7
LOGGER_DATETIME_FORMAT=yyyy-MM-dd HH:mm:ss.SSS
# ============================================================
# \uBA85\uC2DC\uC801 \uD50C\uB7EC\uADF8\uC778 \uBE44\uD65C\uC131\uD654 (\uC27C\uD45C \uAD6C\uBD84)
# ============================================================
disabledPlugins=org.hotswap.agent.plugin.ibatis.IBatisPlugin,org.hotswap.agent.plugin.mybatis.MyBatisPlugin
disabledPlugins=IBatis,MyBatis
# ============================================================
# Core Plugin \uD65C\uC131\uD654
+1 -1
View File
@@ -4,7 +4,7 @@
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
<property name="PREFIX" value="${inst.Type:-ems}" />
<property name="LOG_HOME" value="/Log/App/eapim/${inst.Name:-devSvr00}" />
<property name="LOG_HOME" value="/logs/eapim/${inst.Name:-emsSvr00}" />
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-DEBUG}" />
+1 -1
View File
@@ -4,7 +4,7 @@
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
<property name="PREFIX" value="${inst.Type:-ems}" />
<property name="LOG_HOME" value="/Log/App/eapim/${inst.Name:-devSvr00}" />
<property name="LOG_HOME" value="/logs/eapim/${inst.Name:-devSvr00}" />
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-DEBUG}" />
+1 -1
View File
@@ -4,7 +4,7 @@
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
<property name="PREFIX" value="${inst.Type:-ems}" />
<property name="LOG_HOME" value="/Log/App/eapim/${inst.Name:-emsSvr00}" />
<property name="LOG_HOME" value="/logs/eapim/${inst.Name:-emsSvr00}" />
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-INFO}" />
@@ -0,0 +1,128 @@
package com.eactive.eai.rms.onl.apim.masking;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* eapim-admin {@link MaskingUtils}가 보안아키텍처 표준 규칙(elink-online-core)으로
* 위임되어 동작하는지 검증한다.
*/
class MaskingUtilsTest {
// --- 이메일 (maskEmailId) ---
@Test
@DisplayName("이메일 — 아이디 앞2·뒤2 마스킹")
void maskEmailId_normal() {
assertEquals("**st1**@test.com", MaskingUtils.maskEmailId("test123@test.com"));
}
@Test
@DisplayName("이메일 — 아이디 4자: 전체 마스킹")
void maskEmailId_fourChars() {
assertEquals("****@test.com", MaskingUtils.maskEmailId("test@test.com"));
}
@Test
@DisplayName("이메일 — 아이디 1자: 전체 마스킹")
void maskEmailId_oneChar() {
assertEquals("*@123.com", MaskingUtils.maskEmailId("1@123.com"));
}
@Test
@DisplayName("이메일 — @ 없으면 원본 / null·빈문자 방어")
void maskEmailId_invalidAndNull() {
assertEquals("notanemail", MaskingUtils.maskEmailId("notanemail"));
assertNull(MaskingUtils.maskEmailId(null));
assertEquals("", MaskingUtils.maskEmailId(""));
}
// --- 이름 (maskName) ---
@Test
@DisplayName("이름 — 2자: 뒤 1자리")
void maskName_2chars() {
assertEquals("가*", MaskingUtils.maskName("가나"));
}
@Test
@DisplayName("이름 — 3자: 가운데 1자리")
void maskName_3chars() {
assertEquals("가*다", MaskingUtils.maskName("가나다"));
}
@Test
@DisplayName("이름 — 4자: 앞뒤 제외 가운데 전체")
void maskName_4chars() {
assertEquals("가**라", MaskingUtils.maskName("가나다라"));
}
@Test
@DisplayName("이름 — 5자: 앞뒤 제외 가운데 전체")
void maskName_5chars() {
assertEquals("가***마", MaskingUtils.maskName("가나다라마"));
}
@Test
@DisplayName("이름 — 1자/null/빈문자 방어")
void maskName_oneAndNull() {
assertEquals("", MaskingUtils.maskName(""));
assertNull(MaskingUtils.maskName(null));
assertEquals("", MaskingUtils.maskName(""));
}
// --- 전화/휴대폰/Fax (maskPhoneNumber) ---
@Test
@DisplayName("전화 — 2·3번째 세그먼트 각 뒤 2자리")
void maskPhoneNumber_normal() {
assertEquals("010-12**-12**", MaskingUtils.maskPhoneNumber("010-1234-1234"));
assertEquals("064-12**-56**", MaskingUtils.maskPhoneNumber("064-1234-5678"));
}
@Test
@DisplayName("전화 — 하이픈 없는 11자리")
void maskPhoneNumber_noHyphen() {
assertEquals("01012**56**", MaskingUtils.maskPhoneNumber("01012345678"));
}
@Test
@DisplayName("전화 — null/빈문자 방어")
void maskPhoneNumber_null() {
assertNull(MaskingUtils.maskPhoneNumber(null));
assertEquals("", MaskingUtils.maskPhoneNumber(""));
}
// --- IP (maskIpAddress) ---
@Test
@DisplayName("IPv4 — 3번째 옥텟 마스킹")
void maskIpAddress_ipv4() {
assertEquals("172.0.***.1", MaskingUtils.maskIpAddress("172.0.0.1"));
assertEquals("192.168.***.1", MaskingUtils.maskIpAddress("192.168.100.1"));
}
@Test
@DisplayName("IPv6 — 마지막 그룹 마스킹")
void maskIpAddress_ipv6() {
assertEquals(
"AAAA:BBBB:CCCC:DDDD:EEEE:FFFF:0000:****",
MaskingUtils.maskIpAddress("AAAA:BBBB:CCCC:DDDD:EEEE:FFFF:0000:1111"));
}
@Test
@DisplayName("IP — 쉼표 구분 다중 IPv4")
void maskIpAddress_multiple() {
assertEquals("192.168.***.1,10.0.***.2", MaskingUtils.maskIpAddress("192.168.100.1,10.0.0.2"));
}
@Test
@DisplayName("IP — null/빈문자 방어")
void maskIpAddress_null() {
assertNull(MaskingUtils.maskIpAddress(null));
assertEquals("", MaskingUtils.maskIpAddress(""));
}
}
@@ -0,0 +1,135 @@
package com.eactive.eai.rms.onl.apim.masking;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* 메세지 본문/메신저ID 패턴 마스킹 + 색상강조 + XSS escape 검증.
*/
class MessagePatternMaskingUtilsTest {
// --- 이메일 ---
@Test
@DisplayName("본문 이메일 — mask-email span + 표준 이메일 마스킹")
void maskAndHighlight_email() {
assertEquals("<span class=\"mask-email\">**st1**@test.com</span>",
MessagePatternMaskingUtils.maskAndHighlight("test123@test.com"));
}
// --- 휴대폰(하이픈) ---
@Test
@DisplayName("본문 휴대폰(하이픈) — mask-phone span + 세그먼트별 뒤2자리 마스킹")
void maskAndHighlight_phoneHyphen() {
assertEquals("<span class=\"mask-phone\">010-12**-56**</span>",
MessagePatternMaskingUtils.maskAndHighlight("010-1234-5678"));
}
// --- 6자리 이상 숫자 (앞 3자리 제외 전체 마스킹) ---
@Test
@DisplayName("본문 6자리 숫자 — 앞 3자리 제외 전체 마스킹(mask-digits)")
void maskAndHighlight_digits6() {
assertEquals("<span class=\"mask-digits\">123***</span>",
MessagePatternMaskingUtils.maskAndHighlight("123456"));
}
@Test
@DisplayName("본문 하이픈 없는 휴대폰(11자리) — 6자리+ 숫자 규칙 적용")
void maskAndHighlight_phoneNoHyphen() {
assertEquals("<span class=\"mask-digits\">010********</span>",
MessagePatternMaskingUtils.maskAndHighlight("01012345678"));
}
@Test
@DisplayName("5자리 이하 숫자는 마스킹하지 않음")
void maskAndHighlight_digitsUnder6() {
assertEquals("12345", MessagePatternMaskingUtils.maskAndHighlight("12345"));
}
// --- 우선순위 / 혼합 ---
@Test
@DisplayName("하이픈 휴대폰이 숫자 규칙에 이중 적용되지 않음 (phone 우선)")
void maskAndHighlight_priority() {
String result = MessagePatternMaskingUtils.maskAndHighlight("010-1234-5678");
assertTrue(result.contains("mask-phone"));
assertFalse(result.contains("mask-digits"));
}
@Test
@DisplayName("텍스트 + 이메일/휴대폰/숫자 혼합 — 각 패턴별 span 생성, 일반 텍스트는 보존")
void maskAndHighlight_mixed() {
String result = MessagePatternMaskingUtils.maskAndHighlight(
"인증요청 test123@test.com 전화 010-1234-5678 코드 123456 끝");
assertTrue(result.contains("<span class=\"mask-email\">**st1**@test.com</span>"));
assertTrue(result.contains("<span class=\"mask-phone\">010-12**-56**</span>"));
assertTrue(result.contains("<span class=\"mask-digits\">123***</span>"));
assertTrue(result.contains("인증요청"));
assertTrue(result.contains(""));
// 원문 일부가 그대로 노출되지 않아야 함
assertFalse(result.contains("test123@"));
assertFalse(result.contains("010-1234-5678"));
assertFalse(result.contains("123456"));
}
// --- XSS / HTML escape ---
@Test
@DisplayName("매칭 외 영역의 HTML 태그는 escape 되어 무력화")
void maskAndHighlight_escapesHtml() {
assertEquals("&lt;b&gt;hi&lt;/b&gt;",
MessagePatternMaskingUtils.maskAndHighlight("<b>hi</b>"));
}
@Test
@DisplayName("script 태그 escape")
void maskAndHighlight_escapesScript() {
String result = MessagePatternMaskingUtils.maskAndHighlight("<script>alert(1)</script>");
assertFalse(result.contains("<script>"));
assertTrue(result.contains("&lt;script&gt;"));
}
@Test
@DisplayName("앰퍼샌드 escape")
void maskAndHighlight_escapesAmp() {
assertEquals("a&amp;b", MessagePatternMaskingUtils.maskAndHighlight("a&b"));
}
// --- null / empty ---
@Test
@DisplayName("null·빈문자 본문은 빈문자 반환")
void maskAndHighlight_nullEmpty() {
assertEquals("", MessagePatternMaskingUtils.maskAndHighlight(null));
assertEquals("", MessagePatternMaskingUtils.maskAndHighlight(""));
}
// --- 메신저 ID ---
@Test
@DisplayName("메신저ID — 뒤 2자리 마스킹")
void maskMessengerId_normal() {
assertEquals("kakao_ab**", MessagePatternMaskingUtils.maskMessengerId("kakao_abcd"));
}
@Test
@DisplayName("메신저ID — 2자 이하 전체 마스킹")
void maskMessengerId_short() {
assertEquals("**", MessagePatternMaskingUtils.maskMessengerId("ab"));
assertEquals("*", MessagePatternMaskingUtils.maskMessengerId("a"));
}
@Test
@DisplayName("메신저ID — null·빈문자 방어")
void maskMessengerId_nullEmpty() {
assertNull(MessagePatternMaskingUtils.maskMessengerId(null));
assertEquals("", MessagePatternMaskingUtils.maskMessengerId(""));
}
}