Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7c8e7f41c | |||
| 6c0fca22f3 | |||
| 18489e2440 | |||
| 681e4bd014 | |||
| 27abe51b7b | |||
| 55ef71c5e7 | |||
| 235f4b67b3 | |||
| 89c4b89a71 | |||
| 7193070ebd | |||
| 49616cdd84 | |||
| d1683cc319 | |||
| b5ba2c2bee | |||
| baa528a628 | |||
| f49814b2a1 | |||
| 0ea73aa66f | |||
| ed44183de3 | |||
| aee02cc2a6 | |||
| 99b4470ffe | |||
| d9201eb603 | |||
| 185a23d748 | |||
| 99a440094d | |||
| 37380f2975 | |||
| 908b2ebcc8 | |||
| 25a8e13752 | |||
| 7928a0c27b | |||
| 65ba6c1115 | |||
| 2dcf524cd6 | |||
| bb991cea81 | |||
| 702a9a899b | |||
| 7520b96ed5 | |||
| 7935e69871 | |||
| 4cf6dc4da0 | |||
| a5d55f2647 | |||
| af8b925e65 | |||
| 62ae2d93ff | |||
| 124ecabccf | |||
| c521835633 | |||
| fe201b88cd | |||
| 9f81ad697d | |||
| ca94f2e70a | |||
| 97937a8a35 | |||
| a8e124db5d | |||
| a514b9d7a9 | |||
| 2ac61636a0 | |||
| 399bd9c58d | |||
| b8beeb7c3e | |||
| 0bb00b4fb9 | |||
| 655c35b1e1 | |||
| efd1c748ed | |||
| 64a9c7408e | |||
| 8f9411f990 | |||
| ed1f55c534 | |||
| d1392821ef | |||
| 6542b0c0f8 | |||
| f7de1b6885 | |||
| 3e32220061 | |||
| 10db8129eb | |||
| e34829437d | |||
| ef3f0e802a | |||
| b741a7e563 | |||
| e8069ef371 | |||
| c0e218451e | |||
| af63b1a256 | |||
| 02072df973 |
Vendored
+180
@@ -0,0 +1,180 @@
|
||||
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 > eapim-admin.war.sha1
|
||||
sha256sum eapim-admin.war > eapim-admin.war.sha256
|
||||
md5sum eapim-admin.war > eapim-admin.war.md5
|
||||
'''
|
||||
archiveArtifacts artifacts: 'build/libs/eapim-admin.war,build/libs/eapim-admin.war.sha1,build/libs/eapim-admin.war.sha256,build/libs/eapim-admin.war.md5', fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('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
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
|
||||
<%@ page import="java.lang.reflect.Method" %>
|
||||
<%@ page import="org.apache.commons.lang3.StringUtils" %>
|
||||
<%@ page import="com.eactive.eai.rms.common.login.SessionManager" %>
|
||||
<%!
|
||||
// 최소 HTML 이스케이프 (진단 페이지 XSS 방지)
|
||||
private String esc(String s) {
|
||||
if (s == null) return "";
|
||||
StringBuilder b = new StringBuilder(s.length() + 16);
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '&': b.append("&"); break;
|
||||
case '<': b.append("<"); break;
|
||||
case '>': b.append(">"); break;
|
||||
case '"': b.append("""); break;
|
||||
case '\'': b.append("'"); break;
|
||||
default: b.append(c);
|
||||
}
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
%>
|
||||
<%
|
||||
// 로그인 사용자만 접근 — real 모드에서 운영 키 암복호화 오라클이 되지 않도록 보호
|
||||
if (StringUtils.isBlank(SessionManager.getUserId(request))
|
||||
&& StringUtils.isBlank(SessionManager.getRoleIdString(request))) {
|
||||
response.sendRedirect(request.getContextPath() + "/loginForm.do");
|
||||
return;
|
||||
}
|
||||
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
request.setCharacterEncoding("utf-8");
|
||||
|
||||
String op = request.getParameter("op");
|
||||
String input = request.getParameter("input");
|
||||
boolean run = request.getParameter("run") != null;
|
||||
if (op == null) op = "encrypt";
|
||||
if (input == null) input = "";
|
||||
|
||||
String mode = "UNKNOWN";
|
||||
String revision = "-";
|
||||
String returnCode = "-";
|
||||
String result = "";
|
||||
String error = "";
|
||||
boolean loaded = false;
|
||||
|
||||
try {
|
||||
Class<?> cls = Class.forName("com.eactive.ext.djb.DamoManager");
|
||||
Object damo = cls.getMethod("getInstance").invoke(null);
|
||||
loaded = true;
|
||||
|
||||
boolean bypass = (Boolean) cls.getMethod("isBypassMode").invoke(damo);
|
||||
boolean fake = (Boolean) cls.getMethod("isFakeMode").invoke(damo);
|
||||
mode = bypass ? "BYPASS" : (fake ? "FAKE" : "REAL");
|
||||
revision = String.valueOf(cls.getMethod("getRevision").invoke(damo));
|
||||
returnCode = String.valueOf(cls.getMethod("getReturnCode").invoke(damo));
|
||||
|
||||
if (run) {
|
||||
if ("encrypt".equals(op)) {
|
||||
result = (String) cls.getMethod("encrypt", String.class).invoke(damo, input);
|
||||
} else if ("decrypt".equals(op)) {
|
||||
result = (String) cls.getMethod("decrypt", String.class).invoke(damo, input);
|
||||
} else if ("sha256".equals(op)) {
|
||||
int t = cls.getField("SHA256").getInt(null);
|
||||
result = (String) cls.getMethod("hash", int.class, String.class).invoke(damo, t, input);
|
||||
} else if ("sha512".equals(op)) {
|
||||
int t = cls.getField("SHA512").getInt(null);
|
||||
result = (String) cls.getMethod("hash", int.class, String.class).invoke(damo, t, input);
|
||||
} else {
|
||||
error = "알 수 없는 op: " + op;
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
error = "com.eactive.ext.djb.DamoManager 클래스를 찾을 수 없습니다. "
|
||||
+ "damo-manager.jar 를 WAS lib (운영) 또는 WEB-INF/lib 에 배포하세요.";
|
||||
} catch (Throwable t) {
|
||||
// require-real fail-fast(ExceptionInInitializerError) 또는 초기화 실패(NoClassDefFoundError) 포함
|
||||
Throwable c = (t.getCause() != null) ? t.getCause() : t;
|
||||
error = c.toString();
|
||||
if (c instanceof NoClassDefFoundError
|
||||
|| error.contains("require-real")
|
||||
|| error.contains("Could not initialize")) {
|
||||
error += " (※ -Ddamo-manager.require-real=true 인데 scpdb 가 없어 기동에 실패했을 수 있습니다. "
|
||||
+ "scpdb 를 갖추거나 옵션을 내리세요.)";
|
||||
}
|
||||
}
|
||||
|
||||
String badgeColor = "REAL".equals(mode) ? "#1a7f37"
|
||||
: "BYPASS".equals(mode) ? "#9a3412"
|
||||
: "FAKE".equals(mode) ? "#9a6700" : "#6e7781";
|
||||
%>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>DamoManager 테스트</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, "Malgun Gothic", sans-serif; margin: 24px; color: #24292f; }
|
||||
h1 { font-size: 20px; margin: 0 0 4px; }
|
||||
.sub { color: #6e7781; font-size: 13px; margin-bottom: 16px; }
|
||||
.badge { display:inline-block; color:#fff; padding:2px 10px; border-radius:12px; font-size:12px; font-weight:600; background:<%= badgeColor %>; }
|
||||
.meta { font-size:12px; color:#57606a; margin-left:8px; }
|
||||
.card { border:1px solid #d0d7de; border-radius:8px; padding:16px; max-width:760px; }
|
||||
label { display:block; font-size:13px; font-weight:600; margin:12px 0 4px; }
|
||||
textarea, select { width:100%; box-sizing:border-box; font-size:14px; padding:8px; border:1px solid #d0d7de; border-radius:6px; font-family:ui-monospace, Menlo, monospace; }
|
||||
textarea { height:90px; resize:vertical; }
|
||||
.row { display:flex; gap:12px; }
|
||||
.row > div { flex:1; }
|
||||
button { margin-top:14px; background:#1f6feb; color:#fff; border:0; border-radius:6px; padding:9px 18px; font-size:14px; font-weight:600; cursor:pointer; }
|
||||
.result { margin-top:16px; }
|
||||
.out { background:#f6f8fa; border:1px solid #d0d7de; border-radius:6px; padding:12px; white-space:pre-wrap; word-break:break-all; font-family:ui-monospace, Menlo, monospace; font-size:13px; min-height:20px; }
|
||||
.err { color:#cf222e; background:#fff5f5; border-color:#ffc1c1; }
|
||||
.hint { font-size:12px; color:#6e7781; margin-top:18px; line-height:1.6; }
|
||||
code { background:#eff1f3; padding:1px 5px; border-radius:4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DamoManager 테스트</h1>
|
||||
<div class="sub">웹에서 encrypt / decrypt / hash 동작을 즉시 확인 (eapim-admin)</div>
|
||||
|
||||
<div class="card">
|
||||
<div>
|
||||
<% if (loaded) { %>
|
||||
<span class="badge"><%= mode %> MODE</span>
|
||||
<span class="meta">revision=<%= esc(revision) %> · returnCode=<%= esc(returnCode) %></span>
|
||||
<% } else { %>
|
||||
<span class="badge" style="background:#cf222e;">NOT LOADED</span>
|
||||
<span class="meta">damo-manager.jar 미배포</span>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<form method="post" action="<%= request.getContextPath() %>/damo.jsp">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="op">동작</label>
|
||||
<select id="op" name="op">
|
||||
<option value="encrypt" <%= "encrypt".equals(op) ? "selected" : "" %>>encrypt (암호화)</option>
|
||||
<option value="decrypt" <%= "decrypt".equals(op) ? "selected" : "" %>>decrypt (복호화)</option>
|
||||
<option value="sha256" <%= "sha256".equals(op) ? "selected" : "" %>>hash SHA-256</option>
|
||||
<option value="sha512" <%= "sha512".equals(op) ? "selected" : "" %>>hash SHA-512</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="input">입력</label>
|
||||
<textarea id="input" name="input" placeholder="암호화/복호화/해시할 문자열"><%= esc(input) %></textarea>
|
||||
|
||||
<input type="hidden" name="run" value="1">
|
||||
<button type="submit">실행</button>
|
||||
</form>
|
||||
|
||||
<% if (run || error.length() > 0) { %>
|
||||
<div class="result">
|
||||
<label>결과<% if (run && error.length() == 0) { %> <span class="meta">(<%= esc(op) %>)</span><% } %></label>
|
||||
<div class="out <%= error.length() > 0 ? "err" : "" %>"><%= error.length() > 0 ? esc(error) : esc(result) %></div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="hint">
|
||||
모드 의미 — <b>BYPASS</b>: 무변환 원문 통과(<b>기본값</b>, 옵션 없음) · <b>FAKE</b>: Base64/JRE 해시(<code>-Ddamo-manager.enabled=true</code>, scpdb 없음) · <b>REAL</b>: penta scpdb 실 암복호화(<code>enabled=true</code> + scpdb).<br>
|
||||
BYPASS 모드에서는 encrypt/decrypt/hash 모두 입력을 그대로 반환합니다.<br>
|
||||
운영은 <code>-Ddamo-manager.require-real=true</code> 로 real 강제(아니면 기동 실패). CLI 로도 동일 확인: <code>java -jar damo-manager.jar enc <text></code>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,7 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<%@page import="org.apache.commons.lang3.StringUtils"%>
|
||||
<%@page import="com.eactive.eai.rms.common.login.SessionManager"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* Editor Content Styles (editor-content.css)
|
||||
*
|
||||
* Summernote 에디터로 작성된 HTML 콘텐츠의 공통 스타일
|
||||
* 관리자포탈/개발자포탈 양쪽에서 동일하게 사용
|
||||
*
|
||||
* 사용법:
|
||||
* - 관리자포탈: Summernote .note-editable에 .editor-content 클래스 추가
|
||||
* - 개발자포탈: 콘텐츠 wrapper에 .editor-content 클래스 추가
|
||||
*
|
||||
* @version 1.0.0
|
||||
* @date 2025-12-30
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
CSS Reset + 기본 설정
|
||||
========================================================================== */
|
||||
|
||||
.editor-content {
|
||||
all: revert;
|
||||
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 400 !important;
|
||||
line-height: 1.8 !important;
|
||||
color: #1A1A2E !important;
|
||||
word-wrap: break-word;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
헤딩 (Headings)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content h1 {
|
||||
font-size: 20px !important;
|
||||
font-weight: 700 !important;
|
||||
margin: 32px 0 16px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content h1:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.editor-content h2 {
|
||||
font-size: 18px !important;
|
||||
font-weight: 700 !important;
|
||||
margin: 24px 0 8px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content h3 {
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
margin: 16px 0 8px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content h4,
|
||||
.editor-content h5,
|
||||
.editor-content h6 {
|
||||
font-size: 16px !important;
|
||||
font-weight: 600 !important;
|
||||
margin: 16px 0 8px !important;
|
||||
line-height: 1.4 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
단락 (Paragraphs)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content p {
|
||||
margin-bottom: 16px !important;
|
||||
line-height: 1.8 !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.editor-content p:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
리스트 (Lists) - 글로벌 reset 대응
|
||||
========================================================================== */
|
||||
|
||||
.editor-content ul {
|
||||
list-style-type: disc !important;
|
||||
padding-left: 32px !important;
|
||||
margin: 16px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content ol {
|
||||
list-style-type: decimal !important;
|
||||
padding-left: 32px !important;
|
||||
margin: 16px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content li {
|
||||
margin-bottom: 8px !important;
|
||||
line-height: 1.8 !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.editor-content li:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* 중첩 리스트 */
|
||||
.editor-content ul ul {
|
||||
list-style-type: circle !important;
|
||||
margin: 8px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content ul ul ul {
|
||||
list-style-type: square !important;
|
||||
}
|
||||
|
||||
.editor-content ol ol {
|
||||
list-style-type: lower-alpha !important;
|
||||
margin: 8px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content ol ol ol {
|
||||
list-style-type: lower-roman !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
테이블 (Tables)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content table {
|
||||
width: 100% !important;
|
||||
border-collapse: collapse !important;
|
||||
margin: 24px 0 !important;
|
||||
border: 1px solid #E2E8F0 !important;
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content th,
|
||||
.editor-content td {
|
||||
border: 1px solid #E2E8F0 !important;
|
||||
padding: 8px 16px !important;
|
||||
text-align: left !important;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
.editor-content th {
|
||||
background: #EFF6FF !important;
|
||||
font-weight: 600 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
.editor-content tr:hover {
|
||||
background: #F8FAFC !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
링크 (Links)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content a {
|
||||
color: #0049b4 !important;
|
||||
text-decoration: underline !important;
|
||||
transition: color 0.3s ease !important;
|
||||
}
|
||||
|
||||
.editor-content a:hover {
|
||||
color: #003080 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
인용문 (Blockquote)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content blockquote {
|
||||
border-left: 4px solid #0049b4 !important;
|
||||
padding: 16px 24px !important;
|
||||
margin: 24px 0 !important;
|
||||
background: #F8FAFC !important;
|
||||
font-style: italic !important;
|
||||
color: #64748B !important;
|
||||
border-radius: 0 8px 8px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content blockquote p {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
이미지 (Images)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 8px !important;
|
||||
margin: 16px 0 !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
코드 (Code)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content code {
|
||||
background: #F8FAFC !important;
|
||||
padding: 2px 6px !important;
|
||||
border-radius: 4px !important;
|
||||
font-family: 'Fira Code', 'Courier New', monospace !important;
|
||||
font-size: 0.9em !important;
|
||||
color: #0049b4 !important;
|
||||
}
|
||||
|
||||
.editor-content pre {
|
||||
background: #F8FAFC !important;
|
||||
border: 1px solid #E2E8F0 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 16px !important;
|
||||
overflow-x: auto !important;
|
||||
margin: 16px 0 !important;
|
||||
}
|
||||
|
||||
.editor-content pre code {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
color: #1A1A2E !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
구분선 (Horizontal Rule)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content hr {
|
||||
border: none !important;
|
||||
border-top: 1px solid #E2E8F0 !important;
|
||||
margin: 32px 0 !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
텍스트 강조 (Text Emphasis)
|
||||
========================================================================== */
|
||||
|
||||
.editor-content strong,
|
||||
.editor-content b {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.editor-content em,
|
||||
.editor-content i {
|
||||
font-style: italic !important;
|
||||
}
|
||||
|
||||
.editor-content u {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.editor-content s,
|
||||
.editor-content strike,
|
||||
.editor-content del {
|
||||
text-decoration: line-through !important;
|
||||
}
|
||||
|
||||
.editor-content mark {
|
||||
background-color: #FEF3C7 !important;
|
||||
padding: 0 2px !important;
|
||||
}
|
||||
|
||||
.editor-content sub {
|
||||
vertical-align: sub !important;
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
.editor-content sup {
|
||||
vertical-align: super !important;
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
반응형 (Responsive)
|
||||
========================================================================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.editor-content {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content h1 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.editor-content h2 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.editor-content h3,
|
||||
.editor-content h4,
|
||||
.editor-content h5,
|
||||
.editor-content h6 {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content p,
|
||||
.editor-content li {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.editor-content table {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.editor-content th,
|
||||
.editor-content td {
|
||||
padding: 4px 8px !important;
|
||||
}
|
||||
|
||||
.editor-content ul,
|
||||
.editor-content ol {
|
||||
padding-left: 24px !important;
|
||||
}
|
||||
|
||||
.editor-content blockquote {
|
||||
padding: 8px 16px !important;
|
||||
}
|
||||
|
||||
.editor-content pre {
|
||||
padding: 8px !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
// 계좌이체 API 모의 데이터
|
||||
// locked: true 인 필드는 게이트웨이가 자동 주입한 값(회색 + 자물쇠)
|
||||
// locked: false / 미지정 인 필드는 사용자가 자유롭게 편집
|
||||
|
||||
window.SAMPLE_DATA = (function () {
|
||||
return {
|
||||
info: {
|
||||
title: { value: '계좌이체 API', locked: false },
|
||||
version: { value: '1.0.0', locked: false },
|
||||
summary: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
termsOfService: { value: '', locked: false },
|
||||
contact: {
|
||||
name: { value: 'DJB API Team', locked: false },
|
||||
email: { value: 'api@djb.co.kr', locked: false },
|
||||
url: { value: '', locked: false }
|
||||
},
|
||||
license: {
|
||||
name: { value: '', locked: false },
|
||||
url: { value: '', locked: false }
|
||||
}
|
||||
},
|
||||
|
||||
tags: [
|
||||
{ name: 'transfer', description: '계좌이체 관련 API' }
|
||||
],
|
||||
|
||||
externalDocs: {
|
||||
description: { value: '', locked: false },
|
||||
url: { value: '', locked: false }
|
||||
},
|
||||
|
||||
servers: [
|
||||
{
|
||||
url: { value: 'https://api-dev.djb.co.kr', locked: true },
|
||||
env: 'development',
|
||||
description: { value: '개발 환경', locked: false }
|
||||
},
|
||||
{
|
||||
url: { value: 'https://api-stg.djb.co.kr', locked: true },
|
||||
env: 'staging',
|
||||
description: { value: '스테이징 환경', locked: false }
|
||||
},
|
||||
{
|
||||
url: { value: 'https://api.djb.co.kr', locked: true },
|
||||
env: 'production',
|
||||
description: { value: '운영 환경', locked: false }
|
||||
}
|
||||
],
|
||||
|
||||
serverVariables: [
|
||||
{
|
||||
name: { value: 'basePath', locked: true },
|
||||
default: { value: '/v1', locked: false },
|
||||
enumStr: { value: '/v1,/v2', locked: false },
|
||||
description: { value: 'API 버전 경로', locked: false }
|
||||
}
|
||||
],
|
||||
|
||||
operation: {
|
||||
method: { value: 'POST', locked: true },
|
||||
path: { value: '/v1/accounts/transfer', locked: true },
|
||||
operationId: { value: 'transferAccount', locked: false },
|
||||
summary: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
tags: { value: ['transfer'], locked: false },
|
||||
deprecated: { value: false, locked: false }
|
||||
},
|
||||
|
||||
// 파라미터 (Path/Query/Header/Cookie)
|
||||
parameters: [
|
||||
{
|
||||
in: 'header',
|
||||
name: { value: 'X-Request-Id', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: 'uuid', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '', locked: false }
|
||||
}
|
||||
],
|
||||
|
||||
// Request Body 스키마 (중첩 3단)
|
||||
requestBody: {
|
||||
mediaType: 'application/json',
|
||||
required: { value: true, locked: true },
|
||||
schema: [
|
||||
{
|
||||
name: { value: 'transactionId', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '40', locked: false },
|
||||
pattern: { value: '^tx-[0-9a-zA-Z-]+$', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: 'tx-20260522-001', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'sender', locked: true },
|
||||
type: { value: 'object', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '송금인 정보', locked: false },
|
||||
example: { value: '', locked: false },
|
||||
children: [
|
||||
{
|
||||
name: { value: 'accountNumber', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '20', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '110-1234-567890', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'bankCode', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '3', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '088', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'holderName', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '60', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '김철수', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { value: 'receiver', locked: true },
|
||||
type: { value: 'object', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '수취인 정보', locked: false },
|
||||
example: { value: '', locked: false },
|
||||
children: [
|
||||
{
|
||||
name: { value: 'accountNumber', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '20', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '111-2345-678901', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'bankCode', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '3', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '020', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'holderName', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '60', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '이영희', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { value: 'amount', locked: true },
|
||||
type: { value: 'object', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '이체 금액', locked: false },
|
||||
example: { value: '', locked: false },
|
||||
children: [
|
||||
{
|
||||
name: { value: 'value', locked: true },
|
||||
type: { value: 'integer', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: 'int64', locked: false },
|
||||
maxLength: { value: '', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '50000', locked: false },
|
||||
children: null
|
||||
},
|
||||
{
|
||||
name: { value: 'currency', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: true, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '3', locked: false },
|
||||
pattern: { value: '^[A-Z]{3}$', locked: false },
|
||||
description: { value: 'ISO 4217 통화코드', locked: false },
|
||||
example: { value: 'KRW', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: { value: 'memo', locked: true },
|
||||
type: { value: 'string', locked: true },
|
||||
required: { value: false, locked: true },
|
||||
format: { value: '', locked: false },
|
||||
maxLength: { value: '100', locked: false },
|
||||
pattern: { value: '', locked: false },
|
||||
description: { value: '', locked: false },
|
||||
example: { value: '5월 회비', locked: false },
|
||||
children: null
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 응답 스키마 (상태코드 별)
|
||||
responses: {
|
||||
'200': {
|
||||
description: { value: '이체 성공', locked: false },
|
||||
schema: [
|
||||
{ name: { value: 'transactionId', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: 'tx-20260522-001', locked: false }, children: null },
|
||||
{ name: { value: 'status', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: 'COMPLETED', locked: false }, children: null },
|
||||
{ name: { value: 'completedAt', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: 'date-time', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '2026-05-22T10:00:00Z', locked: false }, children: null },
|
||||
{
|
||||
name: { value: 'balance', locked: true }, type: { value: 'object', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '잔액 정보', locked: false }, example: { value: '', locked: false },
|
||||
children: [
|
||||
{ name: { value: 'before', locked: true }, type: { value: 'integer', locked: true }, required: { value: true, locked: true }, format: { value: 'int64', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '1000000', locked: false }, children: null },
|
||||
{ name: { value: 'after', locked: true }, type: { value: 'integer', locked: true }, required: { value: true, locked: true }, format: { value: 'int64', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '950000', locked: false }, children: null }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
'400': {
|
||||
description: { value: '잘못된 요청', locked: false },
|
||||
schema: [
|
||||
{
|
||||
name: { value: 'error', locked: true }, type: { value: 'object', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '', locked: false },
|
||||
children: [
|
||||
{ name: { value: 'code', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: 'INVALID_AMOUNT', locked: false }, children: null },
|
||||
{ name: { value: 'message', locked: true }, type: { value: 'string', locked: true }, required: { value: true, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '금액이 올바르지 않습니다', locked: false }, children: null },
|
||||
{ name: { value: 'details', locked: true }, type: { value: 'array', locked: true }, required: { value: false, locked: true }, format: { value: '', locked: false }, maxLength: { value: '', locked: false }, pattern: { value: '', locked: false }, description: { value: '', locked: false }, example: { value: '', locked: false }, itemsType: { value: 'string', locked: false }, children: null }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
'401': { description: { value: '인증 실패', locked: false }, schema: [] },
|
||||
'500': { description: { value: '서버 오류', locked: false }, schema: [] }
|
||||
},
|
||||
|
||||
// 보안 스킴
|
||||
securitySchemes: [
|
||||
{
|
||||
key: 'ApiKeyAuth',
|
||||
type: 'apiKey',
|
||||
locked: true,
|
||||
name: { value: 'X-API-Key', locked: true },
|
||||
in: { value: 'header', locked: true },
|
||||
description: { value: '', locked: false }
|
||||
}
|
||||
],
|
||||
globalSecurity: ['ApiKeyAuth'],
|
||||
|
||||
// 예제
|
||||
examples: {
|
||||
request: {
|
||||
'application/json': {
|
||||
default: '{\n "transactionId": "tx-20260522-001",\n "sender": {\n "accountNumber": "110-1234-567890",\n "bankCode": "088",\n "holderName": "김철수"\n },\n "receiver": {\n "accountNumber": "111-2345-678901",\n "bankCode": "020",\n "holderName": "이영희"\n },\n "amount": {\n "value": 50000,\n "currency": "KRW"\n },\n "memo": "5월 회비"\n}'
|
||||
}
|
||||
},
|
||||
response: {
|
||||
'200': { body: '{\n "transactionId": "tx-20260522-001",\n "status": "COMPLETED"\n}', headers: [ { name: 'X-RateLimit-Remaining', type: 'integer', description: '남은 요청 횟수', example: '99' } ] }
|
||||
}
|
||||
},
|
||||
|
||||
// 문서 옵션
|
||||
docOptions: {
|
||||
theme: 'light',
|
||||
lang: 'ko',
|
||||
includeExamples: true,
|
||||
tryItOut: true,
|
||||
defaultExpandDepth: -1,
|
||||
layout: 'BaseLayout',
|
||||
tagFilter: [],
|
||||
sideToc: true
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,103 @@
|
||||
/* DJB OpenAPI Editor POC — Tailwind 보강 커스텀 */
|
||||
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo",
|
||||
"Malgun Gothic", "Noto Sans KR", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* === 잠금 행/카드 좌측 띠 === */
|
||||
.locked-row td:first-child {
|
||||
position: relative;
|
||||
}
|
||||
.locked-row td:first-child::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
width: 3px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.locked-card {
|
||||
position: relative;
|
||||
background: linear-gradient(to right, #f8fafc 0%, #ffffff 4px) #ffffff;
|
||||
}
|
||||
.locked-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0; top: 8px; bottom: 8px;
|
||||
width: 3px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
/* === 스키마 테이블 — 가독성 보조 === */
|
||||
.schema-table th { white-space: nowrap; }
|
||||
.schema-table tbody tr:hover { background: #fafbfc; }
|
||||
.schema-table input,
|
||||
.schema-table select {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.schema-table input[type="text"] {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
/* === 스텝퍼 항목 hover === */
|
||||
.step-item:hover { background: #f8fafc; }
|
||||
|
||||
/* === 미리보기 패널: Swagger UI 크기 미세조정 === */
|
||||
#preview-swagger .swagger-ui {
|
||||
font-size: 13px;
|
||||
}
|
||||
#preview-swagger .swagger-ui .info { margin: 18px 0; }
|
||||
#preview-swagger .swagger-ui .info .title { font-size: 22px; }
|
||||
#preview-swagger .swagger-ui .scheme-container { padding: 12px 0; box-shadow: none; }
|
||||
#preview-swagger .swagger-ui .opblock { margin: 0 0 12px 0; }
|
||||
|
||||
/* === 다크 테마 (POC 시뮬레이션 — Swagger 자체 다크는 미지원이라 컨테이너만) === */
|
||||
body.theme-dark #preview-swagger { background: #1e293b; }
|
||||
|
||||
/* === 토스트 애니메이션 === */
|
||||
#toast { transition: opacity 0.2s; }
|
||||
|
||||
/* === Monaco 컨테이너가 hidden 일 때 layout 깨짐 방지 === */
|
||||
#preview-monaco {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* === Stepper 6단 grid 셀 좌우 라인 (마지막 셀 제외) === */
|
||||
#stepper li {
|
||||
position: relative;
|
||||
}
|
||||
#stepper li:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -8px;
|
||||
width: 16px;
|
||||
height: 1px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
/* === 보안 스킴 카드 — 잠금 표시 === */
|
||||
.locked-card {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
/* === 칩(태그) 미세 조정 === */
|
||||
.tag-chip { transition: background 0.15s; }
|
||||
|
||||
/* === Export menu fade === */
|
||||
#export-menu { animation: fadeIn 0.12s ease-out; }
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* === 1600px 기준 폼 패널 내부 가로 여유 확보 === */
|
||||
#form-content { max-width: 100%; }
|
||||
|
||||
/* === readonly input 위에서 텍스트 커서 안 보이게 === */
|
||||
input[readonly] { user-select: text; }
|
||||
@@ -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){
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<script language="javascript" >
|
||||
var url = '<c:url value="/onl/apim/apigroup/apiGroupMan.json"/>';
|
||||
var url_view = '<c:url value="/onl/apim/apigroup/apiGroupMan.view"/>';
|
||||
var url_popup_view = '<c:url value="/onl/transaction/apim/apiSpecManPopup.view"/>';
|
||||
var url_popup_view = '<c:url value="/onl/transaction/apim/apiSpecMan.view"/>';
|
||||
|
||||
var isDetail = false;
|
||||
let dialog;
|
||||
|
||||
@@ -300,6 +300,10 @@
|
||||
|
||||
<div class="title">Client 정보</div>
|
||||
<table id="client" class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col style="width:20%"/>
|
||||
<col style="width:80%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th style="width:20%;">이름</th>
|
||||
<td><span id="clientName"></span></td>
|
||||
|
||||
@@ -392,15 +392,21 @@
|
||||
|
||||
<div class="title">사용자 정보</div>
|
||||
<table class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col style="width:20%"/>
|
||||
<col style="width:30%"/>
|
||||
<col style="width:20%"/>
|
||||
<col style="width:30%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th style="width:20%;">이메일 아이디</th>
|
||||
<td style="width:30%;" colspan="3"><span id="loginId"></span></td>
|
||||
<th>이메일 아이디</th>
|
||||
<td colspan="3"><span id="loginId"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width:20%;">이름</th>
|
||||
<td style="width:30%;"><span id="userName"></span></td>
|
||||
<th style="width:20%;">권한</th>
|
||||
<td style="width:30%;"><span id="roleCodeDescription"></span></td>
|
||||
<th>이름</th>
|
||||
<td><span id="userName"></span></td>
|
||||
<th>권한</th>
|
||||
<td><span id="roleCodeDescription"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>핸드폰 번호</th>
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<style>
|
||||
.mask-email { color:#1a73e8; font-weight:bold; }
|
||||
.mask-phone { color:#d93025; font-weight:bold; }
|
||||
.mask-digits { color:#9334e6; font-weight:bold; }
|
||||
.msg-cell { display:inline-block; max-width:380px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; vertical-align:middle; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/messagerequest/messageRequestMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/messagerequest/messageRequestMan.view" />';
|
||||
|
||||
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
|
||||
function escapeAttr(s) { return escapeHtmlJs(s).replace(/"/g, '"'); }
|
||||
|
||||
// 제목: 없으면 messageCode, 마우스오버에 코드명(코드) 표기
|
||||
function formatSubject(cellvalue, options, rowObject) {
|
||||
var code = rowObject.messageCode || '';
|
||||
var subject = (cellvalue && cellvalue !== '') ? cellvalue : '';
|
||||
var title = rowObject.messageCodeName ? (rowObject.messageCodeName + ' (' + code + ')') : code;
|
||||
if (subject) {
|
||||
// 제목이 있으면 2줄: 제목 + 하단에 메세지코드
|
||||
var codeLine = code ? '<br><span style="color:#888;font-size:0.85em;">' + escapeHtmlJs(code) + '</span>' : '';
|
||||
return '<span title="' + escapeAttr(title) + '">' + escapeHtmlJs(subject) + codeLine + '</span>';
|
||||
}
|
||||
// 제목이 없으면 메세지코드만
|
||||
return '<span title="' + escapeAttr(title) + '">' + escapeHtmlJs(code) + '</span>';
|
||||
}
|
||||
|
||||
// 본문: 서버에서 마스킹+escape 된 안전 HTML 그대로 렌더
|
||||
function formatMessage(cellvalue, options, rowObject) {
|
||||
return '<span class="msg-cell">' + (rowObject.messageHtml || '') + '</span>';
|
||||
}
|
||||
|
||||
// 수신자 정보: 휴대폰 / 이메일 / 메신저ID (모두 마스킹됨)
|
||||
function formatReceiverInfo(cellvalue, options, rowObject) {
|
||||
var parts = [];
|
||||
if (rowObject.phone) parts.push(escapeHtmlJs(rowObject.phone));
|
||||
if (rowObject.email) parts.push(escapeHtmlJs(rowObject.email));
|
||||
if (rowObject.messengerId) parts.push(escapeHtmlJs(rowObject.messengerId));
|
||||
return parts.join('<br>');
|
||||
}
|
||||
|
||||
function formatRequestDate(cellvalue, options, rowObject) {
|
||||
return '<span title="' + escapeAttr(rowObject.requestDateFull || '') + '">' + escapeHtmlJs(rowObject.requestDateShort || '') + '</span>';
|
||||
}
|
||||
function formatSentDate(cellvalue, options, rowObject) {
|
||||
return '<span title="' + escapeAttr(rowObject.sentDateFull || '') + '">' + escapeHtmlJs(rowObject.sentDateShort || '') + '</span>';
|
||||
}
|
||||
|
||||
function init() {
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'LIST_INIT_COMBO'},
|
||||
success: function (json) {
|
||||
new makeOptions("CODE", "NAME").setObj($("select[name=searchRequestStatus]"))
|
||||
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
|
||||
.setData(json.statusList).rendering();
|
||||
new makeOptions("CODE", "NAME").setObj($("select[name=searchMessageCode]"))
|
||||
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
|
||||
.setData(json.messageCodeList).rendering();
|
||||
|
||||
putSelectFromParam();
|
||||
|
||||
// 콤보 로드 후 그리드 생성 (경합 방지)
|
||||
buildGrid();
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
function buildGrid() {
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json", mtype: 'POST', url: url,
|
||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||
colNames: ['id', 'No.', '제목', '메세지 내용', '수신자명', '수신자 정보', '유형', '상태', '요청시간', '발송시간'],
|
||||
colModel: [
|
||||
{name: 'id', align: 'center', key: true, hidden: true},
|
||||
{name: 'rowNum', align: 'center', width: 45, sortable: false},
|
||||
{name: 'subject', align: 'left', width: 160, formatter: formatSubject},
|
||||
{name: 'messageHtml', align: 'left', width: 300, formatter: formatMessage},
|
||||
{name: 'username', align: 'center', width: 80},
|
||||
{name: 'receiverInfo', align: 'left', width: 150, formatter: formatReceiverInfo},
|
||||
{name: 'messageType', align: 'center', width: 60},
|
||||
{name: 'requestStatus', align: 'center', width: 70},
|
||||
{name: 'requestDateShort', align: 'center', width: 110, formatter: formatRequestDate},
|
||||
{name: 'sentDateShort', align: 'center', width: 110, formatter: formatSentDate}
|
||||
],
|
||||
jsonReader: {repeatitems: false},
|
||||
pager: $('#pager'),
|
||||
page: '${param.page}',
|
||||
rowNum: '${rmsDefaultRowNum}',
|
||||
autoheight: true,
|
||||
height: $("#container").height(),
|
||||
autowidth: true,
|
||||
viewrecords: true,
|
||||
rowList: eval('[${rmsDefaultRowList}]'),
|
||||
ondblClickRow: function (rowId) {
|
||||
var rowData = $(this).getRowData(rowId);
|
||||
var id = rowData['id'];
|
||||
var url2 = url_view + '?cmd=DETAIL';
|
||||
url2 += '&page=' + $(this).getGridParam("page");
|
||||
url2 += '&returnUrl=' + getReturnUrl();
|
||||
url2 += '&menuId=' + '${param.menuId}';
|
||||
url2 += '&id=' + id;
|
||||
goNav(url2);
|
||||
},
|
||||
loadComplete: function () {
|
||||
var page = $(this).getGridParam('page');
|
||||
var rowNum = $(this).getGridParam('rowNum');
|
||||
var rows = $(this).getDataIDs();
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var number = ((page - 1) * rowNum + i) + 1;
|
||||
$(this).setCell(rows[i], 'rowNum', number);
|
||||
}
|
||||
// 서버 고정 정렬(요청일 역순) — 컬럼 정렬 비활성
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, {sortable: false});
|
||||
}
|
||||
},
|
||||
loadError: function (jqXHR, textStatus, errorThrown) {
|
||||
var location = '<%=request.getContextPath()%>/';
|
||||
comloadError(jqXHR, textStatus, errorThrown, location);
|
||||
}
|
||||
});
|
||||
resizeJqGridWidth('grid', 'content_middle', '1000');
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function () {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("select[name^=search]").change(function () { $("#btn_search").click(); });
|
||||
$("input[name^=search]").keydown(function (key) {
|
||||
if (key.keyCode == 13) { $("#btn_search").click(); }
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||
</div>
|
||||
<div class="title">메세지 발송 내역<span class="tooltip">발송 요청 이력을 조회합니다. 개인정보는 마스킹되어 표시됩니다.</span></div>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<colgroup>
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>상태</th>
|
||||
<td><div class="select-style"><select name="searchRequestStatus"></select></div></td>
|
||||
<th>메세지 코드</th>
|
||||
<td><div class="select-style"><select name="searchMessageCode"></select></div></td>
|
||||
<th>수신자명</th>
|
||||
<td><input type="text" name="searchRecipient" autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>유형</th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="searchMessageType">
|
||||
<option value="">전체</option>
|
||||
<option value="EMAIL">EMAIL</option>
|
||||
<option value="SMS">SMS</option>
|
||||
<option value="KAKAO">KAKAO</option>
|
||||
<option value="MESSENGER">MESSENGER</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<th>요청기간</th>
|
||||
<td colspan="3">
|
||||
<input type="date" name="searchFromDate"> ~ <input type="date" name="searchToDate">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,149 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<style>
|
||||
.mask-email { color:#1a73e8; font-weight:bold; }
|
||||
.mask-phone { color:#d93025; font-weight:bold; }
|
||||
.mask-digits { color:#9334e6; font-weight:bold; }
|
||||
.msg-detail { white-space:pre-wrap; word-break:break-all; min-height:80px; line-height:1.6; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/messagerequest/messageRequestMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/messagerequest/messageRequestMan.view" />';
|
||||
var key = '${param.id}';
|
||||
|
||||
function detail() {
|
||||
if (!key) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'DETAIL', id: key},
|
||||
success: function (data) {
|
||||
var codeText = (data.messageCodeName || '') + (data.messageCode ? ' (' + data.messageCode + ')' : '');
|
||||
$('#messageCodeName').text(codeText);
|
||||
$('#subject').text((data.subject && data.subject !== '') ? data.subject : (data.messageCode || ''));
|
||||
$('#messageType').text(data.messageType || '');
|
||||
$('#requestStatus').text(data.requestStatus || '');
|
||||
$('#username').text(data.username || '');
|
||||
$('#email').text(data.email || '');
|
||||
$('#phone').text(data.phone || '');
|
||||
$('#messengerId').text(data.messengerId || '');
|
||||
$('#umsUid').text(data.umsUid || '');
|
||||
$('#eaiInterfaceId').text(data.eaiInterfaceId || '');
|
||||
$('#serviceId').text(data.serviceId || '');
|
||||
$('#requestDate').text(data.requestDateFull || '');
|
||||
$('#sentDate').text(data.sentDateFull || '');
|
||||
// 서버에서 마스킹+escape 된 안전 HTML
|
||||
$('#messageHtml').html(data.messageHtml || '');
|
||||
|
||||
// PENDING 건만 '실패 처리' 노출 (권한 제어는 buttonControl 이 담당)
|
||||
if (data.requestStatus !== 'PENDING') {
|
||||
$('#btn_fail').hide();
|
||||
}
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
|
||||
buttonControl();
|
||||
detail();
|
||||
|
||||
$('#btn_fail').click(function () {
|
||||
if (!confirm('이 건을 FAILED(발송 실패) 상태로 변경하시겠습니까?')) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'UPDATE_STATUS', id: key},
|
||||
success: function () {
|
||||
alert("변경되었습니다.");
|
||||
detail();
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
});
|
||||
|
||||
$('#btn_previous').click(function () { goNav(returnUrl); });
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
|
||||
</div>
|
||||
<div class="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_fail" level="W" status="DETAIL" style="background-color:#dc3545;border-color:#dc3545;color:#fff;"><i class="material-icons">report</i> 실패 처리</button>
|
||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||
</div>
|
||||
<div class="title" id="title">메세지 발송 내역 상세<span class="tooltip">개인정보는 마스킹되어 표시됩니다.</span></div>
|
||||
<table class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>메세지 코드</th>
|
||||
<td colspan="3"><span id="messageCodeName"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>제목</th>
|
||||
<td colspan="3"><span id="subject"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>유형</th>
|
||||
<td><span id="messageType"></span></td>
|
||||
<th>상태</th>
|
||||
<td><span id="requestStatus"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>수신자명</th>
|
||||
<td><span id="username"></span></td>
|
||||
<th>메신저 ID</th>
|
||||
<td><span id="messengerId"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>이메일</th>
|
||||
<td><span id="email"></span></td>
|
||||
<th>휴대폰</th>
|
||||
<td><span id="phone"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>요청 시간</th>
|
||||
<td><span id="requestDate"></span></td>
|
||||
<th>발송 시간</th>
|
||||
<td><span id="sentDate"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>EAI 인터페이스 ID</th>
|
||||
<td><span id="eaiInterfaceId"></span></td>
|
||||
<th>서비스 ID</th>
|
||||
<td><span id="serviceId"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>UMS UID</th>
|
||||
<td colspan="3"><span id="umsUid"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>메세지 내용</th>
|
||||
<td colspan="3"><div id="messageHtml" class="msg-detail"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -208,6 +208,7 @@
|
||||
<option value="">전체</option>
|
||||
<option value="PENDING">문의중</option>
|
||||
<option value="RESPONDED">답변완료</option>
|
||||
<option value="CLOSED">답변종료</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -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;
|
||||
@@ -89,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);
|
||||
@@ -98,8 +103,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
function buildGrid(){
|
||||
|
||||
$('#grid').jqGrid({
|
||||
datatype:"json",
|
||||
mtype: 'POST',
|
||||
@@ -172,9 +177,13 @@
|
||||
}
|
||||
});
|
||||
|
||||
init();
|
||||
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
// 콤보 로드 → 콤보 성공 콜백에서 buildGrid() 호출 (경합 방지)
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function(){
|
||||
var postData = getSearchForJqgrid("cmd","LIST");
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
|
||||
<%@ page import="java.io.*"%>
|
||||
<%@ page import="com.eactive.eai.rms.common.util.CommonUtil"%>
|
||||
<%@ page import="com.eactive.eai.rms.common.context.MonitoringContext"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
@@ -7,6 +9,9 @@
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
|
||||
MonitoringContext monitoringContext = (MonitoringContext)CommonUtil.getBean(request, "monitoringContext");
|
||||
String reverseProxyUrl = monitoringContext.getStringProperty("djb.reverse_proxy.url", "");
|
||||
%>
|
||||
<%!
|
||||
public String getRequiredLabel(String label) {
|
||||
@@ -137,6 +142,7 @@
|
||||
$("#compRegNo").val(formatCompanyRegNo(portalOrgData.compRegNo));
|
||||
$("#corpRegNo").val(formatCorpRegNo(portalOrgData.corpRegNo));
|
||||
$("#createdDate").inputmask("9999-99-99 99:99:99", {'autoUnmask': true});
|
||||
$("#reverseProxyPath").val(portalOrgData.reverseProxyPath);
|
||||
|
||||
// 전화번호 처리
|
||||
if (portalOrgData.scPhoneNumber) {
|
||||
@@ -404,47 +410,6 @@
|
||||
downloadFile(fileId, '1'); // fileSn은 '1'로 가정
|
||||
});
|
||||
|
||||
// OBP 파트너코드 조회 버튼 클릭
|
||||
$("#btn_query_obp").click(function() {
|
||||
var compRegNo = $("#compRegNo").val();
|
||||
if (!compRegNo) {
|
||||
jSuites.notification({ name: '알림', message: '사업자등록번호를 먼저 입력해주세요.', error: true });
|
||||
$("#compRegNo").focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 하이픈 제거
|
||||
compRegNo = compRegNo.replace(/-/g, '');
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
dataType: "json",
|
||||
data: {
|
||||
cmd: 'QUERY_OBP_PARTNER_CODE',
|
||||
compRegNo: compRegNo
|
||||
},
|
||||
beforeSend: function() {
|
||||
$("#btn_query_obp").prop("disabled", true).html('<i class="material-icons">hourglass_empty</i> 조회중...');
|
||||
},
|
||||
success: function(json) {
|
||||
if (json.success) {
|
||||
$("#orgCode").val(json.partnerCode);
|
||||
jSuites.notification({ name: '성공', message: '파트너코드가 조회되었습니다: ' + json.partnerCode });
|
||||
} else {
|
||||
jSuites.notification({ name: '실패', message: json.message, error: true });
|
||||
}
|
||||
},
|
||||
error: function(e) {
|
||||
jSuites.notification({ name: '오류', message: '파트너코드 조회 중 오류가 발생했습니다.', error: true });
|
||||
console.error(e.responseText);
|
||||
},
|
||||
complete: function() {
|
||||
$("#btn_query_obp").prop("disabled", false).html('<i class="material-icons">search</i> 파트너코드 조회');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#btn_modify").click(function(){
|
||||
if (!checkRequired("ajaxForm")) return;
|
||||
|
||||
@@ -630,10 +595,7 @@
|
||||
<tr>
|
||||
<th style="width:15%;"><%= localeMessage.getString("portalOrg.orgCode") %></th>
|
||||
<td colspan="3" style="width:35%;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input type="text" name="orgCode" id="orgCode" style="width: 200px; margin-right: 10px;"/>
|
||||
<button type="button" class="cssbtn smallBtn" id="btn_query_obp"><i class="material-icons">search</i> 파트너코드 조회</button>
|
||||
</div>
|
||||
<input type="text" name="orgCode" id="orgCode"/>
|
||||
</td>
|
||||
<th><%= localeMessage.getString("portalOrg.orgIndustryType") %></th> <%--업종--%>
|
||||
<td><input type="text" name="orgIndustryType" id="orgIndustryType" maxlength="50" /></td>
|
||||
@@ -738,6 +700,16 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><%= localeMessage.getString("portalOrg.reverseProxyPath") %></th>
|
||||
<td colspan="3">
|
||||
<%=reverseProxyUrl %>/<input type="text" name="reverseProxyPath" id="reverseProxyPath" style="width:100px; "/>
|
||||
</td>
|
||||
<th></th>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr><%--사업자등록증--%>
|
||||
<th><%= localeMessage.getString("portalOrg.compRegFile") %></th>
|
||||
<td colspan="5" style="padding-top: 5px; padding-bottom: 5px;">
|
||||
|
||||
@@ -26,7 +26,38 @@ function formatFile(cellvalue, options, rowObject) {
|
||||
return cellvalue + icon;
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
function deleteSelectedRows() {
|
||||
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
|
||||
|
||||
if (selectedIds.length === 0) {
|
||||
alert('항목을 선택하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('선택된 ' + selectedIds.length + '개 항목을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
dataType: "json",
|
||||
data: {
|
||||
cmd: 'DELETE_BULK',
|
||||
ids: selectedIds.join(',')
|
||||
},
|
||||
success: function(data) {
|
||||
alert('삭제되었습니다.');
|
||||
$("#grid").jqGrid('clearGridData', true);
|
||||
$("#grid").trigger("reloadGrid");
|
||||
},
|
||||
error: function(e) {
|
||||
alert('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#grid').jqGrid({
|
||||
datatype:"json",
|
||||
mtype: 'POST',
|
||||
@@ -45,7 +76,7 @@ $(document).ready(function() {
|
||||
],
|
||||
jsonReader: {
|
||||
repeatitems:false
|
||||
},
|
||||
},
|
||||
pager : $('#pager'),
|
||||
page : '${param.page}',
|
||||
rowNum : '${rmsDefaultRowNum}',
|
||||
@@ -54,6 +85,8 @@ $(document).ready(function() {
|
||||
autowidth: true,
|
||||
viewrecords: true,
|
||||
rowList : eval('[${rmsDefaultRowList}]'),
|
||||
multiselect: true,
|
||||
multiboxonly: true,
|
||||
ondblClickRow: function(rowId) {
|
||||
var rowData = $(this).getRowData(rowId);
|
||||
var id = rowData['id'];
|
||||
@@ -94,10 +127,14 @@ $(document).ready(function() {
|
||||
resizeJqGridWidth('grid','content_middle','1000');
|
||||
|
||||
$("#btn_search").click(function(){
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
var postData = getSearchForJqgrid("cmd","LIST"); //jqgrid에서는 object 로
|
||||
$("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("#btn_delete_selected").click(function(){
|
||||
deleteSelectedRows();
|
||||
});
|
||||
|
||||
$("input[name^=search]").keydown(function(key){
|
||||
if (key.keyCode == 13){
|
||||
$("#btn_search").click();
|
||||
@@ -120,8 +157,9 @@ $(document).ready(function() {
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R" ><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
|
||||
</div>
|
||||
<div class="title">사업제휴 신청 목록</div>
|
||||
<div class="title">피드백/개선요청 신청 목록</div>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<tbody>
|
||||
|
||||
@@ -92,6 +92,27 @@ $(document).ready(function() {
|
||||
goNav(returnUrl);//LIST로 이동
|
||||
});
|
||||
|
||||
$("#btn_delete").click(function(){
|
||||
if (!confirm('선택한 항목을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
dataType: "json",
|
||||
data: { cmd: 'DELETE', id: key },
|
||||
success: function(data) {
|
||||
alert('삭제되었습니다.');
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
goNav(returnUrl);//LIST로 이동
|
||||
},
|
||||
error: function(e) {
|
||||
alert('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#btn_unmask").click(function () {
|
||||
const reason = prompt("마스킹 해제 사유를 입력하세요:");
|
||||
|
||||
@@ -134,9 +155,10 @@ $(document).ready(function() {
|
||||
<div class="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_unmask" level="W" status="DETAIL"><i class="material-icons">lock_open</i> 마스킹해제</button>
|
||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 삭제</button>
|
||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||
</div>
|
||||
<div class="title">사업제휴 신청 상세</div>
|
||||
<div class="title">피드백/개선요청 신청 상세</div>
|
||||
|
||||
<table id="grid" ></table>
|
||||
<div id="pager"></div>
|
||||
|
||||
@@ -86,9 +86,11 @@
|
||||
editurl: "clientArray",
|
||||
colNames: ['<%= localeMessage.getString("propertyDetail.propertyKey") %>',
|
||||
'<%= localeMessage.getString("propertyDetail.propertyValue") %>',
|
||||
'설명',
|
||||
'<%= localeMessage.getString("propertyDetail.delYn") %>'],
|
||||
colModel: [{name: 'PRPTYNAME', width: 50, align: 'left', editable: true},
|
||||
{name: 'PRPTY2VAL', width: 200, align: 'left', editable: true},
|
||||
{name: 'PRPTY2VAL', width: 150, align: 'left', editable: true},
|
||||
{name: 'PRPTYDESC', width: 200, align: 'left', editable: true, edittype: 'textarea', editoptions: {rows: 2}},
|
||||
{
|
||||
name: 'DELETEYN',
|
||||
width: 20,
|
||||
@@ -258,6 +260,7 @@
|
||||
var data = new Object();
|
||||
data["PRPTYNAME"] = $("input[name=prptyName]").val();
|
||||
data["PRPTY2VAL"] = $("input[name=prpty2Val]").val();
|
||||
data["PRPTYDESC"] = $("input[name=prptyDesc]").val();
|
||||
|
||||
var rows = $("#grid")[0].rows;
|
||||
var index = Number($(rows[rows.length - 1]).attr("id"));
|
||||
@@ -340,6 +343,11 @@
|
||||
<td colspan="3"><input type="text" name="prpty2Val"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>설명</th>
|
||||
<td colspan="3"><input type="text" name="prptyDesc"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- grid -->
|
||||
<table id="grid"></table>
|
||||
|
||||
@@ -29,6 +29,16 @@
|
||||
// 원본 행 데이터를 ID로 조회하기 위한 맵
|
||||
var gridRowDataMap = {};
|
||||
|
||||
// 휴대폰번호 중복 조회 모드 여부
|
||||
var isDupView = false;
|
||||
|
||||
// 현재 검색조건 + 중복 조회 모드 플래그를 합쳐 LIST 요청 파라미터를 생성
|
||||
function buildListPostData() {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
postData.onlyDuplicateMobile = isDupView;
|
||||
return postData;
|
||||
}
|
||||
|
||||
/* 선택된 사용자 삭제 - 상태에 따라 Soft/Hard Delete 자동 분기 */
|
||||
function deleteSelectedUsers() {
|
||||
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
|
||||
@@ -166,6 +176,13 @@
|
||||
|
||||
$('#grid').jqGrid('setColProp', 'approvalStatus', {editoptions: {value: select_approvalStatus}});
|
||||
$('#grid').jqGrid('setColProp', 'userStatus', {editoptions: {value: select_userStatus}});
|
||||
|
||||
// 휴대폰번호 중복 허용안함(기능 ON) + 중복 사용자 존재 시 배너 노출
|
||||
if (json.mobileDuplicateCheckEnabled && json.mobileDuplicateUserCount > 0) {
|
||||
$("#dupMobileCount").text(json.mobileDuplicateUserCount);
|
||||
$("#dupMobileBanner").show();
|
||||
}
|
||||
|
||||
$('#grid').trigger('reloadGrid');
|
||||
|
||||
},
|
||||
@@ -176,7 +193,7 @@
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
var gridPostData = getSearchForJqgrid("cmd", "LIST");
|
||||
var gridPostData = buildListPostData();
|
||||
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json",
|
||||
@@ -188,7 +205,7 @@
|
||||
'<%= localeMessage.getString("portalUser.orgName") %>',
|
||||
'<%= localeMessage.getString("portalUser.name") %>',
|
||||
'<%= localeMessage.getString("portalUser.userId") %>',
|
||||
<%--'<%= localeMessage.getString("portalUser.mobile") %>',--%>
|
||||
'<%= localeMessage.getString("portalUser.mobileNumber") %>',
|
||||
'<%= localeMessage.getString("portalUser.roleName") %>',
|
||||
'<%= localeMessage.getString("portalUser.userStatus") %>',
|
||||
'<%= localeMessage.getString("portalUser.createOn") %>',
|
||||
@@ -201,7 +218,7 @@
|
||||
{name: 'portalOrgUIs.orgName', align: 'center', width: "120"},
|
||||
{name: 'userName', align: 'center', width: "80" },
|
||||
{name: 'loginId', align: 'center', width: "150" },
|
||||
// {name: 'mobileNumber', align: 'center', width: "100" },
|
||||
{name: 'mobileNumber', align: 'center', width: "110", hidden: true},
|
||||
{name: 'roleCodeDescription', align: 'center', width: "80"},
|
||||
{name: 'userStatusDescription', align: 'center', width: "50"},
|
||||
{name: 'createdDate', align: 'center', width: "100", formatter: timeStampFormat},
|
||||
@@ -274,9 +291,20 @@
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function () {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: buildListPostData(), page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
// 중복 휴대폰번호 사용자만 보기 / 전체 보기 토글
|
||||
$("#btn_dup_toggle").click(function () {
|
||||
isDupView = !isDupView;
|
||||
if (isDupView) {
|
||||
$(this).text("전체 보기");
|
||||
$("#grid").jqGrid('showCol', 'mobileNumber');
|
||||
} else {
|
||||
$(this).text("중복만 보기");
|
||||
$("#grid").jqGrid('hideCol', 'mobileNumber');
|
||||
}
|
||||
$("#grid").setGridParam({url: url, postData: buildListPostData(), page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("#btn_new").click(function () {
|
||||
@@ -334,6 +362,11 @@
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div id="dupMobileBanner" style="display:none; background:#f8d7da; border:1px solid #dc3545; padding:8px 15px; margin:5px 0; border-radius:4px; color:#721c24;">
|
||||
<strong>⚠ 중복 휴대폰번호 사용자 <span id="dupMobileCount">0</span>건 발견</strong>
|
||||
<button type="button" class="cssbtn" id="btn_dup_toggle" style="margin-left:10px;">중복만 보기</button>
|
||||
</div>
|
||||
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -365,15 +398,16 @@
|
||||
<td><input type="text" name="searchUserName"></td>
|
||||
|
||||
<th style="width:10%; min-width:100px;">
|
||||
<%= localeMessage.getString("portalUser.userId") %>
|
||||
<%= localeMessage.getString("portalUser.userId") %><span style="color:#dc3545;">(*)</span>
|
||||
</th>
|
||||
<td><input type="text" name="searchUserId" value="${param.searchUserId}"></td>
|
||||
<th style="width:10%; min-width:100px;"><%= localeMessage.getString("portalUser.mobileNumber") %>
|
||||
<th style="width:10%; min-width:100px;"><%= localeMessage.getString("portalUser.mobileNumber") %><span style="color:#dc3545;">(*)</span>
|
||||
</th>
|
||||
<td><input type="text" name="searchMobileNumber" value="${param.searchMobileNumber}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="color:#dc3545; font-size:12px; margin:4px 2px;">(*) 암호화 항목 · 부분검색 불가(전체 일치)</div>
|
||||
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
var url ='<c:url value="/onl/transaction/apim/apiInterfaceMan.json" />';
|
||||
const url_excel ='<c:url value="/onl/transaction/apim/apiInterfaceMan.excel" />';
|
||||
var url_spec = '<c:url value="/onl/transaction/apim/apiSpecMan.view"/>';
|
||||
var url_openapi_spec = '<c:url value="/onl/transaction/apim/djbApiSpecMan.view"/>';
|
||||
var returnUrl ;
|
||||
let headerValues = [];
|
||||
let headerNames = [];
|
||||
@@ -298,7 +299,7 @@
|
||||
if(!isStandardMessage){
|
||||
$(adapterSettings.adapterUrlId).text(adapterInfo.urlPath);
|
||||
} else {
|
||||
$(adapterSettings.methodSelectId).val('');
|
||||
//$(adapterSettings.methodSelectId).val('');
|
||||
$(adapterSettings.restPathInputId).val('');
|
||||
$(adapterSettings.adapterUrlId).text('');
|
||||
}
|
||||
@@ -871,6 +872,10 @@
|
||||
});
|
||||
|
||||
var postData = $('#ajaxForm').serializeArray();
|
||||
|
||||
console.log('modify', postData);
|
||||
console.log('method', $('#inboundHttpMethod').val());
|
||||
|
||||
if (isDetail){
|
||||
postData.push({ name: "cmd" , value:"UPDATE"});
|
||||
}else{
|
||||
@@ -988,6 +993,28 @@
|
||||
showModal(popupUrl, args, 1200, 800);
|
||||
}
|
||||
|
||||
// v2: OpenAPI Spec — 기본은 모달(iframe), Shift+클릭은 새 창.
|
||||
// 컨텍스트는 전부 query string 으로 전달(dialogArguments 미사용).
|
||||
function showOpenApiSpecPopup(e) {
|
||||
var eaiSvcName = getFullSvcName();
|
||||
var eaiSvcDesc = $('#eaiSvcDesc').val();
|
||||
var apipath = $('#apiFullPath').val(); // METHOD|URL ('|' 포함 → 인코딩 필수)
|
||||
var contentType = $('#contentType').val();
|
||||
var baseUrl = url_openapi_spec
|
||||
+ '?cmd=DETAIL'
|
||||
+ '&eaiSvcName=' + encodeURIComponent(eaiSvcName)
|
||||
+ '&apipath=' + encodeURIComponent(apipath)
|
||||
+ '&eaiSvcDesc=' + encodeURIComponent(eaiSvcDesc)
|
||||
+ '&contentType=' + encodeURIComponent(contentType);
|
||||
if (e && e.shiftKey) {
|
||||
// Shift+클릭: 새 창. window.open 은 serviceType/menuId 자동 부착 안 되므로 직접 붙임.
|
||||
window.open(urlAddServiceType(baseUrl + '&menuId=' + encodeURIComponent(getMenuId())), '_blank');
|
||||
} else {
|
||||
// 기본: 모달 + iframe. showModal 이 serviceType/menuId/pop 을 자동 부착.
|
||||
showModal(baseUrl, { title: 'OpenAPI Spec' }, 1600, 880);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
makeValidate();
|
||||
// var bootstrapButton = $.fn.button.noConflict()
|
||||
@@ -1002,9 +1029,14 @@
|
||||
}
|
||||
init(url,key,detail);
|
||||
|
||||
// v1: 기존 API 스펙 (모달) — 기능 비교용 복원
|
||||
$("#btn_api_spec").click(function() {
|
||||
showApiSpecPopup();
|
||||
});
|
||||
// v2: 신규 OpenAPI Spec (새 탭)
|
||||
$("#btn_openapi_spec").click(function(e) {
|
||||
showOpenApiSpecPopup(e);
|
||||
});
|
||||
|
||||
$("#btn_modify").click(modifyApi);
|
||||
$("#btn_delete").click(function(){
|
||||
@@ -1503,7 +1535,10 @@
|
||||
<button type="button" class="cssbtn" id="btn_json_export" level="R" status="DETAIL,NEW"><i class="material-icons">download</i> <%=localeMessage.getString("button.exportJson")%></button></button>
|
||||
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL"><i class="material-icons">delete</i> <%= localeMessage.getString("button.delete") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_modify" level="R" status="DETAIL,NEW"><i class="material-icons">save</i> <%= localeMessage.getString("button.modify") %></button>
|
||||
<!-- v1: 기존 API 스펙 (모달) — 기능 비교용 복원 -->
|
||||
<button type="button" class="cssbtn" id="btn_api_spec" level="R" status="DETAIL,NEW"><i class="material-icons">description</i> API 스펙</button>
|
||||
<!-- v2: 신규 OpenAPI Spec (새 탭) -->
|
||||
<button type="button" class="cssbtn" id="btn_openapi_spec" level="R" status="DETAIL,NEW"><i class="material-icons">api</i> OpenAPI Spec</button>
|
||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||
</div>
|
||||
<div class="title" id="title" style="font-size:1.4em">${rmsMenuName}</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/transaction/apim/apiSpecMan.json" />';
|
||||
var url_org ='<c:url value="/onl/transaction/apim/apiSpecManPopup.view" />';
|
||||
var url_org ='<c:url value="/onl/transaction/apim/apiSpecMan.view" />';
|
||||
var isDetail = false;
|
||||
var sampleRequestEditor, sampleResponseEditor, testbedSpecEditor;
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8"%>
|
||||
<%@ page import="java.io.*"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title>API 그룹 선택</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css_custom.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/apigroup/apiGroupMan.json" />';
|
||||
|
||||
function search() {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var selectedGroupIds = urlParams.get("selectedGroupIds");
|
||||
|
||||
if (selectedGroupIds) {
|
||||
selectedGroupIds = selectedGroupIds.split(','); // 콤마로 구분된 ID들을 배열로 변환
|
||||
} else {
|
||||
selectedGroupIds = [];
|
||||
}
|
||||
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json",
|
||||
mtype: 'POST',
|
||||
url: url,
|
||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||
colNames: ['API 그룹 ID', 'API 그룹 명', '설명'],
|
||||
colModel: [
|
||||
{name: 'id', hidden: true},
|
||||
{name: 'groupName', align: 'center', width: "180"},
|
||||
{name: 'groupDesc', align: 'left', width: "200"}
|
||||
],
|
||||
jsonReader: {
|
||||
repeatitems: false
|
||||
},
|
||||
pager: $('#pager'),
|
||||
page: '${param.page}',
|
||||
rowNum: '${rmsDefaultRowNum}',
|
||||
autoheight: true,
|
||||
height: 'auto',
|
||||
width: 410,
|
||||
autowidth: false,
|
||||
viewrecords: true,
|
||||
shrinkToFit: false,
|
||||
multiselect: true,
|
||||
multiboxonly: false,
|
||||
loadComplete: function(data) {
|
||||
var $grid = $(this);
|
||||
$.each($grid.getDataIDs(), function(_, id) {
|
||||
var rowData = $grid.getRowData(id);
|
||||
if ($.inArray(rowData.id, selectedGroupIds) !== -1) {
|
||||
$grid.jqGrid('setSelection', id, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_search").click(search);
|
||||
|
||||
$("#btn_save").click(function() {
|
||||
var selectedRows = $('#grid').getGridParam('selarrrow');
|
||||
|
||||
var selectedGroups = [];
|
||||
if (selectedRows.length > 0) {
|
||||
selectedGroups = selectedRows.map(function(rowid) {
|
||||
var rowData = $('#grid').getRowData(rowid);
|
||||
return {
|
||||
id: rowData.id,
|
||||
groupName: rowData.groupName
|
||||
};
|
||||
});
|
||||
}
|
||||
// 부모 창의 selectApiGroup 함수 호출
|
||||
if (window.parent && typeof window.parent.selectApiGroup === "function") {
|
||||
window.parent.selectApiGroup(selectedGroups);
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
$("#btn_close").click(function () {
|
||||
window.close();
|
||||
});
|
||||
|
||||
// 검색어 입력 후 엔터 키 처리
|
||||
$("input[name=searchGroupName]").keydown(function(key) {
|
||||
if (key.keyCode == 13) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="popup_box">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_save" level="W">
|
||||
<i class="material-icons">save</i> <%= localeMessage.getString("button.save") %>
|
||||
</button>
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R">
|
||||
<i class="material-icons">search</i> <%= localeMessage.getString("button.search") %>
|
||||
</button>
|
||||
<button type="button" class="cssbtn" id="btn_close" level="R" status="DETAIL"><i
|
||||
class="material-icons">close</i> <%= localeMessage.getString("button.close") %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="title"></div>
|
||||
<table class="search_condition" cellspacing="0">
|
||||
<tr>
|
||||
<th style="width:80px;">API 그룹 명</th>
|
||||
<td>
|
||||
<input type="text" name="searchGroupName" value="${param.searchGroupName}">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,173 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1600" />
|
||||
<title>OpenAPI Spec 에디터</title>
|
||||
|
||||
<%-- Tailwind (벤더된 Play CDN JS, 오프라인) --%>
|
||||
<script src="<c:url value='/plugins/openapi/tailwind.js'/>"></script>
|
||||
<script>
|
||||
tailwind.config = { theme: { extend: { colors: { brand: {
|
||||
50:'#eff6ff', 100:'#dbeafe', 500:'#3b82f6', 600:'#2563eb', 700:'#1d4ed8'
|
||||
} } } } };
|
||||
</script>
|
||||
|
||||
<%-- Swagger UI 5 (벤더) --%>
|
||||
<link rel="stylesheet" href="<c:url value='/plugins/swaggerUI/swagger-ui.css'/>" />
|
||||
<script src="<c:url value='/plugins/swaggerUI/swagger-ui-bundle.js'/>"></script>
|
||||
<script src="<c:url value='/plugins/swaggerUI/swagger-ui-standalone-preset.js'/>"></script>
|
||||
<script src="<c:url value='/plugins/swaggerUI/djb-swagger-i18n.js'/>" charset="UTF-8"></script>
|
||||
|
||||
<%-- js-yaml (벤더) --%>
|
||||
<script src="<c:url value='/plugins/openapi/js-yaml.min.js'/>"></script>
|
||||
|
||||
<%-- CodeMirror (YAML 편집기 — Monaco 대체, admin addon) --%>
|
||||
<link rel="stylesheet" href="<c:url value='/addon/codemirror/lib/codemirror.css'/>" />
|
||||
<script src="<c:url value='/addon/codemirror/lib/codemirror.js'/>"></script>
|
||||
<script src="<c:url value='/addon/codemirror/mode/yaml/yaml.js'/>"></script>
|
||||
<script src="<c:url value='/addon/codemirror/mode/javascript/javascript.js'/>"></script>
|
||||
|
||||
<%-- jQuery + Summernote(lite, 무-Bootstrap) — 설명 리치 에디터 --%>
|
||||
<link rel="stylesheet" href="<c:url value='/addon/summernote/summernote-lite.css'/>" />
|
||||
<%-- 개발자포탈과 동일한 에디터 콘텐츠 스타일(.editor-content, Noto Sans KR 16px) --%>
|
||||
<link rel="stylesheet" href="<c:url value='/js/djb/apispec/editor-content.css'/>" />
|
||||
<script src="<c:url value='/js/jquery-1.12.1.min.js'/>"></script>
|
||||
<script src="<c:url value='/addon/summernote/summernote-lite.min.js'/>"></script>
|
||||
<script src="<c:url value='/addon/summernote/lang/summernote-ko-KR.js'/>"></script>
|
||||
|
||||
<link rel="stylesheet" href="<c:url value='/js/djb/apispec/styles.css'/>" />
|
||||
<style>
|
||||
/* 페이지 자체 세로 스크롤 제거 — 뷰포트 고정, 내부 패널만 스크롤 */
|
||||
html, body { height: 100%; margin: 0; overflow-x: auto; overflow-y: hidden; }
|
||||
#djb-wrap { height: 100vh; display: flex; flex-direction: column; }
|
||||
#djb-wrap > header, #djb-wrap > nav { flex: 0 0 auto; }
|
||||
#body { flex: 1 1 auto; min-height: 0 !important; overflow: hidden; }
|
||||
#form-panel { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||
#form-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
||||
#preview-panel { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
||||
#preview-swagger, #preview-monaco { flex: 1 1 auto; min-height: 0; height: auto !important; overflow: auto; }
|
||||
#preview-monaco .CodeMirror { height: 100%; }
|
||||
/* 패널 내부 sticky 는 flex 행으로 (스크롤 컨테이너는 form-content / preview) */
|
||||
#form-panel > .sticky, #preview-panel > .sticky { position: static !important; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.DJB_CTX = {
|
||||
eaiSvcName: "${param.eaiSvcName}",
|
||||
apiName: "${param.eaiSvcDesc}",
|
||||
detailUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>?cmd=DETAIL_SPEC&serviceType=APIGW",
|
||||
saveUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>?cmd=SAVE&serviceType=APIGW",
|
||||
swaggerBase: "<c:url value='/plugins/swaggerUI/'/>",
|
||||
gwAddress: "${gwAddress}",
|
||||
orgPopupUrl: "<c:url value='/onl/transaction/apim/apiSpecMan.view'/>",
|
||||
groupPopupUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.view'/>?cmd=GROUP_POPUP",
|
||||
jsonUrl: "<c:url value='/onl/transaction/apim/djbApiSpecMan.json'/>"
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-800 antialiased" style="min-width:1600px;">
|
||||
|
||||
<div id="djb-wrap" class="mx-auto" style="width:1600px;">
|
||||
|
||||
<!-- ===== Header ===== -->
|
||||
<header class="h-14 flex items-center justify-between px-6 bg-white border-b border-slate-200 sticky top-0 z-30">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="font-semibold text-slate-900">OpenAPI Spec 에디터</div>
|
||||
<span class="text-slate-300">|</span>
|
||||
<span class="text-sm text-slate-500">인터페이스:</span>
|
||||
<span class="text-sm font-medium text-slate-800" id="hdr-project-name">${param.eaiSvcName}</span>
|
||||
<span class="ml-2 px-2 py-0.5 text-[11px] rounded-full bg-slate-100 text-slate-600 border border-slate-200" id="hdr-version">v1.0.0</span>
|
||||
<span id="hdr-source" class="hidden ml-1 px-2 py-0.5 text-[11px] rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="btn-newwin" class="hidden px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">새창으로 띄우기</button>
|
||||
<button id="btn-regen" class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">자동 생성(초기화)</button>
|
||||
<button id="btn-save" class="px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700">저장</button>
|
||||
<div class="relative">
|
||||
<button id="btn-export" class="px-3 py-1.5 text-sm rounded bg-brand-600 text-white hover:bg-brand-700">내보내기 ▾</button>
|
||||
<div id="export-menu" class="hidden absolute right-0 mt-1 w-44 bg-white border border-slate-200 rounded shadow-lg z-40">
|
||||
<button data-export="yaml" class="block w-full text-left px-3 py-2 text-sm hover:bg-slate-50">YAML 다운로드</button>
|
||||
<button data-export="json" class="block w-full text-left px-3 py-2 text-sm hover:bg-slate-50">JSON 다운로드</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="btn-close" class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50">닫기</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ===== Stepper ===== -->
|
||||
<nav class="bg-white border-b border-slate-200 sticky z-20" style="top:56px;">
|
||||
<div class="h-1 bg-slate-100">
|
||||
<div id="stepper-progress" class="h-1 bg-brand-500 transition-all duration-300" style="width:16.66%"></div>
|
||||
</div>
|
||||
<ol id="stepper" class="grid grid-cols-5 px-2"></ol>
|
||||
</nav>
|
||||
|
||||
<!-- ===== Body ===== -->
|
||||
<main id="body" class="flex" style="min-height: calc(100vh - 56px - 72px);">
|
||||
<section id="form-panel" class="bg-white border-r border-slate-200 transition-all duration-300" style="width:960px;">
|
||||
<div class="flex items-center justify-between px-6 py-3 border-b border-slate-100 sticky bg-white z-10" style="top:128px;">
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="text-xs uppercase tracking-wide text-slate-400">Step <span id="form-step-num">1</span> / 5</span>
|
||||
<h2 class="text-base font-semibold text-slate-800" id="form-step-title">기본정보</h2>
|
||||
</div>
|
||||
<button id="btn-toggle-preview-from-form" class="hidden text-xs px-2 py-1 rounded border border-slate-200 text-slate-600 hover:bg-slate-50">▶ 미리보기 다시 열기</button>
|
||||
</div>
|
||||
<div id="form-content" class="px-6 py-5"></div>
|
||||
<div class="sticky bottom-0 bg-white border-t border-slate-200 px-6 py-3 flex items-center justify-between">
|
||||
<button id="btn-prev" class="px-4 py-2 text-sm rounded border border-slate-300 bg-white hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed">◀ 이전</button>
|
||||
<div class="text-xs text-slate-500" id="footer-hint">필수 항목을 채우고 다음 단계로 이동하세요</div>
|
||||
<button id="btn-next" class="px-4 py-2 text-sm rounded bg-brand-600 text-white hover:bg-brand-700">다음 ▶</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside id="preview-panel" class="bg-slate-50 transition-all duration-300" style="width:640px;">
|
||||
<div class="flex items-center justify-between px-4 py-2 border-b border-slate-200 bg-white sticky z-10" style="top:128px;">
|
||||
<div class="flex items-center gap-1">
|
||||
<button data-ptab="swagger" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-brand-600 text-brand-700 font-medium">Swagger UI</button>
|
||||
<button data-ptab="yaml-ro" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-transparent text-slate-600 hover:text-slate-800">YAML 보기</button>
|
||||
<button data-ptab="yaml-edit" class="preview-tab px-3 py-1.5 text-sm rounded-t border-b-2 border-transparent text-slate-600 hover:text-slate-800">스펙 편집</button>
|
||||
</div>
|
||||
<button id="btn-toggle-preview" class="text-xs px-2 py-1 rounded border border-slate-200 text-slate-600 hover:bg-slate-100">패널 숨기기 ▶</button>
|
||||
</div>
|
||||
<div id="yaml-edit-bar" class="hidden flex items-center justify-between bg-amber-50 border-b border-amber-200 px-4 py-2 text-xs">
|
||||
<span class="text-amber-800">YAML 을 직접 편집한 뒤 [Form 에 적용] 을 눌러 양식에 동기화하세요.</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="btn-yaml-revert" class="px-2 py-1 rounded border border-amber-300 bg-white hover:bg-amber-100">변경 취소</button>
|
||||
<button id="btn-yaml-apply" class="px-2 py-1 rounded bg-amber-600 text-white hover:bg-amber-700">Form 에 적용</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="preview-swagger" class="overflow-auto" style="height: calc(100vh - 56px - 72px - 41px);"></div>
|
||||
<div id="preview-monaco" class="hidden" style="height: calc(100vh - 56px - 72px - 41px);"></div>
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="fixed bottom-5 left-1/2 -translate-x-1/2 hidden px-4 py-2 rounded bg-slate-900 text-white text-sm shadow-lg z-50"></div>
|
||||
|
||||
<!-- Security scheme modal -->
|
||||
<div id="sec-modal" class="hidden fixed inset-0 z-50 bg-slate-900/40 flex items-center justify-center">
|
||||
<div class="bg-white rounded-lg shadow-xl w-[640px] max-h-[80vh] overflow-auto">
|
||||
<div class="flex items-center justify-between px-5 py-3 border-b border-slate-200">
|
||||
<h3 class="font-semibold text-slate-800">보안 스킴 추가</h3>
|
||||
<button class="text-slate-400 hover:text-slate-700" data-sec-close>✕</button>
|
||||
</div>
|
||||
<div class="p-5" id="sec-modal-body"></div>
|
||||
<div class="px-5 py-3 border-t border-slate-200 flex justify-end gap-2 bg-slate-50">
|
||||
<button class="px-3 py-1.5 text-sm rounded border border-slate-300 bg-white" data-sec-close>취소</button>
|
||||
<button id="btn-sec-save" class="px-3 py-1.5 text-sm rounded bg-brand-600 text-white">추가</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="<c:url value='/js/djb/apispec/sample-data.js'/>"></script>
|
||||
<script src="<c:url value='/js/djb/apispec/app.js'/>"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,122 @@
|
||||
/*!
|
||||
* djb-swagger-i18n.js
|
||||
* Swagger UI 영문 라벨을 한글로 치환하는 i18n 패치 (방법 B - DOM patch)
|
||||
* 배포 경로 권장: static/plugins/swaggerUI/djb-swagger-i18n.js
|
||||
* index.html 의 swagger-initializer.js 직전 또는 직후에 <script> 로 로드
|
||||
*
|
||||
* ?lang=en 쿼리스트링이 있으면 패치 비활성화 (원문 보기)
|
||||
* 매핑 카탈로그는 01-i18n-mapping/i18n-strings.csv 와 동기 유지
|
||||
*/
|
||||
(function (window) {
|
||||
'use strict';
|
||||
|
||||
// ── 비활성화 옵션 ──
|
||||
if (/[?&]lang=en\b/.test(window.location.search)) return;
|
||||
|
||||
// ── 매핑 (key 는 디버깅용, 실제 비교는 EN 문자열 일치) ──
|
||||
var I18N_MAP = {
|
||||
// P0 — OAuth2 모달 본문
|
||||
'auth.modal.title': { en: 'Available authorizations', ko: '사용 가능한 인증' },
|
||||
'auth.scopes.description': {
|
||||
en: 'Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.',
|
||||
ko: '스코프는 최종 사용자를 대신하여 애플리케이션이 데이터에 접근할 수 있는 권한 수준을 부여합니다. 각 API는 하나 이상의 스코프를 선언할 수 있습니다.'
|
||||
},
|
||||
'auth.scopes.swagger_grant': {
|
||||
en: 'API requires the following scopes. Select which ones you want to grant to Swagger UI.',
|
||||
ko: '이 API 는 아래 스코프가 필요합니다. Swagger UI 에 부여할 스코프를 선택하세요.'
|
||||
},
|
||||
'auth.flow.clientCredentials': { en: 'OAuth2 client credentials flow', ko: 'OAuth2 클라이언트 자격증명 흐름' },
|
||||
'auth.flow.implicit': { en: 'OAuth2 implicit flow', ko: 'OAuth2 암묵적 흐름' },
|
||||
'auth.flow.password': { en: 'OAuth2 password flow', ko: 'OAuth2 비밀번호 흐름' },
|
||||
'auth.flow.authorizationCode': { en: 'OAuth2 authorization code flow', ko: 'OAuth2 인가 코드 흐름' },
|
||||
'auth.field.tokenUrl': { en: 'Token URL:', ko: '토큰 URL:' },
|
||||
'auth.field.flow': { en: 'Flow:', ko: '흐름:' },
|
||||
'auth.field.clientId': { en: 'client_id:', ko: '클라이언트 ID (client_id):' },
|
||||
'auth.field.clientSecret': { en: 'client_secret:',ko: '클라이언트 시크릿 (client_secret):' },
|
||||
'auth.field.scopes': { en: 'Scopes:', ko: '스코프:' },
|
||||
'auth.action.selectAll': { en: 'select all', ko: '모두 선택' },
|
||||
'auth.action.selectNone': { en: 'select none', ko: '선택 해제' },
|
||||
'auth.action.authorize': { en: 'Authorize', ko: '인증' },
|
||||
'auth.action.close': { en: 'Close', ko: '닫기' },
|
||||
'auth.action.logout': { en: 'Logout', ko: '로그아웃' },
|
||||
'auth.modal.authorized': { en: 'authorized', ko: '인증됨' },
|
||||
|
||||
// P1 — 기타 인증 흐름
|
||||
'auth.scheme.apiKey': { en: 'API key', ko: 'API 키' },
|
||||
'auth.field.name': { en: 'name:', ko: '이름:' },
|
||||
'auth.field.in': { en: 'in:', ko: '위치:' },
|
||||
'auth.field.value': { en: 'Value:', ko: '값:' },
|
||||
'basic.field.username': { en: 'Username:',ko: '사용자 ID:' },
|
||||
'basic.field.password': { en: 'Password:',ko: '비밀번호:' }
|
||||
// P2 (오퍼레이션/응답) 는 필요 시 추가
|
||||
};
|
||||
|
||||
// 빠른 lookup 을 위해 EN → KO 인덱스 사전 생성
|
||||
var EN_TO_KO = Object.create(null);
|
||||
Object.keys(I18N_MAP).forEach(function (k) {
|
||||
var e = I18N_MAP[k];
|
||||
EN_TO_KO[e.en] = e.ko;
|
||||
});
|
||||
|
||||
// ── 치환 대상 노드 식별 + 치환 ──
|
||||
function translateNode(node) {
|
||||
if (!node) return;
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
var raw = node.nodeValue;
|
||||
if (!raw) return;
|
||||
var trimmed = raw.trim();
|
||||
if (!trimmed) return;
|
||||
// 1) 완전 일치 (가장 안전)
|
||||
if (EN_TO_KO[trimmed]) {
|
||||
node.nodeValue = raw.replace(trimmed, EN_TO_KO[trimmed]);
|
||||
return;
|
||||
}
|
||||
// 2) 라벨 colon 패턴: "Token URL:" 같이 끝이 ":" 인 짧은 라벨
|
||||
// (불필요 — 1번 완전 일치로 대부분 커버)
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
if (node.tagName === 'SCRIPT' || node.tagName === 'STYLE') return;
|
||||
|
||||
// input placeholder / button title 도 일부 케이스에 대비
|
||||
if (node.tagName === 'INPUT' && node.placeholder && EN_TO_KO[node.placeholder.trim()]) {
|
||||
node.placeholder = EN_TO_KO[node.placeholder.trim()];
|
||||
}
|
||||
|
||||
var i, child = node.childNodes, len = child.length;
|
||||
for (i = 0; i < len; i++) translateNode(child[i]);
|
||||
}
|
||||
|
||||
function translateAll() {
|
||||
var root = document.getElementById('swagger-ui') || document.body;
|
||||
translateNode(root);
|
||||
}
|
||||
|
||||
// ── MutationObserver — SwaggerUI 가 동적으로 DOM 을 다시 그릴 때마다 재적용 ──
|
||||
var observer = new MutationObserver(function (mutations) {
|
||||
for (var i = 0; i < mutations.length; i++) {
|
||||
var m = mutations[i];
|
||||
if (m.addedNodes && m.addedNodes.length) {
|
||||
for (var j = 0; j < m.addedNodes.length; j++) translateNode(m.addedNodes[j]);
|
||||
}
|
||||
if (m.type === 'characterData') {
|
||||
translateNode(m.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function start() {
|
||||
translateAll();
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', start);
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
})(window);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+83
-87
@@ -1,10 +1,9 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'java-library'
|
||||
id 'eclipse'
|
||||
id 'eclipse-wtp'
|
||||
id 'eclipse-wtp'
|
||||
id 'idea'
|
||||
id 'war'
|
||||
id 'com.diffplug.eclipse.apt' version '3.41.1'
|
||||
}
|
||||
|
||||
group 'com.eactive'
|
||||
@@ -32,11 +31,11 @@ allprojects {
|
||||
project.webAppDirName = 'WebContent'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(8)
|
||||
}
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(8)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
@@ -46,55 +45,56 @@ sourceSets {
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs = ['-parameters']
|
||||
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
|
||||
|
||||
aptOptions {
|
||||
processorArgs = [ 'querydsl.generatedAnnotationClass' : 'com.querydsl.core.annotations.Generated' ]
|
||||
}
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs = ['-parameters']
|
||||
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
|
||||
}
|
||||
|
||||
war {
|
||||
def profile = project.findProperty("profile") ?: "tomcat"
|
||||
|
||||
if (profile == "weblogic") {
|
||||
webXml = file("WebContent/WEB-INF/weblogic-web.xml")
|
||||
exclude 'WEB-INF/web.xml'
|
||||
}
|
||||
|
||||
|
||||
// rootSpec.exclude '**/persistence.xml'
|
||||
exclude '**/context.xml'
|
||||
exclude '**/context-*.xml'
|
||||
archiveFileName = "eapim-admin.war"
|
||||
duplicatesStrategy = DuplicatesStrategy.WARN
|
||||
def profile = project.findProperty("profile") ?: "tomcat"
|
||||
|
||||
if (profile == "weblogic") {
|
||||
webXml = file("WebContent/WEB-INF/weblogic-web.xml")ㅇ
|
||||
exclude 'WEB-INF/web.xml'
|
||||
}
|
||||
|
||||
|
||||
// rootSpec.exclude '**/persistence.xml'
|
||||
exclude '**/context.xml'
|
||||
exclude '**/context-*.xml'
|
||||
archiveFileName = "eapim-admin.war"
|
||||
duplicatesStrategy = DuplicatesStrategy.WARN
|
||||
}
|
||||
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs += ['-parameters', "-Xlint:unchecked", "-Xlint:deprecation"]
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs += ['-parameters', "-Xlint:unchecked", "-Xlint:deprecation"]
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
annotationProcessor "org.projectlombok:lombok:1.18.28", "org.projectlombok:lombok-mapstruct-binding:0.2.0", "org.mapstruct:mapstruct-processor:1.5.5.Final"
|
||||
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jpa", "jakarta.persistence:jakarta.persistence-api:2.2.3", "jakarta.annotation:jakarta.annotation-api:1.3.5"
|
||||
|
||||
|
||||
annotationProcessor "org.projectlombok:lombok:1.18.28",
|
||||
"org.projectlombok:lombok-mapstruct-binding:0.2.0",
|
||||
"org.mapstruct:mapstruct-processor:1.5.5.Final"
|
||||
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jpa",
|
||||
"jakarta.persistence:jakarta.persistence-api:2.2.3",
|
||||
"jakarta.annotation:jakarta.annotation-api:1.3.5"
|
||||
|
||||
implementation project(':elink-online-core')
|
||||
implementation project(':elink-online-transformer')
|
||||
implementation project(':elink-online-common')
|
||||
implementation project(':elink-online-emsclient')
|
||||
implementation project(':elink-portal-common')
|
||||
//implementation project(':kjb-safedb')
|
||||
implementation project(':elink-online-emsclient')
|
||||
implementation project(':elink-portal-common')
|
||||
//implementation project(':kjb-safedb')
|
||||
implementation project(":eapim-admin-djb")
|
||||
|
||||
/* Custom Libs */
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
/* Custom Libs (damo-manager.jar 는 Tomcat lib 가 런타임 제공 → compileOnly, WAR 미포함) */
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||
compileOnly files('libs/damo-manager.jar')
|
||||
|
||||
//implementation "org.dom4j:dom4j:2.1.3"
|
||||
//implementation "org.dom4j:dom4j:2.1.3"
|
||||
//implementation "com.rabbitmq:amqp-client:3.6.6"
|
||||
|
||||
implementation "commons-primitives:commons-primitives:1.0"
|
||||
@@ -116,10 +116,10 @@ dependencies {
|
||||
|
||||
// https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api
|
||||
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
|
||||
|
||||
|
||||
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.1'
|
||||
//implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1'
|
||||
|
||||
implementation "javax.jms:javax.jms-api:2.0"
|
||||
implementation "org.snmp4j:snmp4j:1.10.1"
|
||||
@@ -186,32 +186,32 @@ dependencies {
|
||||
implementation 'ch.qos.logback:logback-classic:1.2.10'
|
||||
implementation 'ch.qos.logback:logback-access:1.2.10'
|
||||
implementation 'ch.qos.logback:logback-core:1.2.10'
|
||||
|
||||
implementation files("libs/eai-bap-client.jar")
|
||||
|
||||
compileOnly 'javax.xml.bind:jaxb-api:2.3.1'
|
||||
|
||||
//implementation 'org.postgresql:postgresql:42.2.23'
|
||||
|
||||
implementation 'backport-util-concurrent:backport-util-concurrent:3.0'
|
||||
|
||||
implementation ('software.amazon.awssdk:s3:2.20.142') {
|
||||
exclude group: 'org.slf4j', module: 'slf4j-api'
|
||||
exclude group: 'org.slf4j', module: 'logback-classic'
|
||||
}
|
||||
implementation 'software.amazon.awssdk:sso:2.20.142'
|
||||
implementation 'software.amazon.awssdk:sts:2.20.142'
|
||||
|
||||
implementation group: 'commons-net', name: 'commons-net', version: '3.5'
|
||||
implementation files("libs/eai-bap-client.jar")
|
||||
|
||||
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없어 xercesImpl 가 NCDFE 를 일으킴.
|
||||
// xml-apis 1.4.01 에 그 클래스가 포함됨. 직접 의존성으로 묶어 WAR 에 패키징되도록 함.
|
||||
implementation 'xml-apis:xml-apis:1.4.01'
|
||||
compileOnly 'javax.xml.bind:jaxb-api:2.3.1'
|
||||
|
||||
testRuntimeOnly 'com.h2database:h2:2.1.214'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.15'
|
||||
//implementation 'org.postgresql:postgresql:42.2.23'
|
||||
|
||||
implementation 'backport-util-concurrent:backport-util-concurrent:3.0'
|
||||
|
||||
implementation ('software.amazon.awssdk:s3:2.20.142') {
|
||||
exclude group: 'org.slf4j', module: 'slf4j-api'
|
||||
exclude group: 'org.slf4j', module: 'logback-classic'
|
||||
}
|
||||
implementation 'software.amazon.awssdk:sso:2.20.142'
|
||||
implementation 'software.amazon.awssdk:sts:2.20.142'
|
||||
|
||||
implementation group: 'commons-net', name: 'commons-net', version: '3.5'
|
||||
|
||||
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없어 xercesImpl 가 NCDFE 를 일으킴.
|
||||
// xml-apis 1.4.01 에 그 클래스가 포함됨. 직접 의존성으로 묶어 WAR 에 패키징되도록 함.
|
||||
implementation 'xml-apis:xml-apis:1.4.01'
|
||||
|
||||
testRuntimeOnly 'com.h2database:h2:2.1.214'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.15'
|
||||
}
|
||||
|
||||
test {
|
||||
@@ -219,9 +219,9 @@ test {
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude group: 'log4j', module: 'log4j'
|
||||
exclude group: 'org.apache.activemq'
|
||||
exclude group: 'org.codehaus.jackson'
|
||||
exclude group: 'log4j', module: 'log4j'
|
||||
exclude group: 'org.apache.activemq'
|
||||
exclude group: 'org.codehaus.jackson'
|
||||
resolutionStrategy {
|
||||
// 10 minute cache of dynamic version navigation
|
||||
cacheDynamicVersionsFor 10, 'minutes'
|
||||
@@ -245,29 +245,25 @@ task settingEclipseEncoding {
|
||||
}
|
||||
|
||||
task initDirs() {
|
||||
file(generatedJavaDir).mkdirs()
|
||||
file(generatedJavaDir).mkdirs()
|
||||
}
|
||||
|
||||
eclipse {
|
||||
wtp {
|
||||
component {
|
||||
contextPath = 'monitoring'
|
||||
}
|
||||
}
|
||||
synchronizationTasks settingEclipseEncoding, initDirs
|
||||
jdt {
|
||||
apt {
|
||||
// generated 된 패스 경로를 .setting/org.eclipse.jdt.apt.core.prefs 의 org.eclipse.jdt.apt.genSrcDir에 적용한다.
|
||||
// project > properties > Java Compiler > Annoation Processing 화면에서 확인 가능하다.
|
||||
genSrcDir = file(generatedJavaDir)
|
||||
}
|
||||
}
|
||||
project {
|
||||
if (!natures.contains('org.eclipse.buildship.core.gradleprojectnature')) {
|
||||
natures.add('org.eclipse.buildship.core.gradleprojectnature')
|
||||
buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
|
||||
}
|
||||
}
|
||||
wtp {
|
||||
component {
|
||||
contextPath = 'monitoring'
|
||||
}
|
||||
}
|
||||
synchronizationTasks settingEclipseEncoding, initDirs
|
||||
jdt {
|
||||
|
||||
}
|
||||
project {
|
||||
if (!natures.contains('org.eclipse.buildship.core.gradleprojectnature')) {
|
||||
natures.add('org.eclipse.buildship.core.gradleprojectnature')
|
||||
buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
||||
+61
-34
@@ -1,9 +1,9 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'eclipse'
|
||||
id 'eclipse-wtp'
|
||||
id 'idea'
|
||||
id 'war'
|
||||
id 'project-report'
|
||||
id 'checkstyle'
|
||||
}
|
||||
|
||||
group 'com.eactive'
|
||||
@@ -14,17 +14,19 @@ def quartzVersion = "2.2.1"
|
||||
def queryDslVersion = "5.0.0"
|
||||
def hibernateVersion = "5.6.15.Final"
|
||||
|
||||
/*def nexusUrl = "https://nexus.eactive.synology.me:8090"*/
|
||||
def nexusUrl = "https://nexus.eactive.synology.me:8090"
|
||||
//def useOnJboss = false
|
||||
|
||||
def generatedJavaDir = "$buildDir/generated/java"
|
||||
|
||||
/*repositories {
|
||||
maven {
|
||||
url "${nexusUrl}/repository/maven-public/"
|
||||
allowInsecureProtocol = true
|
||||
}
|
||||
}*/
|
||||
allprojects {
|
||||
repositories {
|
||||
maven {
|
||||
url "${nexusUrl}/repository/maven-public/"
|
||||
allowInsecureProtocol = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.webAppDirName = 'WebContent'
|
||||
|
||||
@@ -34,10 +36,6 @@ java {
|
||||
}
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = '8.45.1'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
@@ -48,10 +46,7 @@ sourceSets {
|
||||
|
||||
compileJava {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs = [
|
||||
'-parameters',
|
||||
'-Aquerydsl.generatedAnnotationClass=com.querydsl.core.annotations.Generated'
|
||||
]
|
||||
options.compilerArgs = ['-parameters']
|
||||
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
|
||||
}
|
||||
|
||||
@@ -67,7 +62,6 @@ war {
|
||||
// rootSpec.exclude '**/persistence.xml'
|
||||
exclude '**/context.xml'
|
||||
exclude '**/context-*.xml'
|
||||
exclude '**/context_*.xml'
|
||||
archiveFileName = "eapim-admin.war"
|
||||
duplicatesStrategy = DuplicatesStrategy.WARN
|
||||
}
|
||||
@@ -89,11 +83,12 @@ dependencies {
|
||||
implementation project(':elink-online-common')
|
||||
implementation project(':elink-online-emsclient')
|
||||
implementation project(':elink-portal-common')
|
||||
implementation project(':kjb-safedb')
|
||||
implementation project(":eapim-admin-kjb")
|
||||
//implementation project(':kjb-safedb')
|
||||
implementation project(":eapim-admin-djb")
|
||||
|
||||
/* Custom Libs */
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
/* Custom Libs (damo-manager.jar 는 Tomcat lib 가 런타임 제공 → compileOnly, WAR 미포함) */
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||
compileOnly files('libs/damo-manager.jar')
|
||||
|
||||
//implementation "org.dom4j:dom4j:2.1.3"
|
||||
//implementation "com.rabbitmq:amqp-client:3.6.6"
|
||||
@@ -126,7 +121,7 @@ dependencies {
|
||||
implementation "org.snmp4j:snmp4j:1.10.1"
|
||||
implementation "com.googlecode.json-simple:json-simple:1.1.1"
|
||||
|
||||
implementation "io.netty:netty-all:4.1.0.Final"
|
||||
implementation "io.netty:netty-all:4.1.94.Final"
|
||||
|
||||
compileOnly "org.apache.mina:mina-filter-ssl:1.1.6"
|
||||
compileOnly "org.apache.mina:mina-core:1.1.6"
|
||||
@@ -203,13 +198,12 @@ dependencies {
|
||||
implementation 'software.amazon.awssdk:sso:2.20.142'
|
||||
implementation 'software.amazon.awssdk:sts:2.20.142'
|
||||
|
||||
implementation ('io.kubernetes:client-java:18.0.1') {
|
||||
exclude group: 'org.slf4j', module: 'slf4j-api'
|
||||
exclude group: 'org.slf4j', module: 'logback-classic'
|
||||
}
|
||||
|
||||
implementation group: 'commons-net', name: 'commons-net', version: '3.5'
|
||||
|
||||
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없어 xercesImpl 가 NCDFE 를 일으킴.
|
||||
// xml-apis 1.4.01 에 그 클래스가 포함됨. 직접 의존성으로 묶어 WAR 에 패키징되도록 함.
|
||||
implementation 'xml-apis:xml-apis:1.4.01'
|
||||
|
||||
testRuntimeOnly 'com.h2database:h2:2.1.214'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
||||
@@ -229,18 +223,51 @@ configurations.all {
|
||||
cacheDynamicVersionsFor 10, 'minutes'
|
||||
// Do not cache changing modules
|
||||
cacheChangingModulesFor 0, 'seconds'
|
||||
|
||||
// JDK 8 의 rt.jar 에는 org.w3c.dom.ElementTraversal 이 없음.
|
||||
// 일부 transitive 가 끌어오는 xml-apis:1.0.b2 (2002, DOM L2) 는 이 클래스를 포함하지 않아
|
||||
// xercesImpl 가 NoClassDefFoundError 를 일으킴 → 1.4.01 로 강제 통일.
|
||||
force 'xml-apis:xml-apis:1.4.01'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
task settingEclipseEncoding {
|
||||
if (project.file('.settings').exists()) {
|
||||
File f = file('.settings/org.eclipse.core.resources.prefs')
|
||||
f.write('eclipse.preferences.version=1\n')
|
||||
f.append('encoding/<project>=utf-8')
|
||||
}
|
||||
}
|
||||
|
||||
task initDirs() {
|
||||
file(generatedJavaDir).mkdirs()
|
||||
}
|
||||
|
||||
|
||||
tasks.withType(Checkstyle) {
|
||||
reports {
|
||||
xml.required = false
|
||||
html.required = true
|
||||
}
|
||||
eclipse {
|
||||
wtp {
|
||||
component {
|
||||
contextPath = 'monitoring'
|
||||
}
|
||||
}
|
||||
synchronizationTasks settingEclipseEncoding, initDirs
|
||||
jdt {
|
||||
apt {
|
||||
// generated 된 패스 경로를 .setting/org.eclipse.jdt.apt.core.prefs 의 org.eclipse.jdt.apt.genSrcDir에 적용한다.
|
||||
// project > properties > Java Compiler > Annoation Processing 화면에서 확인 가능하다.
|
||||
genSrcDir = file(generatedJavaDir)
|
||||
}
|
||||
}
|
||||
project {
|
||||
if (!natures.contains('org.eclipse.buildship.core.gradleprojectnature')) {
|
||||
natures.add('org.eclipse.buildship.core.gradleprojectnature')
|
||||
buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.fork = true // 컴파일을 별도 프로세스로 분리
|
||||
options.forkOptions.memoryMaximumSize = '4g' // 컴파일러에 2GB 할당 (필요시 4g로 증량)
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
Binary file not shown.
@@ -33,6 +33,7 @@ import com.eactive.eai.rms.data.entity.man.role.Role;
|
||||
import com.eactive.eai.rms.data.entity.man.role.RoleService;
|
||||
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessService;
|
||||
import com.eactive.eai.rms.onl.common.exception.BizException;
|
||||
import com.eactive.ext.djb.DamoManager;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -99,7 +100,7 @@ public class UserManService extends BaseService {
|
||||
userInfo.setRoleidnfiname(CommonConstants.DEPT_DEVELOPER);
|
||||
|
||||
try {
|
||||
userInfo.setPassword(Seed.encrypt(userUI.getUserId()));
|
||||
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256,userUI.getUserId()));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error encrypting password", e);
|
||||
}
|
||||
@@ -140,7 +141,7 @@ public class UserManService extends BaseService {
|
||||
|
||||
public void updatePassword(UserUI userUI) throws Exception {
|
||||
UserInfo userInfo = userInfoService.getById(userUI.getUserId());
|
||||
userInfo.setPassword(Seed.encrypt(userUI.getUserId()));
|
||||
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256, userUI.getUserId()));
|
||||
userUIMapper.updateToEntity(userUI, userInfo);
|
||||
userInfoService.save(userInfo);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.eactive.eai.rms.data.entity.man.user.UserServiceType;
|
||||
import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeId;
|
||||
import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService;
|
||||
import com.eactive.eai.rms.data.entity.onl.bzwkdstcd.UserBusinessService;
|
||||
import com.eactive.ext.djb.DamoManager;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@@ -124,7 +125,7 @@ public class UserSyncService extends BaseService {
|
||||
UserInfo userInfo = new UserInfo();
|
||||
userInfo.setUserid(userId);
|
||||
userInfo.setUsername(userInfoMap.get("EMPY_NM").toString());
|
||||
userInfo.setPassword(Seed.encrypt(userId));
|
||||
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256,userId));
|
||||
userInfo.setRoleidnfiname(CommonConstants.DEPT_DEVELOPER);
|
||||
|
||||
userInfoService.save(userInfo);
|
||||
|
||||
@@ -53,9 +53,11 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
||||
"^data:image/.*",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
// XSS 문자 변환을 건너뛸 파라미터 목록 (리치 텍스트 에디터 콘텐츠 등)
|
||||
// XSS 패턴 필터링은 적용하지만 <, > 등으로 변환하지 않음
|
||||
private static final String[] SKIP_CHAR_CONVERT_PARAMS = {"contents", "content", "description"};
|
||||
// XSS 문자 변환을 건너뛸 파라미터 목록 (리치 텍스트 에디터 콘텐츠, 메시지 템플릿 본문 등)
|
||||
// XSS 패턴 필터링은 적용하지만 <, >, & 등으로 변환하지 않음
|
||||
// (메시지 템플릿은 SMS/메신저 평문으로 전송되므로 '&'가 '&'로 깨지면 안 됨)
|
||||
private static final String[] SKIP_CHAR_CONVERT_PARAMS = {"contents", "content", "description",
|
||||
"subjectTemplate", "smsTemplate", "emailTemplate", "messengerTemplate"};
|
||||
|
||||
// 현재 처리 중인 파라미터명 (cleanXSS에서 문자 변환 여부 결정에 사용)
|
||||
private ThreadLocal<String> currentParameter = new ThreadLocal<>();
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.springframework.web.servlet.View;
|
||||
|
||||
import com.eactive.eai.common.seed.Seed;
|
||||
import com.eactive.eai.rms.ext.kjb.smsauth.SmsAuthService;
|
||||
import com.eactive.ext.djb.DamoManager;
|
||||
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
|
||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||
import com.eactive.eai.rms.common.datasource.DataSourceType;
|
||||
@@ -72,7 +73,8 @@ public class MainController implements InterceptorSkipController {
|
||||
private final LoginService loginService;
|
||||
private final MonitoringPropertyService monitoringPropertyService;
|
||||
private final UserLoginHistoryService userLoginHistoryService;
|
||||
|
||||
private final SmsAuthService smsAuthService;
|
||||
|
||||
@Autowired
|
||||
public MainController(LocaleMessage localeMessage,
|
||||
MonitoringContext monitoringContext,
|
||||
@@ -81,7 +83,8 @@ public class MainController implements InterceptorSkipController {
|
||||
UserRoleService userRoleService,
|
||||
UserServiceTypeService userServiceTypeService,
|
||||
MonitoringPropertyService monitoringPropertyService,
|
||||
UserLoginHistoryService userLoginHistoryService) {
|
||||
UserLoginHistoryService userLoginHistoryService,
|
||||
SmsAuthService smsAuthService) {
|
||||
this.localeMessage = localeMessage;
|
||||
this.monitoringContext = monitoringContext;
|
||||
this.userInfoService = userInfoService;
|
||||
@@ -90,6 +93,7 @@ public class MainController implements InterceptorSkipController {
|
||||
this.userServiceTypeService = userServiceTypeService;
|
||||
this.monitoringPropertyService = monitoringPropertyService;
|
||||
this.userLoginHistoryService = userLoginHistoryService;
|
||||
this.smsAuthService = smsAuthService;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/loginForm.do")
|
||||
@@ -202,7 +206,7 @@ public class MainController implements InterceptorSkipController {
|
||||
|
||||
// SMS 인증번호 생성 및 발송
|
||||
String authCode = smsAuthService.generateAuthCode();
|
||||
boolean sent = smsAuthService.sendAuthCode(userInfo.getCphnno(), authCode);
|
||||
boolean sent = smsAuthService.sendAuthCode(userInfo, authCode);
|
||||
|
||||
if (!sent) {
|
||||
return LoginResponseDto.builder()
|
||||
@@ -263,16 +267,8 @@ public class MainController implements InterceptorSkipController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SmsAuthService 인스턴스 조회 (singleton)
|
||||
*/
|
||||
private SmsAuthService getSmsAuthService() {
|
||||
try {
|
||||
return SmsAuthService.getInstance();
|
||||
} catch (Exception e) {
|
||||
logger.warn("SmsAuthService 초기화 실패 - SMS 인증 비활성화 처리", e);
|
||||
return null;
|
||||
}
|
||||
return smsAuthService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -363,7 +359,7 @@ public class MainController implements InterceptorSkipController {
|
||||
String loginMode = System.getProperty("login.mode");
|
||||
if("db".equals(loginMode) || "DB".equals(loginMode)) {
|
||||
/* 기존로직 - LDAP 사용 x */
|
||||
if (!userInfo.getPassword().equals(Seed.encrypt(dto.getPassword()))) {
|
||||
if (!userInfo.getPassword().equals(DamoManager.getInstance().hash(DamoManager.SHA256, dto.getPassword()))) {
|
||||
// password 다름.
|
||||
setLoginFailure(request, session, "login.loginfail1",
|
||||
"login.loginfail2");
|
||||
@@ -799,9 +795,9 @@ public class MainController implements InterceptorSkipController {
|
||||
.getString("login.pwdmismatch4"));
|
||||
return REDIRECT_CHANGE_PASSWORD_URL;
|
||||
}
|
||||
|
||||
|
||||
// 3. 비밀번호 체크
|
||||
if (!userInfo.getPassword().equals(Seed.encrypt(resetPassword))
|
||||
if (!userInfo.getPassword().equals(DamoManager.getInstance().hash(DamoManager.SHA256,resetPassword))
|
||||
|| !userInfo.getUserid().equals(resetUserId)) {
|
||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
|
||||
localeMessage
|
||||
@@ -812,7 +808,7 @@ public class MainController implements InterceptorSkipController {
|
||||
}
|
||||
|
||||
// 4. 비밀번호 변경
|
||||
userInfo.setPassword(Seed.encrypt(changePassword));
|
||||
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256,changePassword));
|
||||
userInfoService.save(userInfo);
|
||||
|
||||
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "S", localeMessage
|
||||
|
||||
@@ -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_");
|
||||
|
||||
@@ -124,6 +124,7 @@ public class EMSSchedulerImpl implements EMSScheduler {
|
||||
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);
|
||||
@@ -132,26 +133,47 @@ public class EMSSchedulerImpl implements EMSScheduler {
|
||||
}
|
||||
|
||||
private void reloadClusteredJob(JobInfo jobInfo) throws SchedulerException, ClassNotFoundException {
|
||||
TriggerKey triggerKey = new TriggerKey(getTriggerName(jobInfo.getId()), Scheduler.DEFAULT_GROUP);
|
||||
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);
|
||||
if (existing != null) {
|
||||
JobDetail existingJob = clusteredScheduler.getJobDetail(jobKey);
|
||||
|
||||
if (existing != null && existingJob != null) {
|
||||
// JobDetail(JobData 포함) 갱신
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<Job> jobClass = (Class<Job>) Class.forName(jobInfo.getJobClassName());
|
||||
JobBuilder jobBuilder = newJob(jobClass).withIdentity(jobKey);
|
||||
for (JobData jobData : jobInfo.getJobDatas()) {
|
||||
jobBuilder.usingJobData(jobData.getId().getDataKey(), jobData.getDataValue());
|
||||
}
|
||||
clusteredScheduler.addJob(jobBuilder.build(), true);
|
||||
|
||||
// 트리거(cron) 갱신
|
||||
CronTrigger newTrigger = newTrigger()
|
||||
.withIdentity(triggerKey)
|
||||
.withSchedule(cronSchedule(jobInfo.getCronExp()))
|
||||
.build();
|
||||
Date ft = clusteredScheduler.rescheduleJob(triggerKey, newTrigger);
|
||||
log.info("{} rescheduled at: {} cron: {}", jobInfo.getId(), ft, jobInfo.getCronExp());
|
||||
} else {
|
||||
deleteAndAddJob(clusteredScheduler, jobInfo);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,10 +199,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.eactive.eai.rms.data.entity.onl.apim.apigroup;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroupApi;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroupApiId;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroup;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.QApiGroupApi;
|
||||
import com.eactive.eai.data.jpa.AbstractDataService;
|
||||
@@ -75,4 +76,37 @@ public class ApiGroupService extends AbstractDataService<ApiGroup, String, ApiGr
|
||||
.where(qApiGroup.id.in(apiGroupIds))
|
||||
.fetch();
|
||||
}
|
||||
|
||||
/** API 를 지정한 그룹 목록에만 소속시킨다(기존 소속 제거 후 재설정). */
|
||||
public void setApiGroupsForApi(String apiId, List<String> groupIds) {
|
||||
deleteApiGroupApiByApiId(apiId);
|
||||
if (groupIds == null) {
|
||||
return;
|
||||
}
|
||||
for (String gid : groupIds) {
|
||||
if (StringUtils.isBlank(gid)) {
|
||||
continue;
|
||||
}
|
||||
ApiGroup group = repository.findById(gid).orElse(null);
|
||||
if (group == null) {
|
||||
continue;
|
||||
}
|
||||
Hibernate.initialize(group.getApiGroupApiList());
|
||||
List<ApiGroupApi> list = group.getApiGroupApiList();
|
||||
boolean exists = false;
|
||||
for (ApiGroupApi a : list) {
|
||||
if (a.getId() != null && apiId.equals(a.getId().getApiId())) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
ApiGroupApi a = new ApiGroupApi();
|
||||
a.setId(new ApiGroupApiId(gid, apiId));
|
||||
a.setDisplayOrder(list.size() + 1);
|
||||
list.add(a);
|
||||
repository.save(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -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> {
|
||||
}
|
||||
+65
@@ -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);
|
||||
}
|
||||
}
|
||||
+29
@@ -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;
|
||||
}
|
||||
+6
@@ -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;
|
||||
|
||||
|
||||
+56
-2
@@ -8,12 +8,14 @@ import com.eactive.apim.portal.template.entity.QMessageRequest;
|
||||
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
|
||||
import com.querydsl.core.BooleanBuilder;
|
||||
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||
import com.querydsl.jpa.JPAExpressions;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@@ -36,7 +38,22 @@ public class PortalUserService extends AbstractEMSDataSerivce<PortalUser, String
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(portalUserUISearch.getSearchUserId())) {
|
||||
predicate.and(qPortalUser.loginId.containsIgnoreCase(portalUserUISearch.getSearchUserId()));
|
||||
// login_id / email_addr 는 PersonalDataEncryptConverter 로 암호화 저장되는 컬럼이다.
|
||||
// 암호화는 부분문자열을 보존하지 않으므로 LIKE/contains 검색은 절대 매칭되지 않는다.
|
||||
// 결정적(deterministic) 암호화라 정확일치(eq)만 가능하므로, login_id 와 email_addr 둘 다 정확일치로 OR 검색한다.
|
||||
String searchUserId = portalUserUISearch.getSearchUserId().trim();
|
||||
BooleanBuilder loginIdPredicate = new BooleanBuilder()
|
||||
.or(qPortalUser.loginId.eq(searchUserId))
|
||||
.or(qPortalUser.emailAddr.eq(searchUserId));
|
||||
// 이메일 형식 값(예: user+1@unit-test.com)은 form-urlencoded 디코딩 과정에서
|
||||
// '+' 가 공백으로 치환되어 전달될 수 있다(프록시/컨테이너 단계라 앱에서 인코딩만으로 막기 어려움).
|
||||
// login_id/email 에는 공백이 올 수 없으므로, 공백 포함 검색어는 '+' 치환 값도 함께 매칭한다.
|
||||
if (searchUserId.indexOf(' ') >= 0) {
|
||||
String plusRestored = searchUserId.replace(' ', '+');
|
||||
loginIdPredicate.or(qPortalUser.loginId.eq(plusRestored))
|
||||
.or(qPortalUser.emailAddr.eq(plusRestored));
|
||||
}
|
||||
predicate.and(loginIdPredicate);
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(portalUserUISearch.getSearchUserName())) {
|
||||
@@ -47,16 +64,49 @@ public class PortalUserService extends AbstractEMSDataSerivce<PortalUser, String
|
||||
predicate.and(qPortalUser.portalOrg.id.eq(portalUserUISearch.getSearchOrgId()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(portalUserUISearch.getSearchMobileNumber())) {
|
||||
predicate.and(qPortalUser.mobileNumber.contains(portalUserUISearch.getSearchMobileNumber()));
|
||||
// mobile_number 도 암호화 저장 컬럼이라 부분문자열이 보존되지 않는다.
|
||||
// LIKE/contains 는 매칭되지 않으므로 정확일치(eq)로만 검색한다.
|
||||
predicate.and(qPortalUser.mobileNumber.eq(portalUserUISearch.getSearchMobileNumber().trim()));
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(portalUserUISearch.getSearchRoleCode())) {
|
||||
predicate.and(qPortalUser.roleCode.eq(PortalUserEnums.RoleCode.valueOf(portalUserUISearch.getSearchRoleCode())));
|
||||
}
|
||||
|
||||
// 휴대폰번호 중복(2명 이상 공유) 사용자만 조회
|
||||
if (portalUserUISearch.isOnlyDuplicateMobile()) {
|
||||
// 자바 리스트 .in() 은 컨버터가 IN 파라미터를 암호화하므로 레거시 평문 데이터와 매칭되지 않는다.
|
||||
// 대신 서브쿼리 .in() 으로 컬럼 대 컬럼(SQL 내부) 비교를 하여 평문/암호문 저장 방식과 무관하게 동작시킨다.
|
||||
QPortalUser dupUser = new QPortalUser("dupUser");
|
||||
predicate.and(qPortalUser.mobileNumber.in(
|
||||
JPAExpressions.select(dupUser.mobileNumber)
|
||||
.from(dupUser)
|
||||
.where(dupUser.mobileNumber.isNotNull()
|
||||
.and(dupUser.userStatus.ne(PortalUserEnums.UserStatus.REMOVED)))
|
||||
.groupBy(dupUser.mobileNumber)
|
||||
.having(dupUser.count().gt(1L))));
|
||||
}
|
||||
|
||||
return repository.findAll(predicate, pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴대폰번호가 중복되는 사용자 총 인원수(중복 그룹에 속한 사용자 수의 합)를 반환한다.
|
||||
*/
|
||||
public long countDuplicateMobileUsers() {
|
||||
QPortalUser qPortalUser = QPortalUser.portalUser;
|
||||
// Oracle 빈 문자열=NULL 이슈로 isNotNull 만 사용 (findDuplicateMobileNumbers 와 동일 기준)
|
||||
List<Long> duplicateGroupCounts = getJPAQueryFactory()
|
||||
.select(qPortalUser.count())
|
||||
.from(qPortalUser)
|
||||
.where(qPortalUser.mobileNumber.isNotNull()
|
||||
.and(qPortalUser.userStatus.ne(PortalUserEnums.UserStatus.REMOVED)))
|
||||
.groupBy(qPortalUser.mobileNumber)
|
||||
.having(qPortalUser.count().gt(1L))
|
||||
.fetch();
|
||||
return duplicateGroupCounts.stream().mapToLong(Long::longValue).sum();
|
||||
}
|
||||
|
||||
public boolean existsByLoginId(String loginId) {
|
||||
BooleanExpression predicate = QPortalUser.portalUser.loginId.eq(loginId);
|
||||
return repository.exists(predicate);
|
||||
@@ -77,6 +127,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()
|
||||
|
||||
+3
@@ -23,4 +23,7 @@ public class PortalUserUISearch {
|
||||
|
||||
private boolean includeRemoved;
|
||||
|
||||
/** true 인 경우 휴대폰번호가 중복(2명 이상 공유)되는 사용자만 조회 */
|
||||
private boolean onlyDuplicateMobile;
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -75,8 +76,8 @@ public class ApiStatusService {
|
||||
String event = entry.getKey();
|
||||
List<String> apiIds = entry.getValue();
|
||||
|
||||
String message = getSwingChatMessage(event, apiIds);
|
||||
ums.sendMessenger("api-monitor", MessageCode.API_STATUS_CHANGED, message);
|
||||
HashMap<String, Object> params = getSwingChatMessage(event, apiIds);
|
||||
ums.send("api-monitor", MessageCode.API_STATUS_CHANGED, params);
|
||||
|
||||
//제휴사 웹훅 발송
|
||||
ums.sendWebhook(event, apiIds);
|
||||
@@ -102,7 +103,7 @@ public class ApiStatusService {
|
||||
}
|
||||
|
||||
|
||||
private String getSwingChatMessage(String event, List<String> apiIds) {
|
||||
private HashMap<String, Object> getSwingChatMessage(String event, List<String> apiIds) {
|
||||
String message = "";
|
||||
switch (event) {
|
||||
case "CONTROL_START":
|
||||
@@ -125,9 +126,18 @@ public class ApiStatusService {
|
||||
break;
|
||||
default:
|
||||
message = "";
|
||||
}
|
||||
}
|
||||
|
||||
return message + "\n- " + String.join(",", apiIds);
|
||||
if (apiIds.size() > 1) {
|
||||
message += "\n" + apiIds.get(0) + "외 " + (apiIds.size()-1) + "종";
|
||||
} else {
|
||||
message += "\n- " + apiIds.get(0);
|
||||
}
|
||||
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
params.put("message", message);
|
||||
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.eactive.eai.rms.ext.djb.inflow;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@@ -9,7 +10,6 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.eai.rms.data.entity.man.role.Role;
|
||||
import com.eactive.eai.rms.data.entity.onl.djb.inflow.InflowTokenInsufficient;
|
||||
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
|
||||
|
||||
@@ -31,9 +31,10 @@ public class InflowTokenService {
|
||||
|
||||
for (InflowTokenInsufficient info : rows) {
|
||||
log.debug("유량제어 토큰 획득 실패: {}-{} 최근 {}분 동안 {}건", info.getEaisvcname(), info.getEaisvcdesc(), rangeMinute, info.getCnt());
|
||||
String message = "유량제어 토큰 획득 실패하였습니다 \n" + info.getEaisvcname();
|
||||
|
||||
ums.sendMessenger("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, message);
|
||||
String message = "유량제어 토큰 획득 실패가 발생하였습니다 \n" + info.getEaisvcname();
|
||||
HashMap<String,Object> params = new HashMap<String,Object>();
|
||||
params.put("message", message);
|
||||
ums.send("api-monitor", MessageCode.INFLOW_TOKEN_FAILED, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ import org.quartz.Job;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
@@ -45,9 +46,6 @@ public class PortalInquiryClosingJob implements Job {
|
||||
|
||||
private static final int DEFAULT_CLOSING_DAYS = 7;
|
||||
|
||||
@Autowired
|
||||
private PortalInquiryClosingService inquiryCommentClosingService;
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
|
||||
@@ -67,6 +65,15 @@ public class PortalInquiryClosingJob implements Job {
|
||||
|
||||
log.info("*** START PortalInquiryCommentClosingService run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
||||
|
||||
PortalInquiryClosingService service;
|
||||
try {
|
||||
ApplicationContext appContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext");
|
||||
service = appContext.getBean(PortalInquiryClosingService.class);
|
||||
} catch (SchedulerException e) {
|
||||
log.error("applicationContext 조회 실패", e);
|
||||
throw new JobExecutionException(e);
|
||||
}
|
||||
|
||||
int closingDays = DEFAULT_CLOSING_DAYS;
|
||||
String closingDayParam = context.getMergedJobDataMap().getString(KEY_INQUIRY_COMMENT_CLOSING_DAY);
|
||||
if (closingDayParam != null) {
|
||||
@@ -80,7 +87,7 @@ public class PortalInquiryClosingJob implements Job {
|
||||
DataSourceContextHolder.setDataSourceType(
|
||||
DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));
|
||||
try {
|
||||
int closedCount = inquiryCommentClosingService.closeExpiredInquiries(closingDays);
|
||||
int closedCount = service.closeExpiredInquiries(closingDays);
|
||||
log.info("PortalInquiryCommentClosingService 완료: {}건 CLOSED 처리", closedCount);
|
||||
} catch (Exception e) {
|
||||
log.error("PortalInquiryCommentClosingService execution failed", e);
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -29,14 +29,13 @@ public class UmsManager {
|
||||
private final WebhookService webhookService;
|
||||
|
||||
/**
|
||||
* role 역할을 가진 내부직원에게 메신저(Swing chat) 발송
|
||||
* roleId에 해당하는 역할을 가진 직원에게 UMS(sms,email,messenger) 발송
|
||||
* @param role
|
||||
* @param messageCode
|
||||
* @param message
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
public void sendMessenger(String roleId, MessageCode messageCode, String message) {
|
||||
|
||||
public void send(String roleId, MessageCode messageCode, HashMap<String, Object> params) {
|
||||
List<UserInfo> users = userInfoService.findAllByRoleId(roleId);
|
||||
if (users.isEmpty()) {
|
||||
log.info("sendMessenger: no users found for role '{}'", roleId);
|
||||
@@ -44,36 +43,36 @@ public class UmsManager {
|
||||
}
|
||||
|
||||
for (UserInfo user : users) {
|
||||
String cleanedPhone = user.getCphnno().replaceAll("[^0-9]", "").trim();
|
||||
MessageRecipient recipient = MessageRecipient.builder()
|
||||
.userId(user.getUserid())
|
||||
.username(user.getUsername())
|
||||
.email(user.getEmad())
|
||||
.phone(cleanedPhone)
|
||||
.messengerId(user.getUserid())
|
||||
.build();
|
||||
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
params.put("message", message);
|
||||
|
||||
messageHandlerService.publishEvent(messageCode, recipient, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 내부직원에게 메신저 발송
|
||||
* 직원 또는 회원에게 UMS(sms,email,messenger) 발송
|
||||
* @param user
|
||||
* @param messageCode
|
||||
* @param message
|
||||
* @param params
|
||||
*/
|
||||
public void sendMessenger(UserInfo user, MessageCode messageCode, String message) {
|
||||
public void send(UserInfo user, MessageCode messageCode, HashMap<String, Object> params) {
|
||||
String cleanedPhone = user.getCphnno().replaceAll("[^0-9]", "").trim();
|
||||
MessageRecipient recipient = MessageRecipient.builder()
|
||||
.userId(user.getUserid())
|
||||
.username(user.getUsername())
|
||||
.userId(user.getUserid())
|
||||
.username(user.getUsername())
|
||||
.email(user.getEmad())
|
||||
.phone(cleanedPhone)
|
||||
.messengerId(user.getUserid())
|
||||
.build();
|
||||
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
params.put("message", message);
|
||||
|
||||
messageHandlerService.publishEvent(messageCode, recipient, params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* event를 구독중인 제휴사에게 웹훅 발송 (비동기)
|
||||
* @param event
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
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 AdminVerificationMobilePhoneEventHandler implements MessageEventHandler {
|
||||
|
||||
@Override
|
||||
public MessageCode getKey() {
|
||||
return MessageCode.ADMIN_VERIFICATION_MOBILEPHONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowAdditionalRecipients() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "관리자포탈 휴대폰 인증번호 발송";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "<div>사용 가능 변수: </div><br/>"
|
||||
+ "<div> - userName 수신자 이름 </div><br/>"
|
||||
+ "<div> - authNumber 인증번호 </div><br/>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
|
||||
Map<String, String> eventParams = new HashMap<>();
|
||||
String authNumber = (String) params.get("authNumber");
|
||||
eventParams.put("authNumber", authNumber);
|
||||
eventParams.put("AUTH_CODE", authNumber);
|
||||
eventParams.put("userName", (String) params.get("userName"));
|
||||
return new MessageSendEvent(source, MessageCode.ADMIN_VERIFICATION_MOBILEPHONE, recipient, eventParams);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.apim.portal.common.util.PhoneNumberUtil;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import com.eactive.apim.portal.template.entity.MessageRequest;
|
||||
import com.eactive.eai.common.util.SystemUtil;
|
||||
@@ -35,7 +36,11 @@ public class UmsService {
|
||||
|
||||
private static final Gson gson = new Gson();
|
||||
|
||||
private static final String SYS_DVCD = "OPA";
|
||||
private static final String SYS_DVCD = "API"; //시스템코드
|
||||
private static final String BZWK_DVCD = "99"; //업무구분코드
|
||||
private static final String SUB_BZWK_CD = "9019"; //세부구분코드
|
||||
|
||||
|
||||
private final MonitoringPropertyService propertyService;
|
||||
|
||||
public UmsService(MonitoringPropertyService propertyService) {
|
||||
@@ -145,12 +150,14 @@ public class UmsService {
|
||||
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.setChnl_tycd("EAI"); // 채널유형코드
|
||||
stdHeadVO.setHmab_dvcd("1"); // 내외구분코드 1:내부 2:외부(타발)
|
||||
stdHeadVO.setIf_id(messageRequest.getEaiInterfaceId()); // 인터페이스ID
|
||||
stdHeadVO.setTx_id(messageRequest.getServiceId()); // 거래ID
|
||||
|
||||
if ("MESSENGER".equals(messageRequest.getMessageType()) ) {
|
||||
|
||||
stdHeadVO.setChnl_tycd("FEP"); // 채널유형코드
|
||||
|
||||
// 스윙챗 알림 공용
|
||||
SwingCommonVO swiComVO = new SwingCommonVO();
|
||||
@@ -168,7 +175,10 @@ public class UmsService {
|
||||
// 스윙챗 알림 데이터
|
||||
List<SwingReceiverDataVO> swiReDataVOList = new ArrayList<SwingReceiverDataVO>();
|
||||
SwingReceiverDataVO receiver = new SwingReceiverDataVO();
|
||||
receiver.setType("");
|
||||
receiver.setCompanyCode("CB");
|
||||
receiver.setCode(messageRequest.getMessengerId());
|
||||
receiver.setPrdNm("");
|
||||
swiReDataVOList.add(receiver);
|
||||
|
||||
swiDataVO.setReceiverInfo(swiReDataVOList);
|
||||
@@ -194,7 +204,7 @@ public class UmsService {
|
||||
dataVO.setSnd_dvcd("EAI");
|
||||
dataVO.setMsg_titl("");
|
||||
dataVO.setMsg_ctnt(messageRequest.getMessage());
|
||||
dataVO.setRecvr_no(messageRequest.getPhone());
|
||||
dataVO.setRecvr_no(PhoneNumberUtil.digitsOnly(messageRequest.getPhone()));
|
||||
dataVO.setSndn_no("15880079"); //발신번호
|
||||
dataVO.setSnd_empno(""); //요청자
|
||||
dataVO.setSnd_emp_dprm_cd(""); //요청부서코드
|
||||
@@ -209,8 +219,8 @@ public class UmsService {
|
||||
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_trnm_bzwk_dvcd(BZWK_DVCD); //업무구분코드
|
||||
dataVO.setSms_bzwk_dcls_dvcd(SUB_BZWK_CD); //업무세분코드
|
||||
dataVO.setSms_sys_dvcd("83"); //시스템코드
|
||||
dataVO.setTx_dt(jsonData[0]); //거래일자
|
||||
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
|
||||
@@ -248,8 +258,8 @@ public class UmsService {
|
||||
dataVO.setTmplt_mpng_rptt_info(""); // 탬플릿매핑반복정보
|
||||
dataVO.setSnd_empno(""); // 발신자ID(필수)
|
||||
dataVO.setSnd_emp_dprm_cd(""); // 발신자부서코드(필수)
|
||||
dataVO.setBzwk_cd(""); // 업무구분코드
|
||||
dataVO.setSub_bzwk_cd(""); // 업무세부코드
|
||||
dataVO.setBzwk_cd(BZWK_DVCD); // 업무구분코드
|
||||
dataVO.setSub_bzwk_cd(SUB_BZWK_CD); // 업무세부코드
|
||||
dataVO.setSys_dvcd(SYS_DVCD);
|
||||
dataVO.setTx_dt(jsonData[0]); //거래일자
|
||||
dataVO.setTx_tktm(jsonData[1].substring(0,6)); //거래시간
|
||||
@@ -277,58 +287,41 @@ public class UmsService {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,15 @@ import javax.servlet.http.HttpSession;
|
||||
public class SmsAuthController implements InterceptorSkipController {
|
||||
|
||||
private final MainController mainController;
|
||||
private final SmsAuthService smsAuthService;
|
||||
|
||||
public SmsAuthController(MainController mainController) {
|
||||
public SmsAuthController(MainController mainController, SmsAuthService smsAuthService) {
|
||||
this.mainController = mainController;
|
||||
this.smsAuthService = smsAuthService;
|
||||
}
|
||||
|
||||
private SmsAuthService getSmsAuthService() {
|
||||
return SmsAuthService.getInstance();
|
||||
return smsAuthService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +58,7 @@ public class SmsAuthController implements InterceptorSkipController {
|
||||
|
||||
// 인증번호 생성 및 발송
|
||||
String authCode = getSmsAuthService().generateAuthCode();
|
||||
boolean sent = getSmsAuthService().sendAuthCode(userInfo.getCphnno(), authCode);
|
||||
boolean sent = getSmsAuthService().sendAuthCode(userInfo, authCode);
|
||||
|
||||
if (sent) {
|
||||
// 세션에 인증번호 저장 (기존 UserInfo, LoginVo 유지)
|
||||
@@ -106,7 +108,7 @@ public class SmsAuthController implements InterceptorSkipController {
|
||||
|
||||
// 새 인증번호 생성 및 발송
|
||||
String authCode = getSmsAuthService().generateAuthCode();
|
||||
boolean sent = getSmsAuthService().sendAuthCode(userInfo.getCphnno(), authCode);
|
||||
boolean sent = getSmsAuthService().sendAuthCode(userInfo, authCode);
|
||||
|
||||
if (sent) {
|
||||
LoginVo loginVo = getSmsAuthService().getLoginVoFromSession(session);
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
package com.eactive.eai.rms.ext.kjb.smsauth;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||
import com.eactive.eai.rms.common.login.LoginVo;
|
||||
import com.eactive.eai.rms.ext.djb.ums.UmsManager;
|
||||
import com.eactive.ext.kjb.common.KjbProperty;
|
||||
import com.eactive.ext.kjb.common.KjbPropertyHolder;
|
||||
import com.eactive.ext.kjb.common.KjbUmsException;
|
||||
import com.eactive.ext.kjb.ums.KjbUmsService;
|
||||
import com.eactive.ext.kjb.ums.UmsBizWorkCode;
|
||||
import com.eactive.ext.kjb.ums.gson.GsonUtil;
|
||||
import com.eactive.ext.kjb.ums.gson.VerifyPhoneUmsTemplate;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SmsAuthService {
|
||||
private static volatile SmsAuthService instance;
|
||||
|
||||
// 세션 키
|
||||
public static final String SESSION_KEY_AUTH_CODE = "SMS_AUTH_CODE";
|
||||
@@ -39,20 +44,7 @@ public class SmsAuthService {
|
||||
|
||||
private final SecureRandom random = new SecureRandom();
|
||||
|
||||
private SmsAuthService() {
|
||||
// 싱글톤
|
||||
}
|
||||
|
||||
public static SmsAuthService getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (SmsAuthService.class) {
|
||||
if (instance == null) {
|
||||
instance = new SmsAuthService();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
private final UmsManager ums;
|
||||
|
||||
private static KjbProperty getProperty() {
|
||||
return KjbPropertyHolder.get();
|
||||
@@ -116,20 +108,15 @@ public class SmsAuthService {
|
||||
/**
|
||||
* SMS 인증번호 발송
|
||||
*/
|
||||
public boolean sendAuthCode(String phone, String authCode) {
|
||||
public boolean sendAuthCode(UserInfo userInfo, String authCode) {
|
||||
try {
|
||||
String cleanedPhone = phone.replaceAll("[^0-9]", "").trim();
|
||||
String guid = String.format("EAPIM_EMS-%s",
|
||||
DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").format(LocalDateTime.now()));
|
||||
|
||||
JsonObject content = GsonUtil.toJsonObject(VerifyPhoneUmsTemplate.builder().authNumber(authCode).build());
|
||||
|
||||
getUmsService().sendKakaoAlimTalk(guid, UmsBizWorkCode.VERIFY_PHONE, cleanedPhone, content);
|
||||
log.info("SMS 인증번호 발송 완료 - guid: {}, phone: {}****{}",
|
||||
guid, cleanedPhone.substring(0, 3), cleanedPhone.substring(cleanedPhone.length() - 4));
|
||||
log.debug("SMS 인증번호 발송 - authCode: {}", authCode);
|
||||
HashMap<String,Object> params = new HashMap<String, Object>();
|
||||
params.put("authNumber", authCode);
|
||||
params.put("userName", userInfo.getUsername());
|
||||
log.debug("\n\n\nDDDDDDDDD" + userInfo.toString());
|
||||
ums.send(userInfo, MessageCode.ADMIN_VERIFICATION_MOBILEPHONE, params);
|
||||
return true;
|
||||
} catch (KjbUmsException e) {
|
||||
} catch (Exception e) {
|
||||
log.error("SMS 인증번호 발송 실패", e);
|
||||
return false;
|
||||
}
|
||||
@@ -267,17 +254,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,11 +270,4 @@ public class SmsAuthService {
|
||||
public boolean isEnabled() {
|
||||
return getProperty().isSmsAuthEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 테스트용 인스턴스 리셋
|
||||
*/
|
||||
public static void resetForTest() {
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+35
-2
@@ -110,11 +110,13 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
||||
return;
|
||||
}
|
||||
MessageRecipient recipient = new MessageRecipient();
|
||||
recipient.setUsername(requester.getUserName());
|
||||
recipient.setPhone(requester.getMobileNumber());
|
||||
recipient.setUserId(requester.getEmailAddr());
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("apiKey", appRequest.getClientName());
|
||||
messageSendService.sendMessage(MessageCode.APP_REGISTER_APPROVED, recipient, params);
|
||||
// DB 템플릿 APP_APPROVE SMS 변수(%APP_NAME%)에 매핑
|
||||
params.put("APP_NAME", appRequest.getClientName());
|
||||
messageSendService.sendMessage(MessageCode.APP_APPROVE, recipient, params);
|
||||
}
|
||||
|
||||
private void syncTargetServer(AppRequestType action, CredentialUI credentialUI) throws ApprovalDeployException {
|
||||
@@ -225,5 +227,36 @@ public class PortalAppApprovalListener implements ApprovalListener {
|
||||
@Transactional
|
||||
public void rollback(Approval approval) {
|
||||
logger.info("Approval {} rollback", approval.getId());
|
||||
|
||||
// 앱 사용 거절 통지
|
||||
Optional<AppRequest> optRequest = appRequestRepository.findAppRequestByApproval(approval);
|
||||
if (!optRequest.isPresent()) {
|
||||
return;
|
||||
}
|
||||
AppRequest appRequest = optRequest.get();
|
||||
com.eactive.apim.portal.portaluser.entity.PortalUser requester = approval.getRequester();
|
||||
if (requester == null) {
|
||||
logger.warn("앱 거절 통지 생략 - 요청자 정보 없음. appRequestId: {}", appRequest.getId());
|
||||
return;
|
||||
}
|
||||
MessageRecipient recipient = new MessageRecipient();
|
||||
recipient.setUsername(requester.getUserName());
|
||||
recipient.setPhone(requester.getMobileNumber());
|
||||
recipient.setUserId(requester.getEmailAddr());
|
||||
Map<String, String> params = new HashMap<>();
|
||||
// DB 템플릿 APP_REJECTED SMS 변수(%APP_NAME%, %REASON%)에 매핑
|
||||
params.put("APP_NAME", appRequest.getClientName());
|
||||
params.put("REASON", extractRejectReason(approval));
|
||||
messageSendService.sendMessage(MessageCode.APP_REJECTED, recipient, params);
|
||||
}
|
||||
|
||||
private String extractRejectReason(Approval approval) {
|
||||
if (approval.getApprovers() == null) {
|
||||
return "";
|
||||
}
|
||||
return approval.getApprovers().stream()
|
||||
.map(com.eactive.apim.portal.approval.entity.Approver::getApprovalMessage)
|
||||
.filter(m -> m != null && !m.isEmpty())
|
||||
.findFirst().orElse("");
|
||||
}
|
||||
}
|
||||
|
||||
+37
-12
@@ -94,10 +94,15 @@ public class PortalUserApprovalListener implements ApprovalListener {
|
||||
|
||||
private void sendApprovalResult(PortalUser portalUser) {
|
||||
MessageRecipient recipient = new MessageRecipient();
|
||||
recipient.setUsername(portalUser.getUserName());
|
||||
recipient.setPhone(portalUser.getMobileNumber());
|
||||
recipient.setUserId(portalUser.getEmailAddr());
|
||||
Map<String, String> params = new HashMap<>();
|
||||
// messageSendService.sendMessage(MessageCode.USER_REGISTRATION_APPROVED, recipient, params);
|
||||
// DB 템플릿 MANAGER_WITH_ORG_REGISTER_APPROVED SMS 변수(%ORG_NAME%)에 매핑
|
||||
PortalOrg org = portalUser.getPortalOrg();
|
||||
if (org != null) {
|
||||
params.put("ORG_NAME", org.getOrgName());
|
||||
}
|
||||
messageSendService.sendMessage(MessageCode.MANAGER_WITH_ORG_REGISTER_APPROVED, recipient, params);
|
||||
}
|
||||
|
||||
@@ -155,6 +160,13 @@ public class PortalUserApprovalListener implements ApprovalListener {
|
||||
PortalUser portalUser = portalUserService.findByUserId(userId);
|
||||
PortalOrg portalOrg = portalUser.getPortalOrg();
|
||||
|
||||
// 거절 통지에 필요한 정보를 반려 처리(정보 마스킹) 이전에 캡처
|
||||
String noticeName = portalUser.getUserName();
|
||||
String noticePhone = portalUser.getMobileNumber();
|
||||
String noticeEmail = portalUser.getEmailAddr();
|
||||
String noticeOrgName = portalOrg != null ? portalOrg.getOrgName() : null;
|
||||
String rejectReason = extractRejectReason(approval);
|
||||
|
||||
// 개인→법인 전환 요청 반려: approvalStatus가 PENDING이면 전환 신청이었으므로 개인 사용자로 복원
|
||||
if (PortalUserEnums.ApprovalStatus.PENDING.equals(portalUser.getApprovalStatus())) {
|
||||
logger.info("Approval {} is a personal-to-corporate conversion rejection. Reverting user {} to personal user.", approval.getId(), userId);
|
||||
@@ -185,19 +197,32 @@ public class PortalUserApprovalListener implements ApprovalListener {
|
||||
portalOrg.setOrgName("삭제된법인");
|
||||
portalOrg.setApprovalStatus(ApprovalStatus.REJECTED);
|
||||
portalOrg.setOrgStatus(OrgStatus.REMOVED);
|
||||
portalOrg.setOrgCode(null);
|
||||
portalOrg.setCompRegFile(null);
|
||||
portalOrg.setCorpRegNo(null);
|
||||
portalOrg.setCompRegNo(null);
|
||||
portalOrg.setCeoName(null);
|
||||
portalOrg.setOrgAddr(null);
|
||||
portalOrg.setOrgPhoneNumber(null);
|
||||
portalOrg.setScPhoneNumber(null);
|
||||
portalOrg.setOrgSectors(null);
|
||||
portalOrg.setOrgIndustryType(null);
|
||||
portalOrg.setServiceName(null);
|
||||
portalOrg.clearSensitiveDataOnRemoval();
|
||||
|
||||
portalOrgService.save(portalOrg);
|
||||
}
|
||||
|
||||
// 법인관리자/법인 등록 거절 통지 (정보 마스킹 이전 캡처값 사용)
|
||||
MessageRecipient recipient = new MessageRecipient();
|
||||
recipient.setUsername(noticeName);
|
||||
recipient.setPhone(noticePhone);
|
||||
recipient.setUserId(noticeEmail);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
// DB 템플릿 MANAGER_WITH_ORG_REGISTER_REJECTED SMS 변수(%ORG_NAME%, %REASON%)에 매핑
|
||||
if (noticeOrgName != null) {
|
||||
params.put("ORG_NAME", noticeOrgName);
|
||||
}
|
||||
params.put("REASON", rejectReason);
|
||||
messageSendService.sendMessage(MessageCode.MANAGER_WITH_ORG_REGISTER_REJECTED, recipient, params);
|
||||
}
|
||||
|
||||
private String extractRejectReason(Approval approval) {
|
||||
if (approval.getApprovers() == null) {
|
||||
return "";
|
||||
}
|
||||
return approval.getApprovers().stream()
|
||||
.map(com.eactive.apim.portal.approval.entity.Approver::getApprovalMessage)
|
||||
.filter(m -> m != null && !m.isEmpty())
|
||||
.findFirst().orElse("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 → <span class="mask-email">**st@gmail.com</span>
|
||||
* 휴대폰(하이픈) 010-1234-5678 → <span class="mask-phone">010-12**-56**</span>
|
||||
* 6자리+ 숫자 123456 → <span class="mask-digits">123***</span> (앞 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;
|
||||
|
||||
/**
|
||||
* 매칭 우선순위(이메일 > 하이픈 휴대폰 > 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("&"); break;
|
||||
case '<': sb.append("<"); break;
|
||||
case '>': sb.append(">"); break;
|
||||
case '"': sb.append("""); break;
|
||||
case '\'': sb.append("'"); 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);
|
||||
}
|
||||
}
|
||||
+67
@@ -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();
|
||||
}
|
||||
}
|
||||
+106
@@ -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;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.eactive.eai.rms.onl.apim.portalorg;
|
||||
|
||||
import com.eactive.ext.kjb.common.KjbObpException;
|
||||
import com.eactive.ext.kjb.obp.KjbObpModule;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* OBP 파트너코드 조회 서비스
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ObpPartnerCodeService {
|
||||
|
||||
private KjbObpModule getObpModule() {
|
||||
return KjbObpModule.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사업자등록번호로 OBP 파트너코드 조회
|
||||
* @param compRegNo 사업자등록번호
|
||||
* @return 조회 결과 (success, partnerCode 또는 message)
|
||||
*/
|
||||
public Map<String, Object> queryPartnerCode(String compRegNo) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
String partnerCode = getObpModule().queryPartnerCode(compRegNo);
|
||||
|
||||
result.put("success", true);
|
||||
result.put("partnerCode", partnerCode);
|
||||
log.info("[OBP] 파트너코드 조회 성공 - compRegNo: {}, partnerCode: {}", compRegNo, partnerCode);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("[OBP] 파트너코드 조회 실패 - 잘못된 입력: {}", e.getMessage());
|
||||
result.put("success", false);
|
||||
result.put("message", e.getMessage());
|
||||
|
||||
} catch (KjbObpException e) {
|
||||
log.warn("[OBP] 파트너코드 조회 실패 - OBP 오류: {}", e.getMessage());
|
||||
result.put("success", false);
|
||||
result.put("message", "OBP 조회 실패: " + e.getMessage());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[OBP] 파트너코드 조회 중 오류 발생", e);
|
||||
result.put("success", false);
|
||||
result.put("message", "시스템 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@@ -36,7 +35,6 @@ public class PortalOrgManController extends BaseAnnotationController {
|
||||
|
||||
private final PortalOrgManService portalOrgManService;
|
||||
private final ComboService comboService;
|
||||
private final ObpPartnerCodeService obpPartnerCodeService;
|
||||
|
||||
@GetMapping(value = "/onl/apim/portalorg/portalOrgMan.view")
|
||||
public void view() {
|
||||
@@ -194,15 +192,4 @@ public class PortalOrgManController extends BaseAnnotationController {
|
||||
return ResponseEntity.ok(new GridResponse<>(page));
|
||||
}
|
||||
|
||||
/**
|
||||
* OBP 파트너코드 조회
|
||||
* @param compRegNo 사업자등록번호
|
||||
* @return partnerCode
|
||||
*/
|
||||
@PostMapping(value = "/onl/apim/portalorg/portalOrgMan.json", params = "cmd=QUERY_OBP_PARTNER_CODE")
|
||||
public ResponseEntity<Map<String, Object>> queryObpPartnerCode(@RequestParam("compRegNo") String compRegNo) {
|
||||
Map<String, Object> result = obpPartnerCodeService.queryPartnerCode(compRegNo);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ public class PortalOrgManService extends BaseService {
|
||||
PortalOrg portalOrg = portalOrgService.getById(id);
|
||||
portalOrg.setOrgName("삭제된법인");
|
||||
portalOrg.setOrgStatus(OrgStatus.REMOVED);
|
||||
portalOrg.clearSensitiveDataOnRemoval();
|
||||
portalOrgService.save(portalOrg);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,5 +70,7 @@ public class PortalOrgUI {
|
||||
private String serviceName;
|
||||
|
||||
private String scPhoneNumber;
|
||||
|
||||
private String reverseProxyPath;
|
||||
|
||||
}
|
||||
|
||||
+12
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+38
-4
@@ -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);
|
||||
}
|
||||
|
||||
// 첨부파일 처리
|
||||
|
||||
@@ -14,6 +14,7 @@ public interface PortalPropertyMapper extends GenericMapper<PropertyUI, PortalPr
|
||||
@Mapping(source = "prptyGroupName", target = "id.propertyGroupName")
|
||||
@Mapping(source = "prptyName", target = "id.propertyName")
|
||||
@Mapping(source = "prpty2Val", target = "propertyValue")
|
||||
@Mapping(source = "prptyDesc", target = "propertyDesc")
|
||||
@Override
|
||||
PortalProperty toEntity(PropertyUI vo);
|
||||
|
||||
@@ -21,6 +22,7 @@ public interface PortalPropertyMapper extends GenericMapper<PropertyUI, PortalPr
|
||||
@Mapping(source ="id.propertyGroupName", target = "prptyGroupName")
|
||||
@Mapping(source ="id.propertyName", target = "prptyName")
|
||||
@Mapping(source ="propertyValue", target = "prpty2Val")
|
||||
@Mapping(source ="propertyDesc", target = "prptyDesc")
|
||||
@InheritInverseConfiguration
|
||||
@Override
|
||||
PropertyUI toVo(PortalProperty entity);
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ public class PortalTermsManController extends BaseAnnotationController {
|
||||
.map(type -> {
|
||||
ComboVo vo = new ComboVo();
|
||||
vo.setCode(type.name());
|
||||
vo.setCodename(type.getDescription());
|
||||
vo.setCodename(PortalTermsManService.displayLabel(type));
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -20,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -48,8 +50,35 @@ public class PortalTermsManService extends BaseService {
|
||||
this.fileService = fileService;
|
||||
}
|
||||
|
||||
/** 관리 화면에서 '(사용안함)'으로 표시할(=신규 등록에 더 이상 쓰지 않는) 약관 유형 */
|
||||
private static final Set<AgreementType> DEPRECATED_AGREEMENT_TYPES = EnumSet.of(
|
||||
AgreementType.PRIVACY_POLICY,
|
||||
AgreementType.PRIVACY_COLLECT_IND,
|
||||
AgreementType.PRIVACY_COLLECT_ORG);
|
||||
|
||||
/**
|
||||
* 관리 화면(콤보/목록)에 표시할 약관 유형 라벨.
|
||||
* 개인정보처리방침 및 개인용/법인용 개인정보수집동의서는 신규 통합 유형 {@link AgreementType#PRIVACY_COLLECT} 등으로
|
||||
* 대체될 예정이므로 '(사용안함)'을 덧붙여 안내한다.
|
||||
* enum 자체의 description은 포털 사용자 화면(AgreementsController#agreementTitle)에서도 쓰이므로 변경하지 않는다.
|
||||
*/
|
||||
public static String displayLabel(AgreementType type) {
|
||||
String label = type.getDescription();
|
||||
if (DEPRECATED_AGREEMENT_TYPES.contains(type)) {
|
||||
label += "(사용안함)";
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -58,7 +87,7 @@ public class PortalTermsManService extends BaseService {
|
||||
PortalTermsUI portalTermsUI = portalTermsUIMapper.toVo(agreements);
|
||||
portalTermsUI.setCreatedByName(findName(agreements.getCreatedBy()));
|
||||
portalTermsUI.setLastModifiedByName(findName(agreements.getLastModifiedBy()));
|
||||
portalTermsUI.setName(agreements.getAgreementsType().getDescription());
|
||||
portalTermsUI.setName(displayLabel(agreements.getAgreementsType()));
|
||||
return portalTermsUI;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,6 +102,12 @@ public class PortalUserManController extends BaseAnnotationController {
|
||||
resultMap.put("userStatusRows", userStatusList);
|
||||
resultMap.put("roleCodeRows", roleCodeList);
|
||||
resultMap.put("isProdServer", SystemUtil.isProdServer());
|
||||
|
||||
// 휴대폰번호 중복 허용안함 여부 및 중복 사용자 수 (배너 노출용)
|
||||
Map<String, Object> mobileDuplicateInfo = portalUserManService.getMobileDuplicateInfo();
|
||||
resultMap.put("mobileDuplicateCheckEnabled", mobileDuplicateInfo.get("enabled"));
|
||||
resultMap.put("mobileDuplicateUserCount", mobileDuplicateInfo.get("count"));
|
||||
|
||||
return ResponseEntity.ok(resultMap);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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.portalorg.entity.PortalOrg;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserPrivacyAgreement;
|
||||
@@ -31,7 +32,9 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
@@ -39,6 +42,11 @@ import java.util.List;
|
||||
@Slf4j
|
||||
public class PortalUserManService extends BaseService {
|
||||
|
||||
/** PortalProperty 그룹명 */
|
||||
private static final String PORTAL_PROPERTY_GROUP = "Portal";
|
||||
/** 사용자 별 휴대폰 번호 중복 허용 프로퍼티 키 (true:허용, false:허용안함, 기본 false) */
|
||||
private static final String PROP_MOBILE_DUPLICATE_ALLOW = "user.mobile.duplicate.allow";
|
||||
|
||||
private final PortalUserService portalUserService;
|
||||
private final PortalUserUIMapper portalUserUIMapper;
|
||||
private final LocaleMessage localeMessage;
|
||||
@@ -50,6 +58,7 @@ public class PortalUserManService extends BaseService {
|
||||
private final PortalUserTermsService portalUserTermsService;
|
||||
private final ApprovalService approvalService;
|
||||
private final PortalInquiryService portalInquiryService;
|
||||
private final PortalPropertyService portalPropertyService;
|
||||
|
||||
|
||||
public Page<PortalUserUI> selectList(Pageable pageable, PortalUserUISearch portalUserUISearch) {
|
||||
@@ -57,6 +66,19 @@ public class PortalUserManService extends BaseService {
|
||||
return portalUser.map(entity -> {
|
||||
PortalUserUI ui = convertToUIWithMaskOption(entity, false);
|
||||
|
||||
// 목록의 개인정보(사용자명/이메일아이디/휴대폰번호)는 항상 마스킹하여 노출 (상세 조회에서만 마스킹 해제)
|
||||
if (ui != null) {
|
||||
if (StringUtils.isNotBlank(ui.getUserName())) {
|
||||
ui.setUserName(MaskingUtils.maskName(ui.getUserName()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(ui.getLoginId())) {
|
||||
ui.setLoginId(MaskingUtils.maskEmailId(ui.getLoginId()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(ui.getMobileNumber())) {
|
||||
ui.setMobileNumber(MaskingUtils.maskPhoneNumber(ui.getMobileNumber()));
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.getPortalOrg() != null) {
|
||||
setPortalOrgUI(entity, ui);
|
||||
}
|
||||
@@ -64,6 +86,30 @@ public class PortalUserManService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 별 휴대폰 번호 중복 허용 프로퍼티(Portal/user.mobile.duplicate.allow)를 확인한다.
|
||||
* 프로퍼티가 없으면 기본값 'false'(허용안함)으로 생성하며, '허용안함'일 때 중복 휴대폰번호 사용자 수를 함께 반환한다.
|
||||
*
|
||||
* @return enabled(boolean): 중복 조회 기능 활성 여부, count(long): 중복 휴대폰번호 사용자 수
|
||||
*/
|
||||
public Map<String, Object> getMobileDuplicateInfo() {
|
||||
String allowValue = portalPropertyService.getOrCreateProperty(
|
||||
PORTAL_PROPERTY_GROUP,
|
||||
PROP_MOBILE_DUPLICATE_ALLOW,
|
||||
"false",
|
||||
"사용자 별 휴대폰 번호 중복 허용 여부 (true:허용, false:허용안함). 허용안함일 때 사용자 관리 목록에 중복 휴대폰번호 사용자 조회 기능을 노출");
|
||||
|
||||
// 레거시 Y/N 값도 허용으로 함께 인식 (true/Y → 허용, 그 외 → 허용안함)
|
||||
boolean allowed = "true".equalsIgnoreCase(allowValue) || "Y".equalsIgnoreCase(allowValue);
|
||||
boolean checkEnabled = !allowed;
|
||||
long duplicateUserCount = checkEnabled ? portalUserService.countDuplicateMobileUsers() : 0L;
|
||||
|
||||
Map<String, Object> info = new HashMap<>();
|
||||
info.put("enabled", checkEnabled);
|
||||
info.put("count", duplicateUserCount);
|
||||
return info;
|
||||
}
|
||||
|
||||
public PortalUserUI selectDetail(String id, boolean isMasked) {
|
||||
PortalUser portalUser = portalUserService.getById(id);
|
||||
PortalUserUI portalUserUI = convertToUIWithMaskOption(portalUser, isMasked);
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package com.eactive.eai.rms.onl.common.service.ums;
|
||||
|
||||
import com.eactive.apim.portal.common.util.PhoneNumberUtil;
|
||||
import com.eactive.apim.portal.template.entity.MessageRequest;
|
||||
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
|
||||
import com.eactive.eai.transformer.util.ByteUtil;
|
||||
@@ -126,7 +127,7 @@ public class StandardMessageFactory {
|
||||
template.get("KKO_CTT").setValue(request.getMessage());
|
||||
template.get("KKO_ATTACHMENT").setValue("{\"button\":[{\"name\":\"채널 추가\",\"type\":\"AC\"}]}");
|
||||
template.get("KKO_SMS_SND_MSG").setValue(request.getMessage());
|
||||
template.get("RCPN_MVBL_TEL_NO").setValue(request.getPhone());
|
||||
template.get("RCPN_MVBL_TEL_NO").setValue(PhoneNumberUtil.digitsOnly(request.getPhone()));
|
||||
|
||||
String kkoSenderKey = monitoringPropertyService.getPrpty2ValById("Monitoring", "ums.kko.sender.key");
|
||||
template.put("KKO_SENDER_KEY", new FieldDefinition(40, kkoSenderKey)); // 카카오발신프로필키
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,4 +15,7 @@ public class PropertyUI {
|
||||
|
||||
@JsonProperty("PRPTY2VAL")
|
||||
private String prpty2Val;
|
||||
|
||||
@JsonProperty("PRPTYDESC")
|
||||
private String prptyDesc;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
|
||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
|
||||
import com.eactive.eai.data.entity.onl.unifbwk.UnifBwkTp;
|
||||
@@ -33,6 +34,7 @@ public class UnifbwkManService extends BaseService {
|
||||
private final BizManService bizManService;
|
||||
private final UserInfoService userInfoService;
|
||||
private final UserDao userDao;
|
||||
private final PersonalDataEncryptConverter encryptConverter = new PersonalDataEncryptConverter();
|
||||
|
||||
@Autowired
|
||||
public UnifbwkManService(UnifBwkTpService unifBwkTpService,
|
||||
@@ -54,7 +56,8 @@ public class UnifbwkManService extends BaseService {
|
||||
|
||||
private UnifbwkUI getUserOrgInfo(UnifBwkTp unifBwkTp) {
|
||||
UnifbwkUI unifbwkUI = unifbwkUIMapper.toVo(unifBwkTp);
|
||||
String rspblpsncntpctnt = unifBwkTp.getRspblpsncntpctnt();
|
||||
String rspblpsncntpctnt = encryptConverter.convertToEntityAttribute(unifBwkTp.getRspblpsncntpctnt());
|
||||
unifbwkUI.setRsPblpsncntpcTnt(rspblpsncntpctnt);
|
||||
Optional<UserInfo> userInfo = Optional.empty();
|
||||
|
||||
if (StringUtils.isNotBlank(rspblpsncntpctnt)) {
|
||||
@@ -75,10 +78,13 @@ public class UnifbwkManService extends BaseService {
|
||||
UnifBwkTp unifBwkTp = unifBwkTpService.getById(eaiBzwkDstcd);
|
||||
UnifbwkUI unifbwkUI = unifbwkUIMapper.toVo(unifBwkTp);
|
||||
|
||||
String rspblpsncntpctnt = encryptConverter.convertToEntityAttribute(unifBwkTp.getRspblpsncntpctnt());
|
||||
unifbwkUI.setRsPblpsncntpcTnt(rspblpsncntpctnt);
|
||||
|
||||
Optional<UserInfo> optionalUserInfo = Optional.empty();
|
||||
|
||||
if (StringUtils.isNotBlank(unifBwkTp.getRspblpsncntpctnt())) {
|
||||
optionalUserInfo = userInfoService.findByOfctelno(unifBwkTp.getRspblpsncntpctnt());
|
||||
if (StringUtils.isNotBlank(rspblpsncntpctnt)) {
|
||||
optionalUserInfo = userInfoService.findByOfctelno(rspblpsncntpctnt);
|
||||
}
|
||||
if (optionalUserInfo.isPresent()) {
|
||||
UserInfo userInfo = optionalUserInfo.get();
|
||||
@@ -90,8 +96,9 @@ public class UnifbwkManService extends BaseService {
|
||||
|
||||
public void insert(UnifbwkUI unifbwkUI) {
|
||||
UnifBwkTp unifBwkTp = unifbwkUIMapper.toEntity(unifbwkUI);
|
||||
unifBwkTp.setRspblpsncntpctnt(encryptConverter.convertToDatabaseColumn(unifBwkTp.getRspblpsncntpctnt()));
|
||||
unifBwkTpService.save(unifBwkTp);
|
||||
|
||||
|
||||
bizManService.transactionAdminBiz(unifbwkUI.getEaiBzwkDstcd());
|
||||
}
|
||||
|
||||
@@ -113,6 +120,7 @@ public class UnifbwkManService extends BaseService {
|
||||
unifbwkUI.setTrackAsisKey2Name(trackAsisKey2Name);
|
||||
|
||||
unifbwkUIMapper.updateToEntity(unifbwkUI, unifBwkTp);
|
||||
unifBwkTp.setRspblpsncntpctnt(encryptConverter.convertToDatabaseColumn(rspblPsnCntpCtnt));
|
||||
unifBwkTpService.save(unifBwkTp);
|
||||
}
|
||||
|
||||
@@ -132,7 +140,7 @@ public class UnifbwkManService extends BaseService {
|
||||
map.put("TRACKASISKEY1NAME", e.getTrackasiskey1name());
|
||||
map.put("TRACKASISKEY2NAME", e.getTrackasiskey2name());
|
||||
map.put("RSEMPNAME", e.getRsempname());
|
||||
map.put("RSPBLPSNCNTPCTNT", e.getRspblpsncntpctnt());
|
||||
map.put("RSPBLPSNCNTPCTNT", encryptConverter.convertToEntityAttribute(e.getRspblpsncntpctnt()));
|
||||
map.put("GROUPCOCD", e.getGroupcocd());
|
||||
if (e.getLastModDtm() != null) {
|
||||
map.put("LAST_MOD_DTM",
|
||||
@@ -174,6 +182,7 @@ public class UnifbwkManService extends BaseService {
|
||||
}
|
||||
|
||||
UnifBwkTp unifBwkTp = unifbwkUIMapper.toEntity(unifbwkUI);
|
||||
unifBwkTp.setRspblpsncntpctnt(encryptConverter.convertToDatabaseColumn(unifBwkTp.getRspblpsncntpctnt()));
|
||||
unifBwkTpService.save(unifBwkTp);
|
||||
}
|
||||
|
||||
@@ -206,6 +215,7 @@ public class UnifbwkManService extends BaseService {
|
||||
}
|
||||
|
||||
unifbwkUIMapper.updateToEntity(unifbwkUI, optionalUnifBwkTp.get());
|
||||
optionalUnifBwkTp.get().setRspblpsncntpctnt(encryptConverter.convertToDatabaseColumn(unifbwkUI.getRsPblpsncntpcTnt()));
|
||||
unifBwkTpService.save(optionalUnifBwkTp.get());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-5
@@ -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);
|
||||
|
||||
+1
-5
@@ -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);
|
||||
|
||||
@@ -74,6 +74,7 @@ public class LayoutDao extends SqlMapClientTemplateDao {
|
||||
entity.setEaibzwkdstcd((String) paramMap.get("eaiBzwkDstcd"));
|
||||
entity.setUapplname((String) paramMap.get("uapplName"));
|
||||
entity.setSysintfacname((String) paramMap.get("sysIntfacName"));
|
||||
entity.setUseyn((String) paramMap.get("useYn"));
|
||||
|
||||
layoutService.save(entity);
|
||||
}
|
||||
|
||||
+69
-4
@@ -683,7 +683,9 @@ public class LayoutSyncController extends OnlBaseAnnotationController implements
|
||||
param.put("recvData", "");
|
||||
}
|
||||
|
||||
syncLayout(param, recvTime, json);
|
||||
//syncLayout(param, recvTime, json);
|
||||
syncLayoutDjb(param, recvTime, json);
|
||||
|
||||
// results.getChild("Result").setText("S".equals(param.get("prcssRslt"))?"True":"False");
|
||||
// results.getChild("ResultMessage").setText("S".equals(param.get("prcssRslt"))?"성공":param.get("prcssRsltCmnt"));
|
||||
|
||||
@@ -900,7 +902,11 @@ public class LayoutSyncController extends OnlBaseAnnotationController implements
|
||||
private void syncLayout(HashMap<String, String> param, JSONObject body, String command, JSONObject layout)
|
||||
throws Exception, BizException, ParseException {
|
||||
|
||||
String layoutName = (String) layout.get("id");
|
||||
String id = (String) layout.get("id");
|
||||
String layoutName = (String) layout.get("layoutName");
|
||||
if (StringUtils.isEmpty(layoutName)) {
|
||||
layoutName = id;
|
||||
}
|
||||
String useSystem = (String) layout.get("usesystem");
|
||||
|
||||
param.put("logPrcssSeqno", UUIDGenerator.getUUID());
|
||||
@@ -935,7 +941,7 @@ public class LayoutSyncController extends OnlBaseAnnotationController implements
|
||||
break;
|
||||
}
|
||||
|
||||
if(!layoutName.matches(fattern)) {
|
||||
if(!id.matches(fattern)) {
|
||||
throw new BizException("잘못된 전문레이아웃명 입니다.\n명명규칙을 확인하세요.\n"+fattern);
|
||||
}
|
||||
|
||||
@@ -955,7 +961,8 @@ public class LayoutSyncController extends OnlBaseAnnotationController implements
|
||||
// Layout Format (TR07)
|
||||
vo.put("loutName",layoutName);
|
||||
vo.put("loutPtrnName",msgType); // ASCII, EBCDIC, XML
|
||||
vo.put("loutDesc",layout.get("desc"));
|
||||
vo.put("loutDesc",layout.get("desc"));
|
||||
vo.put("useYn", "1");
|
||||
|
||||
String date = (String) layout.get("date");
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
@@ -1183,5 +1190,63 @@ public class LayoutSyncController extends OnlBaseAnnotationController implements
|
||||
}
|
||||
return null ;
|
||||
}
|
||||
|
||||
|
||||
private void syncLayoutDjb(HashMap<String, String> param, String recvTime, JSONObject json)
|
||||
throws Exception, BizException, ParseException {
|
||||
JSONObject header = (JSONObject) ((JSONObject) json.get("Message")).get("Header");
|
||||
JSONObject body = (JSONObject) ((JSONObject) json.get("Message")).get("Body");
|
||||
|
||||
String serviceName = (String) header.get("ServiceName");
|
||||
String command = (String) header.get("Command");
|
||||
|
||||
param.put("serviceName", serviceName);
|
||||
param.put("command", command);
|
||||
param.put("recvAmndHMS", recvTime);
|
||||
|
||||
|
||||
Object layoutObject = body.get("Layout");
|
||||
|
||||
JSONArray layoutArray = null;
|
||||
if (layoutObject instanceof JSONArray) {
|
||||
|
||||
String interfaceId = (String) body.get("interfaceCode");
|
||||
if (interfaceId.startsWith("ES")) {
|
||||
interfaceId = interfaceId.replaceFirst("ES", "AS");
|
||||
}
|
||||
|
||||
layoutArray = (JSONArray) layoutObject;
|
||||
|
||||
for (int i = 0; i < layoutArray.size(); i++) {
|
||||
JSONObject layout = (JSONObject)layoutArray.get(i);
|
||||
|
||||
String usesystem = (String)layout.get("usesystem");
|
||||
|
||||
if ("EAI".equals(usesystem)) {
|
||||
String id = (String)layout.get("id");
|
||||
String inOutType = "OPA".equals(id.substring(5,8)) ? "S1" : "S2";
|
||||
String group = (String)layout.get("group");
|
||||
String ioType = (String)layout.get("ioType");
|
||||
|
||||
if ("I".equals(ioType)) {
|
||||
layout.put("layoutName", group + "_" + interfaceId + inOutType + "_SRCS");
|
||||
syncLayout(param, body, command, layout);
|
||||
|
||||
layout.put("layoutName", group + "_" + interfaceId + inOutType + "_TGTS");
|
||||
syncLayout(param, body, command, layout);
|
||||
} else {
|
||||
layout.put("layoutName", group + "_" + interfaceId + inOutType + "_SRCR");
|
||||
syncLayout(param, body, command, layout);
|
||||
|
||||
layout.put("layoutName", group + "_" + interfaceId + inOutType + "_TGTR");
|
||||
syncLayout(param, body, command, layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new BizException("단독 레이아웃 연동은 지원하지 않습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class ApiSpecController extends OnlBaseAnnotationController {
|
||||
return "/onl/transaction/apim/apiSpecManPopup";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/onl/transaction/apim/apiSpecManPopup.view", params = "cmd=POPUP_LIST")
|
||||
@GetMapping(value = "/onl/transaction/apim/apiSpecMan.view", params = "cmd=POPUP_LIST")
|
||||
public String detailOrgPopupView() {
|
||||
return "/onl/transaction/apim/apiSpecManDisplayOrgPopup";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
|
||||
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
|
||||
import com.eactive.eai.rms.data.entity.onl.eaimsg.EAIMessageService;
|
||||
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutUI;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI;
|
||||
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
|
||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||
import com.eactive.eai.rms.data.entity.onl.apim.apigroup.ApiGroupService;
|
||||
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
/**
|
||||
* 신규 OpenAPI Spec 6단계 편집 마법사 컨트롤러.
|
||||
*
|
||||
* apiInterfaceMan 상세의 "OpenAPI Spec" 버튼이 새 탭(_blank)으로 여는 페이지 및 마법사가 호출하는
|
||||
* cmd API 를 제공한다. 저장은 기존 컬럼 + testbedSpec(OpenAPI JSON)에 매핑(스키마 무변경, 경량).
|
||||
*
|
||||
* (MVC 컨트롤러는 springapp-servlet.xml 의 com.eactive.eai.rms 스캔 범위에서만 등록되므로 본 컨트롤러는
|
||||
* 해당 패키지에 둔다. 기존 apiSpecMan.view(v1)와 URL 을 분리해 공존한다.)
|
||||
*/
|
||||
@Controller
|
||||
public class DjbApiSpecController extends OnlBaseAnnotationController {
|
||||
|
||||
@Autowired
|
||||
private ApiSpecManService apiSpecManService;
|
||||
|
||||
@Autowired
|
||||
private ApiInterfaceService apiInterfaceService;
|
||||
|
||||
@Autowired
|
||||
private EAIMessageService eaiMessageService;
|
||||
|
||||
@Autowired
|
||||
private DjbApiSpecSwaggerEnricher swaggerEnricher;
|
||||
|
||||
@Autowired
|
||||
private DjbApiSpecLayoutMergeService layoutMergeService;
|
||||
|
||||
@Autowired
|
||||
private PortalPropertyService portalPropertyService;
|
||||
|
||||
@Autowired
|
||||
private ApiGroupService apiGroupService;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// GW base URL 단일 기준 = 포탈 DjbTestbedGatewayProperty 의 djb.gateway.base-url (PTL_PROPERTY 그룹 'Portal').
|
||||
// 별도 swagger.gw.address 프로퍼티는 두지 않는다.
|
||||
private static final String GW_BASE_URL_KEY = "djb.gateway.base-url";
|
||||
private static final String GW_BASE_URL_DEFAULT = "PortalMock";
|
||||
// "PortalMock" 은 포탈이 요청 origin 으로 치환하는 sentinel 이라 admin 미리보기용 서버 주소로는 부적합 → 대체값 사용.
|
||||
private static final String GW_PREVIEW_FALLBACK = "http://127.0.0.1:39310";
|
||||
|
||||
// ── 진입 ────────────────────────────────────────────────
|
||||
@GetMapping(value = "/onl/transaction/apim/djbApiSpecMan.view", params = "cmd=DETAIL")
|
||||
public String viewDetail(Model model) {
|
||||
// GW base URL (djb.gateway.base-url, 그룹 'Portal') — 없으면 기본값으로 생성. DjbTestbedGatewayProperty 와 동일 키.
|
||||
String gw = portalPropertyService.getOrCreateProperty(
|
||||
"Portal", GW_BASE_URL_KEY, GW_BASE_URL_DEFAULT, "GW base URL (테스트베드 공통)");
|
||||
if (StringUtils.isBlank(gw) || GW_BASE_URL_DEFAULT.equalsIgnoreCase(gw.trim())) {
|
||||
gw = GW_PREVIEW_FALLBACK; // PortalMock → admin 미리보기용 구체 주소로 대체
|
||||
}
|
||||
model.addAttribute("gwAddress", gw.trim());
|
||||
return "/onl/transaction/apim/djbApiSpecManPopup";
|
||||
}
|
||||
|
||||
// API 그룹 선택 팝업(iframe 모달 대상). 그룹 목록 그리드는 apiGroupMan.json?cmd=LIST 재사용.
|
||||
@GetMapping(value = "/onl/transaction/apim/djbApiSpecMan.view", params = "cmd=GROUP_POPUP")
|
||||
public String viewGroupPopup() {
|
||||
return "/onl/transaction/apim/djbApiGroupPopup";
|
||||
}
|
||||
|
||||
// ── 조회: 저장 spec 우선 + authtype securityScheme 주입 ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=DETAIL_SPEC")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> detailSpec(String eaiSvcName, String regen) throws Exception {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (StringUtils.isBlank(eaiSvcName)) {
|
||||
result.put("status", "fail");
|
||||
result.put("errorMsg", "eaiSvcName 이 없습니다.");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
|
||||
// regen=true 이면 저장본 무시하고 레이아웃 기반으로 강제 재생성 (자동 생성/초기화)
|
||||
boolean force = "true".equalsIgnoreCase(regen);
|
||||
String spec;
|
||||
String source;
|
||||
ApiSpecInfoUI saved = force ? null : apiSpecManService.selectDetail(eaiSvcName);
|
||||
if (!force && saved != null && StringUtils.isNotBlank(saved.getTestbedSpec())) {
|
||||
spec = saved.getTestbedSpec();
|
||||
source = "saved";
|
||||
} else {
|
||||
spec = generateSpec(eaiSvcName);
|
||||
source = force ? "regenerated" : "generated";
|
||||
}
|
||||
|
||||
String authtype = eaiMessageService.findById(eaiSvcName)
|
||||
.map(EAIMessageEntity::getAuthtype)
|
||||
.orElse(null);
|
||||
String enriched = swaggerEnricher.enrich(spec, authtype);
|
||||
|
||||
result.put("testbedSpec", enriched);
|
||||
result.put("source", source);
|
||||
result.put("authType", authtype == null ? "" : authtype);
|
||||
// API 그룹 소속(조인테이블 ApiGroupApi) — 저장 여부와 무관하게 현재 소속 반환
|
||||
try {
|
||||
ArrayNode groups = objectMapper.createArrayNode();
|
||||
for (ApiGroup g : apiGroupService.findByApiId(eaiSvcName)) {
|
||||
ObjectNode gn = objectMapper.createObjectNode();
|
||||
gn.put("id", g.getId());
|
||||
gn.put("groupName", g.getGroupName());
|
||||
groups.add(gn);
|
||||
}
|
||||
result.put("apiGroups", objectMapper.writeValueAsString(groups));
|
||||
} catch (Exception e) {
|
||||
logger.warn("API 그룹 조회 실패: " + eaiSvcName, e);
|
||||
}
|
||||
// 저장본이면 폼 복원용 기본정보/공개설정도 함께 반환
|
||||
if (saved != null) {
|
||||
putIfNotNull(result, "apiName", saved.getApiName());
|
||||
putIfNotNull(result, "apiSimpleDescription", saved.getApiSimpleDescription());
|
||||
putIfNotNull(result, "apiUrl", saved.getApiUrl());
|
||||
putIfNotNull(result, "apiMethod", saved.getApiMethod());
|
||||
putIfNotNull(result, "apiContentType", saved.getApiContentType());
|
||||
putIfNotNull(result, "description", saved.getDescription());
|
||||
putIfNotNull(result, "apiRequestSpec", saved.getApiRequestSpec());
|
||||
putIfNotNull(result, "apiResponseSpec", saved.getApiResponseSpec());
|
||||
putIfNotNull(result, "sampleRequest", saved.getSampleRequest());
|
||||
putIfNotNull(result, "sampleResponse", saved.getSampleResponse());
|
||||
putIfNotNull(result, "responseType", saved.getResponseType());
|
||||
putIfNotNull(result, "mockUrl", saved.getMockUrl());
|
||||
putIfNotNull(result, "displayYn", saved.getDisplayYn());
|
||||
putIfNotNull(result, "displayRoleCode", saved.getDisplayRoleCode());
|
||||
putIfNotNull(result, "displayOrg", saved.getDisplayOrg());
|
||||
}
|
||||
// 예제/설명자료(요청·응답 메시지 스펙 HTML)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치.
|
||||
// 저장본이면 위 saved 블록에서 저장값을 이미 반환.
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 저장: 마법사 폼 → ApiSpecInfoUI → apiSpecManService.save (기존 재사용) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=SAVE")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> save(ApiSpecInfoUI apiSpecInfoUI, String apiGroupIds) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (apiSpecInfoUI == null || StringUtils.isBlank(apiSpecInfoUI.getApiId())) {
|
||||
result.put("status", "fail");
|
||||
result.put("errorMsg", "API ID 가 없습니다.");
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
// CrossScriptingFilter(RequestWrapper.cleanXSS)가 요청 파라미터의 특수문자를 엔티티로 치환
|
||||
// ('('→( ')'→) '"'→" 등)하므로, 저장 전 모든 자유텍스트/JSON 필드를 언이스케이프해 원복한다.
|
||||
// (JSON 필드는 특히 '"'→" 로 깨지므로 필수)
|
||||
apiSpecInfoUI.setApiName(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiName()));
|
||||
apiSpecInfoUI.setApiSimpleDescription(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiSimpleDescription()));
|
||||
apiSpecInfoUI.setApiUrl(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiUrl()));
|
||||
apiSpecInfoUI.setMockUrl(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getMockUrl()));
|
||||
apiSpecInfoUI.setApiRequestSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiRequestSpec()));
|
||||
apiSpecInfoUI.setApiResponseSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiResponseSpec()));
|
||||
apiSpecInfoUI.setSampleRequest(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getSampleRequest()));
|
||||
apiSpecInfoUI.setSampleResponse(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getSampleResponse()));
|
||||
apiSpecInfoUI.setDescription(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getDescription()));
|
||||
apiSpecInfoUI.setTestbedSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getTestbedSpec()));
|
||||
|
||||
apiSpecManService.save(apiSpecInfoUI);
|
||||
|
||||
// API 그룹 소속(조인테이블) 동기화 — apiGroupIds(CSV) 로 재설정
|
||||
try {
|
||||
java.util.List<String> gids = new ArrayList<>();
|
||||
if (StringUtils.isNotBlank(apiGroupIds)) {
|
||||
for (String g : apiGroupIds.split(",")) {
|
||||
if (StringUtils.isNotBlank(g)) {
|
||||
gids.add(g.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
apiGroupService.setApiGroupsForApi(apiSpecInfoUI.getApiId(), gids);
|
||||
} catch (Exception e) {
|
||||
logger.warn("API 그룹 동기화 실패: " + apiSpecInfoUI.getApiId(), e);
|
||||
}
|
||||
|
||||
result.put("status", "success");
|
||||
result.put("apiId", apiSpecInfoUI.getApiId());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 레이아웃 불러오기 (GW) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=LOAD_REQUEST_LAYOUT")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> loadRequestLayout(String eaiSvcName) throws Exception {
|
||||
return loadLayout(eaiSvcName, true);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=LOAD_RESPONSE_LAYOUT")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, Object>> loadResponseLayout(String eaiSvcName) throws Exception {
|
||||
return loadLayout(eaiSvcName, false);
|
||||
}
|
||||
|
||||
// ── HTML 테이블 생성 (기존 generateSpecTableFromLayout 재사용) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=GENERATE_HTML")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> generateHtml(String eaiSvcName, String layoutType) throws Exception {
|
||||
LayoutUI layout = resolveLayout(eaiSvcName, !"RESPONSE".equalsIgnoreCase(layoutType));
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("html", apiSpecManService.generateSpecTableFromLayout(layout));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 메시지 예제 생성 (기존 generateSampleDataFromLayout 재사용) ──
|
||||
@PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=GENERATE_SAMPLE")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Map<String, String>> generateSample(String eaiSvcName, String layoutType) throws Exception {
|
||||
LayoutUI layout = resolveLayout(eaiSvcName, !"RESPONSE".equalsIgnoreCase(layoutType));
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("json", apiSpecManService.generateSampleDataFromLayout(layout));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── 내부 헬퍼 ──────────────────────────────────────────
|
||||
private ResponseEntity<Map<String, Object>> loadLayout(String eaiSvcName, boolean request) throws Exception {
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
if (StringUtils.isBlank(eaiSvcName)) {
|
||||
res.put("layoutType", request ? "REQUEST" : "RESPONSE");
|
||||
res.put("fields", new ArrayList<>());
|
||||
warnings.add("eaiSvcName 이 없습니다.");
|
||||
res.put("warnings", warnings);
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
LayoutUI layout = resolveLayout(eaiSvcName, request);
|
||||
if (layout == null || layout.getLayoutItems() == null || layout.getLayoutItems().isEmpty()) {
|
||||
warnings.add((request ? "요청" : "응답") + " 레이아웃이 없습니다.");
|
||||
}
|
||||
res.put("layoutType", request ? "REQUEST" : "RESPONSE");
|
||||
res.put("fields", layoutMergeService.toFields(layout, request ? "REQUEST" : "RESPONSE"));
|
||||
res.put("warnings", warnings);
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
|
||||
private LayoutUI resolveLayout(String eaiSvcName, boolean request) throws Exception {
|
||||
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
||||
if (apiInterfaceUI == null) {
|
||||
return null;
|
||||
}
|
||||
String layoutName = request ? apiInterfaceUI.getInboundRequestLayout() : apiInterfaceUI.getInboundResponseLayout();
|
||||
return StringUtils.isNotEmpty(layoutName) ? apiSpecManService.selectLayoutUI(layoutName) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 기반 자동 생성 + 자동생성 규칙(백엔드 단일 소스).
|
||||
* 규칙: title/tag/summary = API명(eaiSvcDesc), operationId = 인터페이스ID,
|
||||
* servers[0].url 을 path 앞에 붙이고 servers 제거(서버 선택 제거),
|
||||
* 요청/200 응답 예제를 레이아웃 샘플로 임베드.
|
||||
*/
|
||||
private String generateSpec(String eaiSvcName) throws Exception {
|
||||
ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
|
||||
if (apiInterfaceUI == null) {
|
||||
throw new IllegalStateException("API 인터페이스 정보를 찾을 수 없습니다: " + eaiSvcName);
|
||||
}
|
||||
Map<String, String> inboundAdapterSpec = apiInterfaceService.getHttpAdapterInfo(apiInterfaceUI.getFromAdapter());
|
||||
if (inboundAdapterSpec == null) {
|
||||
inboundAdapterSpec = new HashMap<>();
|
||||
}
|
||||
String requestLayoutName = apiInterfaceUI.getInboundRequestLayout();
|
||||
String responseLayoutName = apiInterfaceUI.getInboundResponseLayout();
|
||||
LayoutUI requestLayoutUI = StringUtils.isNotEmpty(requestLayoutName)
|
||||
? apiSpecManService.selectLayoutUI(requestLayoutName) : null;
|
||||
LayoutUI responseLayoutUI = StringUtils.isNotEmpty(responseLayoutName)
|
||||
? apiSpecManService.selectLayoutUI(responseLayoutName) : null;
|
||||
|
||||
String baseSpec = apiSpecManService.generateSwaggerSpec(requestLayoutUI, responseLayoutUI, inboundAdapterSpec, apiInterfaceUI);
|
||||
|
||||
String apiName = StringUtils.isNotBlank(apiInterfaceUI.getEaiSvcDesc()) ? apiInterfaceUI.getEaiSvcDesc() : eaiSvcName;
|
||||
String reqSample = apiSpecManService.generateSampleDataFromLayout(requestLayoutUI);
|
||||
String resSample = apiSpecManService.generateSampleDataFromLayout(responseLayoutUI);
|
||||
String contentType = StringUtils.isNotBlank(apiInterfaceUI.getRestContentType()) ? apiInterfaceUI.getRestContentType() : "application/json";
|
||||
return applyAutoRules(baseSpec, apiName, eaiSvcName, reqSample, resSample, contentType);
|
||||
}
|
||||
|
||||
/** 자동생성 규칙 후처리(Jackson 트리 조작). 실패 시 원본 유지. */
|
||||
private String applyAutoRules(String specJson, String apiName, String eaiSvcName, String reqSample, String resSample, String contentType) {
|
||||
try {
|
||||
JsonNode parsed = objectMapper.readTree(specJson);
|
||||
if (!parsed.isObject()) {
|
||||
return specJson;
|
||||
}
|
||||
ObjectNode root = (ObjectNode) parsed;
|
||||
|
||||
root.put("openapi", "3.1.0"); // OpenAPI 3.1 방출(Swagger UI 5.x oas31 렌더 지원)
|
||||
getOrCreateObject(root, "info").put("title", apiName);
|
||||
|
||||
String tagName = "DJBank"; // 자동생성 태그명 고정
|
||||
ArrayNode tags = objectMapper.createArrayNode();
|
||||
ObjectNode tag = objectMapper.createObjectNode();
|
||||
tag.put("name", tagName);
|
||||
tag.put("description", apiName); // 태그 설명 = 간단설명(API명)
|
||||
tags.add(tag);
|
||||
root.set("tags", tags);
|
||||
|
||||
// 어댑터경로(servers[0].url = getHttpAdapterInfo urlPath)를 path 앞에 baking 하고 servers 제거.
|
||||
// 호스트(gw=djb.gateway.base-url / mock=Mock URL / sample=없음)는 프론트가 응답유형에 따라 서버로 붙인다.
|
||||
String adapterPath = "";
|
||||
JsonNode serversNode = root.get("servers");
|
||||
if (serversNode != null && serversNode.isArray() && serversNode.size() > 0 && serversNode.get(0).get("url") != null) {
|
||||
adapterPath = serversNode.get(0).get("url").asText("");
|
||||
}
|
||||
root.remove("servers");
|
||||
|
||||
JsonNode pathsNode = root.get("paths");
|
||||
if (pathsNode != null && pathsNode.isObject()) {
|
||||
ObjectNode paths = (ObjectNode) pathsNode;
|
||||
List<String> pathKeys = new ArrayList<>();
|
||||
Iterator<String> pit = paths.fieldNames();
|
||||
while (pit.hasNext()) {
|
||||
pathKeys.add(pit.next());
|
||||
}
|
||||
for (String pk : pathKeys) {
|
||||
JsonNode pathItem = paths.get(pk);
|
||||
if (pathItem != null && pathItem.isObject()) {
|
||||
List<String> methods = new ArrayList<>();
|
||||
Iterator<String> mit = pathItem.fieldNames();
|
||||
while (mit.hasNext()) {
|
||||
methods.add(mit.next());
|
||||
}
|
||||
for (String mk : methods) {
|
||||
JsonNode opNode = pathItem.get(mk);
|
||||
if (opNode != null && opNode.isObject()) {
|
||||
ObjectNode op = (ObjectNode) opNode;
|
||||
op.put("operationId", eaiSvcName);
|
||||
op.put("summary", apiName);
|
||||
ArrayNode opTags = objectMapper.createArrayNode();
|
||||
opTags.add(tagName);
|
||||
op.set("tags", opTags);
|
||||
// 예제(요청/응답 본문)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치. 여기선 임베드하지 않음.
|
||||
markGwOnOperation(op);
|
||||
addResponseHeader(op, "200", "Content-Type", contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
String newKey = adapterPath.isEmpty() ? pk
|
||||
: (adapterPath.replaceAll("/+$", "") + (pk.startsWith("/") ? "" : "/") + pk);
|
||||
if (!newKey.equals(pk)) {
|
||||
paths.set(newKey, pathItem);
|
||||
paths.remove(pk);
|
||||
}
|
||||
}
|
||||
}
|
||||
return objectMapper.writeValueAsString(root);
|
||||
} catch (Exception e) {
|
||||
logger.error("applyAutoRules 실패, 원본 spec 유지", e);
|
||||
return specJson;
|
||||
}
|
||||
}
|
||||
|
||||
private void setContentExample(ObjectNode op, String sampleJson) {
|
||||
if (StringUtils.isBlank(sampleJson)) {
|
||||
return;
|
||||
}
|
||||
JsonNode body = op.get("requestBody");
|
||||
if (body == null || !body.isObject()) {
|
||||
return;
|
||||
}
|
||||
JsonNode content = body.get("content");
|
||||
if (content == null || !content.isObject()) {
|
||||
return;
|
||||
}
|
||||
Iterator<String> mts = content.fieldNames();
|
||||
if (!mts.hasNext()) {
|
||||
return;
|
||||
}
|
||||
JsonNode mtNode = content.get(mts.next());
|
||||
if (mtNode != null && mtNode.isObject()) {
|
||||
((ObjectNode) mtNode).set("example", parseJsonOrText(sampleJson));
|
||||
}
|
||||
}
|
||||
|
||||
private void setResponseExample(ObjectNode op, String code, String sampleJson) {
|
||||
if (StringUtils.isBlank(sampleJson)) {
|
||||
return;
|
||||
}
|
||||
JsonNode resps = op.get("responses");
|
||||
if (resps == null || !resps.isObject()) {
|
||||
return;
|
||||
}
|
||||
JsonNode r = resps.get(code);
|
||||
if (r == null || !r.isObject()) {
|
||||
return;
|
||||
}
|
||||
ObjectNode content = getOrCreateObject((ObjectNode) r, "content");
|
||||
ObjectNode appjson = getOrCreateObject(content, "application/json");
|
||||
appjson.set("example", parseJsonOrText(sampleJson));
|
||||
}
|
||||
|
||||
/** GW 유래 스키마 필드에 x-djb-gw 마커 부여(프론트에서 잠금 표시). */
|
||||
private void markGwOnOperation(ObjectNode op) {
|
||||
JsonNode body = op.get("requestBody");
|
||||
if (body != null && body.isObject()) {
|
||||
markGwInContent(body.get("content"));
|
||||
}
|
||||
JsonNode resps = op.get("responses");
|
||||
if (resps != null && resps.isObject()) {
|
||||
List<String> codes = new ArrayList<>();
|
||||
Iterator<String> it = resps.fieldNames();
|
||||
while (it.hasNext()) {
|
||||
codes.add(it.next());
|
||||
}
|
||||
for (String c : codes) {
|
||||
JsonNode r = resps.get(c);
|
||||
if (r != null && r.isObject()) {
|
||||
markGwInContent(r.get("content"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void markGwInContent(JsonNode content) {
|
||||
if (content == null || !content.isObject()) {
|
||||
return;
|
||||
}
|
||||
List<String> mts = new ArrayList<>();
|
||||
Iterator<String> it = content.fieldNames();
|
||||
while (it.hasNext()) {
|
||||
mts.add(it.next());
|
||||
}
|
||||
for (String mt : mts) {
|
||||
JsonNode m = content.get(mt);
|
||||
if (m != null && m.isObject()) {
|
||||
markGwSchema(m.get("schema"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void markGwSchema(JsonNode schema) {
|
||||
if (schema == null || !schema.isObject()) {
|
||||
return;
|
||||
}
|
||||
ObjectNode s = (ObjectNode) schema;
|
||||
JsonNode props = s.get("properties");
|
||||
if (props != null && props.isObject()) {
|
||||
ArrayNode required = (s.get("required") != null && s.get("required").isArray())
|
||||
? (ArrayNode) s.get("required") : objectMapper.createArrayNode();
|
||||
java.util.Set<String> existing = new java.util.HashSet<>();
|
||||
required.forEach(n -> existing.add(n.asText()));
|
||||
List<String> keys = new ArrayList<>();
|
||||
Iterator<String> it = props.fieldNames();
|
||||
while (it.hasNext()) {
|
||||
keys.add(it.next());
|
||||
}
|
||||
for (String k : keys) {
|
||||
JsonNode p = props.get(k);
|
||||
if (p != null && p.isObject()) {
|
||||
((ObjectNode) p).put("x-djb-gw", true);
|
||||
if (!existing.contains(k)) { // GW 필드는 기본 '필수'
|
||||
required.add(k);
|
||||
existing.add(k);
|
||||
}
|
||||
markGwSchema(p);
|
||||
}
|
||||
}
|
||||
if (required.size() > 0) {
|
||||
s.set("required", required);
|
||||
}
|
||||
}
|
||||
JsonNode items = s.get("items");
|
||||
if (items != null && items.isObject()) {
|
||||
markGwSchema(items);
|
||||
}
|
||||
}
|
||||
|
||||
/** 응답 헤더 미리 등록(중복 시 스킵). */
|
||||
private void addResponseHeader(ObjectNode op, String code, String headerName, String example) {
|
||||
JsonNode resps = op.get("responses");
|
||||
if (resps == null || !resps.isObject()) {
|
||||
return;
|
||||
}
|
||||
JsonNode r = resps.get(code);
|
||||
if (r == null || !r.isObject()) {
|
||||
return;
|
||||
}
|
||||
ObjectNode headers = getOrCreateObject((ObjectNode) r, "headers");
|
||||
if (headers.has(headerName)) {
|
||||
return;
|
||||
}
|
||||
ObjectNode h = objectMapper.createObjectNode();
|
||||
h.put("description", headerName);
|
||||
ObjectNode sc = objectMapper.createObjectNode();
|
||||
sc.put("type", "string");
|
||||
h.set("schema", sc);
|
||||
h.put("example", example);
|
||||
headers.set(headerName, h);
|
||||
}
|
||||
|
||||
private JsonNode parseJsonOrText(String s) {
|
||||
try {
|
||||
return objectMapper.readTree(s);
|
||||
} catch (Exception e) {
|
||||
return TextNode.valueOf(s);
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectNode getOrCreateObject(ObjectNode parent, String field) {
|
||||
JsonNode node = parent.get(field);
|
||||
if (node != null && node.isObject()) {
|
||||
return (ObjectNode) node;
|
||||
}
|
||||
ObjectNode created = objectMapper.createObjectNode();
|
||||
parent.set(field, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private void putIfNotNull(Map<String, String> map, String key, String value) {
|
||||
if (value != null) {
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutItemUI;
|
||||
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutUI;
|
||||
|
||||
/**
|
||||
* D9 레이아웃 머지: GW LayoutUI 를 마법사 그리드용 필드 목록으로 변환한다.
|
||||
*
|
||||
* <p>GW 에서 온 필드는 {@code sourceType=GW}(항목명/데이터타입 잠금)로 표기하고, 예제값은
|
||||
* {@link DjbApiSpecSampleGenerator} 규칙으로 자동 채운다. 저장된 spec 의 부가정보(설명/예제/제약)
|
||||
* 병합은 클라이언트(그리드)에서 사용자 편집분과 합치므로, 서버는 GW 원본 필드 + 자동 예제까지 제공한다.
|
||||
*/
|
||||
@Service
|
||||
public class DjbApiSpecLayoutMergeService {
|
||||
|
||||
@Autowired
|
||||
private DjbApiSpecSampleGenerator sampleGenerator;
|
||||
|
||||
/** LayoutUI → 그리드 필드 행 목록(flat, depth/parentIndex 로 트리 복원 가능). */
|
||||
public List<Map<String, Object>> toFields(LayoutUI layout, String layoutType) {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
if (layout == null || layout.getLayoutItems() == null) {
|
||||
return rows;
|
||||
}
|
||||
for (LayoutItemUI it : layout.getLayoutItems()) {
|
||||
// serno 0 은 root 컨테이너 → 스킵 (ApiSpecManService.generateSpecTableRows 규칙과 동일)
|
||||
if (it.getLoutItemSerno() != null && it.getLoutItemSerno() == 0) {
|
||||
continue;
|
||||
}
|
||||
String dataType = it.getLoutItemDataType();
|
||||
int len = it.getLoutItemLength();
|
||||
int dec = it.getLoutItemDecimal();
|
||||
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
r.put("serno", it.getLoutItemSerno());
|
||||
r.put("name", it.getLoutItemName());
|
||||
r.put("desc", it.getLoutItemDesc());
|
||||
r.put("itemType", it.getLoutItemType()); // FIELD / GROUP / GRID
|
||||
r.put("dataType", dataType);
|
||||
r.put("category", sampleGenerator.categoryOf(dataType));
|
||||
r.put("length", len);
|
||||
r.put("decimal", dec);
|
||||
r.put("depth", it.getLoutItemDepth());
|
||||
r.put("parentIndex", it.getParentLoutItemIndex());
|
||||
r.put("required", false);
|
||||
r.put("sourceType", "GW");
|
||||
r.put("example", sampleGenerator.exampleFor(dataType, it.getLoutItemName(), len, dec));
|
||||
rows.add(r);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* D10 예제값 자동 생성 규칙.
|
||||
* <ul>
|
||||
* <li>string → {@code sample_} + 항목명. 최대길이 초과 시 left-cut(앞에서 자름).</li>
|
||||
* <li>integer → {@code 1}</li>
|
||||
* <li>number → {@code 1.1} (소수 자릿수 지정 시 {@code 1.1000} 형태)</li>
|
||||
* <li>boolean → {@code false}</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Service
|
||||
public class DjbApiSpecSampleGenerator {
|
||||
|
||||
/** 원시 데이터타입(레이아웃 값)을 OpenAPI 계열 카테고리로 분류. */
|
||||
public String categoryOf(String rawDataType) {
|
||||
if (rawDataType == null) {
|
||||
return "string";
|
||||
}
|
||||
String t = rawDataType.trim().toLowerCase();
|
||||
if (t.contains("bool")) {
|
||||
return "boolean";
|
||||
}
|
||||
if (t.contains("decimal") || t.contains("double") || t.contains("float") || t.contains("number")) {
|
||||
return "number";
|
||||
}
|
||||
if (t.contains("int") || t.contains("long")) {
|
||||
return "integer";
|
||||
}
|
||||
if (t.contains("object") || t.contains("group")) {
|
||||
return "object";
|
||||
}
|
||||
if (t.contains("array") || t.contains("grid") || t.contains("list")) {
|
||||
return "array";
|
||||
}
|
||||
return "string";
|
||||
}
|
||||
|
||||
/**
|
||||
* 예제값 생성.
|
||||
* @param rawDataType 레이아웃 데이터타입(원시)
|
||||
* @param fieldName 항목명
|
||||
* @param maxLen 최대 길이(0/음수면 무제한)
|
||||
* @param decimalLen 소수 자릿수(0이면 미적용)
|
||||
*/
|
||||
public String exampleFor(String rawDataType, String fieldName, int maxLen, int decimalLen) {
|
||||
switch (categoryOf(rawDataType)) {
|
||||
case "integer":
|
||||
return "1";
|
||||
case "number":
|
||||
if (decimalLen > 0) {
|
||||
StringBuilder sb = new StringBuilder("1.");
|
||||
for (int i = 0; i < decimalLen; i++) {
|
||||
sb.append(i == 0 ? '1' : '0');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
return "1.1";
|
||||
case "boolean":
|
||||
return "false";
|
||||
case "object":
|
||||
case "array":
|
||||
return ""; // 컨테이너는 예제값 없음(자식으로 표현)
|
||||
default:
|
||||
String v = "sample_" + StringUtils.defaultString(fieldName);
|
||||
if (maxLen > 0 && v.length() > maxLen) {
|
||||
v = v.substring(0, maxLen); // left-cut
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package com.eactive.eai.rms.onl.transaction.apim;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
/**
|
||||
* 생성/저장된 OpenAPI(3.0) spec JSON 에 인증 체계(components.securitySchemes + 글로벌 security)를 주입한다.
|
||||
*
|
||||
* <p>포털 {@code DjbSwaggerSpecEnricher} 와 동일 모델링:
|
||||
* 게이트웨이가 토큰/키를 표준 Authorization 이 아니라 커스텀 헤더로 받으므로 OAuth·API_KEY 모두
|
||||
* <b>apiKey-in-header</b> 스킴으로 표현(헤더명만 상이).
|
||||
*
|
||||
* <p>authtype(TSEAIHE01.AUTHTYPE, 소문자 저장) 매핑:
|
||||
* {@code oauth/oauth2/ca → OAUTH}, {@code api_key/apikey → API_KEY}, 그 외 → 주입 없음.
|
||||
* 원본 트리를 보존하며 조작한다(문자열 치환 금지). 매핑 불가/빈 스펙/파싱 실패 시 원본 유지(멱등).
|
||||
*/
|
||||
@Service
|
||||
public class DjbApiSpecSwaggerEnricher {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public static final String OAUTH_SCHEME = "djbOAuth";
|
||||
public static final String API_KEY_SCHEME = "djbApiKey";
|
||||
// 게이트웨이 헤더명 (포털 DjbTestbedGatewayProperty 기본값과 동일)
|
||||
public static final String OAUTH_HEADER = "X-AUTH-TOKEN";
|
||||
public static final String API_KEY_HEADER = "X-DJB-API-Key";
|
||||
|
||||
/** authtype → 스킴명. 매핑 불가면 null. */
|
||||
public String schemeNameOf(String authtype) {
|
||||
if (authtype == null) {
|
||||
return null;
|
||||
}
|
||||
switch (authtype.trim().toLowerCase()) {
|
||||
case "oauth":
|
||||
case "oauth2":
|
||||
case "ca":
|
||||
return OAUTH_SCHEME;
|
||||
case "api_key":
|
||||
case "apikey":
|
||||
return API_KEY_SCHEME;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* spec JSON 에 authtype 기반 securityScheme 주입. 매핑 불가/빈 스펙/파싱 실패 시 원본 그대로 반환.
|
||||
*/
|
||||
public String enrich(String specJson, String authtype) {
|
||||
String schemeName = schemeNameOf(authtype);
|
||||
if (StringUtils.isBlank(specJson) || schemeName == null) {
|
||||
return specJson;
|
||||
}
|
||||
try {
|
||||
JsonNode parsed = objectMapper.readTree(specJson);
|
||||
if (!parsed.isObject()) {
|
||||
return specJson;
|
||||
}
|
||||
ObjectNode root = (ObjectNode) parsed;
|
||||
|
||||
ObjectNode scheme = objectMapper.createObjectNode();
|
||||
scheme.put("type", "apiKey");
|
||||
scheme.put("in", "header");
|
||||
scheme.put("name", OAUTH_SCHEME.equals(schemeName) ? OAUTH_HEADER : API_KEY_HEADER);
|
||||
|
||||
boolean swagger2 = root.has("swagger") && !root.has("openapi");
|
||||
if (swagger2) {
|
||||
getOrCreateObject(root, "securityDefinitions").set(schemeName, scheme);
|
||||
} else {
|
||||
ObjectNode components = getOrCreateObject(root, "components");
|
||||
getOrCreateObject(components, "securitySchemes").set(schemeName, scheme);
|
||||
}
|
||||
|
||||
// 글로벌 security 요구사항 (apiKey 스킴은 빈 스코프 배열)
|
||||
ArrayNode security = objectMapper.createArrayNode();
|
||||
ObjectNode requirement = objectMapper.createObjectNode();
|
||||
requirement.set(schemeName, objectMapper.createArrayNode());
|
||||
security.add(requirement);
|
||||
root.set("security", security);
|
||||
|
||||
return objectMapper.writeValueAsString(root);
|
||||
} catch (Exception e) {
|
||||
return specJson; // 파싱/직렬화 실패 → 원본 유지(멱등)
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectNode getOrCreateObject(ObjectNode parent, String field) {
|
||||
JsonNode node = parent.get(field);
|
||||
if (node != null && node.isObject()) {
|
||||
return (ObjectNode) node;
|
||||
}
|
||||
ObjectNode created = objectMapper.createObjectNode();
|
||||
parent.set(field, created);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,26 @@
|
||||
package com.eactive.eai.rms.onl.transaction.tracking;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
|
||||
import com.eactive.apim.portal.monitoringcode.entity.MonitoringCodeId;
|
||||
import com.eactive.eai.common.EAITable;
|
||||
import com.eactive.eai.common.header.HeaderAction;
|
||||
@@ -8,7 +29,11 @@ import com.eactive.eai.common.header.HeaderActionKeys;
|
||||
import com.eactive.eai.common.logger.HttpAdapterExtraLogVo;
|
||||
import com.eactive.eai.common.logger.mapper.HttpAdapterExtraLogMapper;
|
||||
import com.eactive.eai.common.util.MessageUtil;
|
||||
import com.eactive.eai.data.entity.onl.logger.*;
|
||||
import com.eactive.eai.data.entity.onl.logger.EAILogId;
|
||||
import com.eactive.eai.data.entity.onl.logger.HttpAdapterExtraLog;
|
||||
import com.eactive.eai.data.entity.onl.logger.HttpAdapterExtraLogId;
|
||||
import com.eactive.eai.data.entity.onl.logger.SubMessageLog;
|
||||
import com.eactive.eai.data.entity.onl.logger.SubMessageLogId;
|
||||
import com.eactive.eai.data.entity.onl.message.ServiceMessageEntity;
|
||||
import com.eactive.eai.data.entity.onl.message.ServiceMessageEntityId;
|
||||
import com.eactive.eai.data.entity.onl.transformer.layout.LayoutEntity;
|
||||
@@ -26,7 +51,12 @@ import com.eactive.eai.rms.common.login.SessionManager;
|
||||
import com.eactive.eai.rms.data.entity.man.monitoringCode.service.MonitoringCodeService;
|
||||
import com.eactive.eai.rms.data.entity.onl.eaimsg.ServiceMessageService;
|
||||
import com.eactive.eai.rms.data.entity.onl.layout.LayoutService;
|
||||
import com.eactive.eai.rms.data.entity.onl.logger.*;
|
||||
import com.eactive.eai.rms.data.entity.onl.logger.EAILogDetailSearch;
|
||||
import com.eactive.eai.rms.data.entity.onl.logger.EAILogDto;
|
||||
import com.eactive.eai.rms.data.entity.onl.logger.EAILogSearch;
|
||||
import com.eactive.eai.rms.data.entity.onl.logger.EAILogService;
|
||||
import com.eactive.eai.rms.data.entity.onl.logger.HttpAdapterExtraLogService;
|
||||
import com.eactive.eai.rms.data.entity.onl.logger.SubMessageLogService;
|
||||
import com.eactive.eai.rms.data.entity.onl.transform.TransformService;
|
||||
import com.eactive.eai.rms.onl.common.exception.EMSRuntimeException;
|
||||
import com.eactive.eai.rms.onl.manage.adapter.adapter.AdapterManService;
|
||||
@@ -38,26 +68,14 @@ import com.eactive.eai.transformer.dao.jdbc.LayoutJdbcDao;
|
||||
import com.eactive.eai.transformer.layout.Item;
|
||||
import com.eactive.eai.transformer.layout.Layout;
|
||||
import com.eactive.eai.transformer.layout.LayoutTypeManager;
|
||||
import com.eactive.eai.transformer.message.*;
|
||||
import com.eactive.eai.transformer.message.Message;
|
||||
import com.eactive.eai.transformer.message.MessageSetDataException;
|
||||
import com.eactive.eai.transformer.message.MonitoringVBytesMessage;
|
||||
import com.eactive.eai.transformer.message.MonitoringVEbcdicMessage;
|
||||
import com.eactive.eai.transformer.message.XMLMessage;
|
||||
import com.eactive.eai.transformer.util.IConv;
|
||||
import com.eactive.eai.util.HexaConverter;
|
||||
import com.ext.eai.common.stdmessage.STDMessageKeys;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
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.io.UnsupportedEncodingException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service("dbTrackingService")
|
||||
@Transactional
|
||||
@@ -85,6 +103,7 @@ public class DbTrackingService extends BaseService {
|
||||
private final TransformService transformService;
|
||||
private final LayoutJdbcDao layoutJdbcDao;
|
||||
private final LayoutService layoutService;
|
||||
private final PersonalDataEncryptConverter encryptConverter = new PersonalDataEncryptConverter();
|
||||
|
||||
@Autowired
|
||||
private AdapterManService adapterManService;
|
||||
@@ -222,7 +241,7 @@ public class DbTrackingService extends BaseService {
|
||||
|
||||
|
||||
if (!StringUtils.isBlank(data)) {
|
||||
eaiLogUI.setBzwkdatactnt(data);
|
||||
eaiLogUI.setBzwkdatactnt(encryptConverter.convertToEntityAttribute(data));
|
||||
} else {
|
||||
eaiLogUI.setBzwkdatactnt("");
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-DEBUG}" />
|
||||
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
</appender> -->
|
||||
|
||||
<appender name="STDOUT"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
@@ -183,6 +183,5 @@
|
||||
<root>
|
||||
<level value="DEBUG" />
|
||||
<appender-ref ref="STDOUT" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<configuration scan="true" scanPeriod="10 seconds">
|
||||
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
|
||||
<!-- <statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />-->
|
||||
|
||||
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
|
||||
<property name="PREFIX" value="${inst.Type:-ems}" />
|
||||
<property name="LOG_HOME" value="/logs/eapim/${inst.Name:-devSvr00}" />
|
||||
<property name="LOG_HOME" value="D:/Log/App/eapim/${inst.Name:-devSvr00}" />
|
||||
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
|
||||
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-DEBUG}" />
|
||||
|
||||
@@ -113,12 +113,13 @@
|
||||
<logger name="com.eactive.eai.rms.onl.dashboard" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
|
||||
|
||||
<logger name="org.springframework" level="DEBUG" />
|
||||
<logger name="org.hibernate" level="DEBUG" >
|
||||
<logger name="org.springframework" level="INFO" />
|
||||
<logger name="org.hibernate" level="INFO" >
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<logger name="net.sf.ehcache" level="INFO" />
|
||||
<logger name="org.quartz" level="INFO" />
|
||||
|
||||
|
||||
<logger name="ACCESS_LOGGER" level="DEBUG" >
|
||||
@@ -135,6 +136,7 @@
|
||||
<!-- QUERY_APPENDER start -->
|
||||
<logger name="java.sql" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</logger>
|
||||
<logger name="com.ibatis" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
@@ -159,9 +161,11 @@
|
||||
</logger>
|
||||
<logger name="java.sql.PreparedStatement" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="CONSOLE" /> <!-- 이 줄 추가 -->
|
||||
</logger>
|
||||
<logger name="java.sql.ResultSet" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="CONSOLE" /> <!-- 이 줄 추가 -->
|
||||
</logger>
|
||||
<logger name="com.eactive.eai.rms.common.base.SchemaIdConfiguredSqlMapTemplate" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
@@ -178,6 +182,22 @@
|
||||
|
||||
<!-- QUERY_APPENDER end -->
|
||||
|
||||
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</logger>
|
||||
|
||||
<!-- org.hibernate.SQL 로거 아래에 추가 -->
|
||||
<logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="TRACE" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</logger>
|
||||
|
||||
<!-- 바인딩 파라미터 값 (Hibernate 5.4 기준) -->
|
||||
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</logger>
|
||||
|
||||
<root>
|
||||
<level value="DEBUG" />
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<configuration scan="true" scanPeriod="10 seconds">
|
||||
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
|
||||
|
||||
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] \\(%F:%L\\) %-20M -%msg%n" />
|
||||
<property name="PREFIX" value="${inst.Type:-ems}" />
|
||||
<property name="LOG_HOME" value="/Users/rinjae/eactive/djb-eapim/djb-eapim/eapim-admin/logs" />
|
||||
<property name="BACKUP_HOME" value="${LOG_HOME}/backup/" />
|
||||
<property name="LOG_LEVEL" value="${LOGBACK_LOG_LEVEL:-DEBUG}" />
|
||||
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="STDOUT"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_stdout.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_stdout.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="ACCESS_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_access.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_access.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="SMS_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/sms.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/sms.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="QUERY_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_query.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_query.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="DYNAMIC_APPENDER"
|
||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_dynamic.log</file>
|
||||
<rollingPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>
|
||||
${LOG_HOME}/${PREFIX}_dynamic.log-%d{yyyy-MM-dd}.%i
|
||||
</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="HIBERNATE_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_hibernate.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/${PREFIX}_hibernate.log-%d{yyyy-MM-dd}.%i</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="HOTSWAP_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_HOME}/${PREFIX}_hotswap.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_HOME}/${PREFIX}_hotswap.log-%d{yyyy-MM-dd}.%i</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>10</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>utf-8</charset>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="com.eactive.eai.rms.onl.server" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.onl.dashboard" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.common.acl.user.BizDao" level="ERROR" />
|
||||
<logger name="com.eactive.eai.rms.ext.djb.job.ApiStatusMonitorJob" level="ERROR" />
|
||||
|
||||
<logger name="org.springframework.orm.jpa.JpaTransactionManager" level="ERROR" additivity="false" />
|
||||
|
||||
<logger name="org.springframework" level="INFO" />
|
||||
<logger name="net.sf.ehcache" level="INFO" />
|
||||
|
||||
|
||||
<logger name="ACCESS_LOGGER" level="DEBUG" >
|
||||
<appender-ref ref="ACCESS_APPENDER" />
|
||||
</logger>
|
||||
<logger name="SMS_LOGGER" level="DEBUG" >
|
||||
<appender-ref ref="SMS_APPENDER" />
|
||||
</logger>
|
||||
<logger name="DYNAMIC_LOGGER" level="DEBUG" >
|
||||
<appender-ref ref="DYNAMIC_APPENDER" />
|
||||
</logger>
|
||||
|
||||
|
||||
<!-- QUERY_APPENDER start -->
|
||||
<!-- Hibernate SQL & 파라미터 바인딩 -->
|
||||
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="TRACE" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<!-- Hibernate 일반 로그 (DDL/init/connection 등) -->
|
||||
<logger name="org.hibernate" level="INFO" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
<appender-ref ref="STDOUT" />
|
||||
</logger>
|
||||
|
||||
<!-- iBATIS 2.3.4 (commons-logging 또는 log4j-over-slf4j 통해 SLF4J 도달) -->
|
||||
<logger name="com.ibatis" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
</logger>
|
||||
<logger name="com.eactive.eai.rms.common.base.SchemaIdConfiguredSqlMapTemplate" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="QUERY_APPENDER" />
|
||||
</logger>
|
||||
<!-- QUERY_APPENDER end -->
|
||||
|
||||
<!-- HotswapAgent는 자체 logger를 사용하므로 logback에 안 잡힘. hotswap-agent.properties의 LOGFILE로 직접 파일 출력 -->
|
||||
|
||||
<root>
|
||||
<level value="INFO" />
|
||||
<appender-ref ref="STDOUT" />
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -2751,6 +2751,7 @@ portalOrg.compRegFile =\uC0AC\uC5C5\uC790 \uB4F1\uB85D\uC99D
|
||||
portalOrg.serviceName =\uC11C\uBE44\uC2A4\uBA85
|
||||
portalOrg.ipWhitelist =IP \uB300\uC5ED
|
||||
portalOrg.noManagerMsg=\uB4F1\uB85D\uB41C \uBC95\uC778 \uAD00\uB9AC\uC790\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
|
||||
portalOrg.reverseProxyPath=\uB9AC\uBC84\uC2A4 \uD504\uB85D\uC2DC \uACBD\uB85C
|
||||
|
||||
userLoginHistory.title =\uB85C\uADF8\uC778\uC774\uB825
|
||||
userLoginHistory.loginDttm =\uC811\uC18D\uC77C\uC2DC
|
||||
|
||||
@@ -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(""));
|
||||
}
|
||||
}
|
||||
+135
@@ -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("<b>hi</b>",
|
||||
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("<script>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("앰퍼샌드 escape")
|
||||
void maskAndHighlight_escapesAmp() {
|
||||
assertEquals("a&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(""));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user