2 Commits

Author SHA1 Message Date
Rinjae 8a884e53e3 ci - readiness probe port 39120 으로 정정
eapim-admin CI / build (push) Has been cancelled
- DEPLOY_HTTP_PORT 30120 → 39120 (실서버 tomcat 포트)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 16:30:13 +09:00
Rinjae 7de00ecad2 ci - readiness probe 안정화
eapim-admin CI / build (push) Has been cancelled
- probe URL 변경: login.do(POST) → loginForm.do(GET)
- timeout 180s → 300s (200MB war 첫 explode 시간 확보)
- 실패 시 catalina 로그 300줄 + systemd status + 포트 출력

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 16:22:07 +09:00
31 changed files with 180 additions and 744 deletions
Vendored
-179
View File
@@ -1,179 +0,0 @@
pipeline {
agent { label 'djb-vm' }
options {
timestamps()
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
}
environment {
JAVA_HOME = '/apps/opts/jdk8'
JAVA_HOME_TOMCAT = '/apps/opts/jdk17'
GRADLE_HOME = '/apps/opts/gradle-8.7'
GRADLE_USER_HOME = '/apps/opts/gradle-home'
NODE_HOME = '/apps/opts/node-v24'
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/node-v24/bin:/apps/opts/bin:${env.PATH}"
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
CATALINA_BASE = '/prod/eapim/adminportal'
CATALINA_HOME = '/prod/eapim/apache-tomcat-9.0.116'
CATALINA_PID = '/prod/eapim/adminportal/temp/catalina.pid'
DEPLOY_HTTP_PORT = '39120'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Checkout dependencies') {
steps {
sh '''
set -eu
cd "$WORKSPACE/.."
mkdir -p eapim-online
for REPO in elink-online-core elink-online-core-jpa elink-online-transformer elink-online-common elink-online-emsclient; do
TARGET="eapim-online/$REPO"
if [ ! -d "$TARGET/.git" ]; then
rm -rf "$TARGET"
git clone --depth=1 --branch master \
"ssh://git@172.30.1.50:2222/djb-eapim/$REPO.git" \
"$TARGET"
else
git -C "$TARGET" fetch --depth=1 origin master
git -C "$TARGET" reset --hard origin/master
git -C "$TARGET" clean -fdx
fi
done
for REPO in elink-portal-common eapim-admin-djb; do
if [ ! -d "$REPO/.git" ]; then
rm -rf "$REPO"
git clone --depth=1 --branch master \
"ssh://git@172.30.1.50:2222/djb-eapim/$REPO.git" \
"$REPO"
else
git -C "$REPO" fetch --depth=1 origin master
git -C "$REPO" reset --hard origin/master
git -C "$REPO" clean -fdx
fi
done
'''
}
}
stage('Verify toolchain') {
steps {
sh '''
set -eu
java -version
gradle --version
'''
}
}
stage('Build WAR') {
steps {
sh 'gradle clean build -x test --no-daemon'
}
post {
success {
sh '''
set -eu
cd build/libs
sha1sum eapim-admin.war > SHA1SUMS
sha256sum eapim-admin.war > SHA256SUMS
'''
archiveArtifacts artifacts: 'build/libs/eapim-admin.war,build/libs/SHA1SUMS,build/libs/SHA256SUMS', fingerprint: true
}
}
}
stage('Stop Tomcat') {
steps {
sh '''
set +e
systemctl --user stop eapim-admin 2>/dev/null
if [ -f "$CATALINA_PID" ] && kill -0 "$(cat "$CATALINA_PID")" 2>/dev/null; then
JAVA_HOME="$JAVA_HOME_TOMCAT" "$CATALINA_HOME/bin/shutdown.sh" 30 -force || true
for i in $(seq 1 30); do
[ -f "$CATALINA_PID" ] && kill -0 "$(cat "$CATALINA_PID")" 2>/dev/null || break
sleep 1
done
fi
rm -f "$CATALINA_PID"
'''
}
}
stage('Deploy monitoring.war') {
steps {
sh '''
set -eu
DEPLOY_DIR="$CATALINA_BASE/webapps"
rm -rf "$DEPLOY_DIR/monitoring" "$DEPLOY_DIR/monitoring.war"
cp build/libs/eapim-admin.war "$DEPLOY_DIR/monitoring.war"
ls -la "$DEPLOY_DIR"
'''
}
}
stage('Start Tomcat and readiness') {
steps {
sh '''
set -eu
LOG="$CATALINA_BASE/logs/catalina.$(date +%Y-%m-%d).log"
BEFORE=0
[ -f "$LOG" ] && BEFORE=$(stat -c %s "$LOG")
systemctl --user start eapim-admin
PROBE_URL="http://localhost:$DEPLOY_HTTP_PORT/monitoring/loginForm.do"
DEADLINE=$(($(date +%s) + 300))
STATUS=000
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
"$PROBE_URL" 2>/dev/null || echo "000")
case "$STATUS" in
200|302|401)
echo "Readiness OK ($PROBE_URL HTTP $STATUS)"
break
;;
esac
if [ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | grep -qE "Application listener .* Exception|SEVERE.*startup.Catalina"; then
echo "Startup error detected in log"
tail -c +$((BEFORE+1)) "$LOG" | tail -80
exit 1
fi
sleep 2
done
case "$STATUS" in
200|302|401) ;;
*)
echo "Readiness probe failed within 300s, last HTTP status: $STATUS"
echo "--- last 300 lines of catalina log ---"
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -300
echo "--- systemd unit status ---"
systemctl --user status eapim-admin --no-pager || true
echo "--- listening ports ---"
ss -tlnp 2>/dev/null | grep -E ":$DEPLOY_HTTP_PORT\\b" || echo "(port $DEPLOY_HTTP_PORT not listening)"
exit 1
;;
esac
echo "--- key boot log lines ---"
tail -c +$((BEFORE+1)) "$LOG" | grep -E "Server startup|Deployment of web" | head -10 || true
'''
}
}
}
}
+1 -1
View File
@@ -12,6 +12,6 @@ PortalTermsManController_약관관리_APIGW_INSERT,UPDATE,DELETE
ProdClientManController_운영Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE
ApiSpecController_API스펙관리_APIGW_INSERT,DELETE
MessageTemplateManController_메세지템플릿관리_APIGW_INSERT,UPDATE,DELETE
PortalPartnershipManController_피드백/개선요청관리_APIGW_UNMASK,DELETE
PortalPartnershipManController_사업제휴신청관리_APIGW_UNMASK
PortalPropertyController_포탈 프로퍼티 관리_APIGW_UPDATE
PortalUserTermsManController_약관동의 이력_APIGW_UNMASK
-1
View File
@@ -1,7 +1,6 @@
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@page import="org.apache.commons.lang3.StringUtils"%>
<%@page import="com.eactive.eai.rms.common.login.SessionManager"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
+2 -12
View File
@@ -52,13 +52,6 @@
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("&"));
@@ -76,13 +69,10 @@
$(document).ready(function() {
var url = '<%=topPage%>';
var serviceType = getServiceType();
if (serviceType) {
if (url.indexOf("?")>=0){
url = url + "&serviceType="+ serviceType;
url = url + "&serviceType="+ sessionStorage["serviceType"];
}else{
url = url + "?serviceType="+ serviceType;
}
url = url + "?serviceType="+ sessionStorage["serviceType"];
}
$("#topFrame").attr('src',url);
+22 -39
View File
@@ -1,10 +1,5 @@
/**
* 문자열 마스킹 처리를 위한 유틸리티
*
* 보안아키텍처 표준 마스킹 규칙(Java MaskingUtils)과 동일한 결과를 내도록 구현한다.
* 이름 2자: 뒤 1자리(가나→가*) / 3자 이상: 앞뒤 제외 가운데 전체(가나다라→가**라)
* 이메일 아이디 앞2·뒤2 마스킹, 4자 이하 전체(test→****, test123→**st1**)
* 전화 2·3번째 세그먼트 각각 뒤 2자리(010-1234-1234→010-12**-12**)
*/
const StringMaskingUtil = {
@@ -13,51 +8,41 @@ 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;
}
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);
return name.length > 1 ?
name.substring(0, name.length - 1) + "*" :
name;
},
// 이메일 마스킹 처리 (아이디 앞2·뒤2, 4자 이하 전체)
// 이메일 마스킹 처리
maskEmail: function(email) {
if (!this.isValidString(email) || !email.includes('@')) {
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 parts = email.split('@');
if (parts.length !== 2) {
return email;
}
return this._stars(2) + local.substring(2, local.length - 2) + this._stars(2) + 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];
},
// 휴대폰/전화 번호 마스킹 처리 (2·3번째 세그먼트 각각 뒤 2자리)
// 휴대폰 번호 마스킹 처리
maskMobileNumber: function(number) {
if (!this.isValidString(number)) {
return number;
@@ -65,9 +50,7 @@ const StringMaskingUtil = {
const parts = number.split('-');
if (parts.length === 3) {
return parts[0] + '-' +
this._maskSegmentTail(parts[1], 2) + '-' +
this._maskSegmentTail(parts[2], 2);
return parts[0] + '-****-' + parts[2];
}
return number;
}
@@ -20,9 +20,7 @@
var combo;
function formatNoticeType(cellvalue, options, rowObject) {
if (!combo || !combo.noticeTypeList) {
return cellvalue;
}
var name = "";
for (var i = 0; i < combo.noticeTypeList.length; i++) {
if (combo.noticeTypeList[i].CODE == cellvalue) {
return combo.noticeTypeList[i].NAME;
@@ -49,37 +47,6 @@
return cellvalue + icon;
}
function deleteSelectedRows() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
if (selectedIds.length === 0) {
alert('항목을 선택하세요.');
return;
}
if (!confirm('선택된 ' + selectedIds.length + '개 항목을 삭제하시겠습니까?')) {
return;
}
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {
cmd: 'DELETE_BULK',
ids: selectedIds.join(',')
},
success: function(data) {
alert('삭제되었습니다.');
$("#grid").jqGrid('clearGridData', true);
$("#grid").trigger("reloadGrid");
},
error: function(e) {
alert('삭제 중 오류가 발생했습니다.');
}
});
}
function init(){
$.ajax({
@@ -93,9 +60,6 @@
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);
@@ -103,7 +67,7 @@
});
}
function buildGrid(){
$(document).ready(function() {
$('#grid').jqGrid({
datatype:"json",
@@ -139,8 +103,6 @@
autowidth: true,
viewrecords: true,
rowList : eval('[${rmsDefaultRowList}]'),
multiselect: true,
multiboxonly: true,
ondblClickRow: function(rowId) {
var rowData = $(this).getRowData(rowId);
var id = rowData['id'];
@@ -177,14 +139,10 @@
}
});
resizeJqGridWidth('grid','content_middle','1000');
}
$(document).ready(function() {
// 콤보 로드 → 콤보 성공 콜백에서 buildGrid() 호출 (경합 방지)
init();
resizeJqGridWidth('grid','content_middle','1000');
$("#btn_search").click(function(){
var postData = getSearchForJqgrid("cmd","LIST");
$("#grid").setGridParam({ url:url,postData: postData ,page:1 }).trigger("reloadGrid");
@@ -199,10 +157,6 @@
goNav(url2);
});
$("#btn_delete_selected").click(function(){
deleteSelectedRows();
});
$("select[name=searchUseYn], input[name^=search]").keydown(function(key){
if (key.keyCode == 13){
$("#btn_search").click();
@@ -225,7 +179,6 @@
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> <%= localeMessage.getString("button.new") %></button>
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
</div>
<div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div>
<form id="ajaxForm" onsubmit="return false;">
@@ -26,37 +26,6 @@ function formatFile(cellvalue, options, rowObject) {
return cellvalue + icon;
}
function deleteSelectedRows() {
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
if (selectedIds.length === 0) {
alert('항목을 선택하세요.');
return;
}
if (!confirm('선택된 ' + selectedIds.length + '개 항목을 삭제하시겠습니까?')) {
return;
}
$.ajax({
type: "POST",
url: url,
dataType: "json",
data: {
cmd: 'DELETE_BULK',
ids: selectedIds.join(',')
},
success: function(data) {
alert('삭제되었습니다.');
$("#grid").jqGrid('clearGridData', true);
$("#grid").trigger("reloadGrid");
},
error: function(e) {
alert('삭제 중 오류가 발생했습니다.');
}
});
}
$(document).ready(function() {
$('#grid').jqGrid({
datatype:"json",
@@ -85,8 +54,6 @@ $(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'];
@@ -131,10 +98,6 @@ $(document).ready(function() {
$("#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();
@@ -157,9 +120,8 @@ $(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,27 +92,6 @@ $(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("마스킹 해제 사유를 입력하세요:");
@@ -155,10 +134,9 @@ $(document).ready(function() {
<div class="content_middle">
<div class="search_wrap">
<button type="button" class="cssbtn" id="btn_unmask" level="W" status="DETAIL"><i class="material-icons">lock_open</i> 마스킹해제</button>
<button type="button" class="cssbtn" id="btn_delete" level="W" status="DETAIL" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 삭제</button>
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
</div>
<div class="title">피드백/개선요청 신청 상세</div>
<div class="title">사업제휴 신청 상세</div>
<table id="grid" ></table>
<div id="pager"></div>
@@ -222,7 +222,7 @@
$("input[name=searchStartDate], input[name=searchEndDate]").datepicker();
var today = getToday();
var startDate = today.substring(0,6)+"01";
var startDate = today;
var endDate = today;
@@ -234,7 +234,6 @@
}
list();
search();
resizeJqGridWidth('grid', 'content_middle', '1200');
@@ -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.StdJDBCDelegate");
quartzProperties.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate");
quartzProperties.put("org.quartz.jobStore.useProperties", "false");
quartzProperties.put("org.quartz.jobStore.dataSource", monitoringSchema);
quartzProperties.put("org.quartz.jobStore.tablePrefix", monitoringSchema + ".QRTZ_");
@@ -15,6 +15,4 @@ public interface EMSScheduler {
void deleteAndAddJob(Scheduler scheduler, String jobName) throws SchedulerException, ClassNotFoundException;
void reloadJob(String jobName) throws SchedulerException, ClassNotFoundException;
}
@@ -120,50 +120,12 @@ public class EMSSchedulerImpl implements EMSScheduler {
}
}
@Override
public void reloadJob(String jobName) throws SchedulerException, ClassNotFoundException {
JobInfo jobInfo = jobInfoService.getById(jobName);
if (CLUSTERED.equals(jobInfo.getInstanceName())) {
deleteJob(scheduler, jobName);
reloadClusteredJob(jobInfo);
} else {
deleteAndAddJob(scheduler, jobInfo);
deleteJob(clusteredScheduler, jobName);
}
}
private void reloadClusteredJob(JobInfo jobInfo) throws SchedulerException, ClassNotFoundException {
String jobId = jobInfo.getId();
TriggerKey triggerKey = new TriggerKey(getTriggerName(jobId), Scheduler.DEFAULT_GROUP);
JobKey jobKey = new JobKey(jobId, Scheduler.DEFAULT_GROUP);
Trigger existing = clusteredScheduler.getTrigger(triggerKey);
JobDetail existingJob = clusteredScheduler.getJobDetail(jobKey);
if (existing != null && existingJob != null) {
CronTrigger newTrigger = newTrigger()
.withIdentity(triggerKey)
.withSchedule(cronSchedule(jobInfo.getCronExp()))
.build();
Date ft = clusteredScheduler.rescheduleJob(triggerKey, newTrigger);
if (ft != null) {
return;
}
log.warn("[reloadClusteredJob] {} rescheduleJob returned null, falling back to deleteAndAddJob", jobId);
} else if (existing != null) {
// orphaned trigger (job missing) — clean up before re-adding
log.warn("[reloadClusteredJob] {} has orphaned trigger without job, unscheduling", jobId);
clusteredScheduler.unscheduleJob(triggerKey);
}
deleteAndAddJob(clusteredScheduler, jobInfo);
}
public void deleteJob(Scheduler inScheduler, String jobNm) throws SchedulerException {
String schedulerType = (inScheduler == clusteredScheduler) ? "clusteredScheduler" : "scheduler";
JobKey key = new JobKey(jobNm, Scheduler.DEFAULT_GROUP);
JobDetail jobDetail = inScheduler.getJobDetail(key);
if (jobDetail != null) {
inScheduler.deleteJob(key);
log.info(jobNm + " has been scheduled to deleted");
}
}
@@ -189,7 +151,10 @@ public class EMSSchedulerImpl implements EMSScheduler {
.withSchedule(cronSchedule(info.getCronExp()))
.build();
inScheduler.scheduleJob(job, trigger);
Date ft = inScheduler.scheduleJob(job, trigger);
log
.info("{} has been scheduled to run at: {} and repeat based on expression: {}", job.getKey(), ft,
trigger.getCronExpression());
}
}
@@ -45,9 +45,6 @@ public class SchedulerManController extends BaseAnnotationController {
@Qualifier("monitoringSyncAgent")
private MonitoringSyncAgent monitoringSyncAgent;
@Autowired
private EMSScheduler emsScheduler;
@GetMapping(value = "/common/scheduler/schedulerMan.view")
public String viewList() {
return "/common/scheduler/schedulerMan";
@@ -99,11 +96,6 @@ public class SchedulerManController extends BaseAnnotationController {
@PostMapping(value = "/common/scheduler/schedulerMan.json", params = "cmd=UPDATE")
public ResponseEntity<Void> update(JobInfoUI jobInfoUI) {
service.update(jobInfoUI);
try {
emsScheduler.reloadJob(jobInfoUI.getId());
} catch (Exception e) {
logger.error("reloadJob error on current server", e);
}
syncReloadScheduler(jobInfoUI.getId());
return ResponseEntity.ok().build();
}
@@ -123,7 +115,7 @@ public class SchedulerManController extends BaseAnnotationController {
String url = "/monitoring/schedulerReload.do";
try {
ret = monitoringSyncAgent.targetSync(url, paramMap);
ret = monitoringSyncAgent.sync(url, paramMap);
} catch (Exception e) {
logger.error("syncReloadScheduler error", e);
} finally {
@@ -13,16 +13,10 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import java.util.List;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, String, PortalNoticeRepository> {
public void deleteByIds(List<String> ids) {
repository.deleteAllById(ids);
}
public Page<PortalNotice> findAll(Pageable pageable, PortalNoticeUISearch portalNoticeUISearch) {
QPortalNotice qPortalNotice = QPortalNotice.portalNotice;
@@ -10,16 +10,10 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class PortalPartnershipService extends AbstractEMSDataSerivce<PartnershipApplication, String, PortalPartnershipRepository> {
public void deleteByIds(List<String> ids) {
repository.deleteAllById(ids);
}
public Page<PartnershipApplication> findAll(Pageable pageable, String searchBizSubject, String searchBizDetail) {
QPartnershipApplication qPartnershipApplication = QPartnershipApplication.partnershipApplication;
@@ -77,10 +77,6 @@ public class PortalUserService extends AbstractEMSDataSerivce<PortalUser, String
}
public PortalUser findByUserId(String userId) {
if (StringUtils.isBlank(userId)) {
return null;
}
QPortalUser qPortalUser = QPortalUser.portalUser;
return getJPAQueryFactory()
@@ -39,10 +39,11 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
+ " FROM ("
+ " SELECT A.EAISVCNAME"
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
+ " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X"
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID "
+ " AND SYSTIMESTAMP BETWEEN STARTED_AT AND END_AT) "
+ " AND A.EAISVCNAME = X.API_ID),'N') AS CTRL_YN "
//TODO: 게시판 임시로 생성하여 사용. 유차장이 완료하면 변경할것
+ " , NVL(( SELECT 'Y' FROM PTL_NOTICE_API X "
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.PTL_NOTICE Y WHERE X.NOTICE_ID = Y.ID)"
//+ " AND TO_CHAR(SYSDATE, 'YYYYMMDDHH24MI') BETWEEN START_TIME AND END_TIME) "
+ " AND A.EAISVCNAME = X.API_NAME),'N') AS CTRL_YN "
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
+ " ELSE 'N' END"
@@ -55,8 +55,7 @@ public class ApiStatusService {
apiStatus.setEaisvcname(event.getEaisvcname());
apiStatus.setStatusCode(newStatusCode);
apiStatusRepository.saveAndFlush(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT (즉시 flush)
apiStatusRepository.save(apiStatus); // PK 있으면 UPDATE, 없으면 INSERT
log.debug("API 상태 변경: {}-{} {} → {}", event.getEaisvcname(), event.getEaisvcdesc(), event.getEvent(), newStatusCode);
@@ -1,9 +1,6 @@
package com.eactive.eai.rms.ext.djb.job;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.quartz.DisallowConcurrentExecution;
@@ -71,24 +68,10 @@ public class ApiStatusMonitorJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
// execute.instances 가 지정된 경우 해당 인스턴스에서만 실행
String allowedInstances = jobDataMap.getString("execute.instances");
if (StringUtils.isNotEmpty(allowedInstances)) {
Set<String> instanceSet = Arrays.stream(allowedInstances.split(","))
.map(s -> s.trim().toLowerCase())
.collect(Collectors.toSet());
String currentInstance = StringUtils.defaultString(System.getProperty("inst.Name")).trim().toLowerCase();
if (!instanceSet.contains(currentInstance)) {
log.debug("Job 실행 인스턴스 제한 - 현재: {}, 허용: {}", currentInstance, allowedInstances);
return;
}
}
log.info("*** START ApiStatusUpdateJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
// Job 파라미터 로깅
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
logJobParameters(jobDataMap);
HashMap<String, String> param = this.checkParameters(jobDataMap);
@@ -1,12 +1,8 @@
package com.eactive.eai.rms.ext.djb.job;
import java.util.Arrays;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
@@ -52,21 +48,6 @@ public class InflowTokenMonitorJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
// execute.instances 가 지정된 경우 해당 인스턴스에서만 실행
String allowedInstances = jobDataMap.getString("execute.instances");
if (StringUtils.isNotEmpty(allowedInstances)) {
Set<String> instanceSet = Arrays.stream(allowedInstances.split(","))
.map(s -> s.trim().toLowerCase())
.collect(Collectors.toSet());
String currentInstance = StringUtils.defaultString(System.getProperty("inst.Name")).trim().toLowerCase();
if (!instanceSet.contains(currentInstance)) {
log.debug("Job 실행 인스턴스 제한 - 현재: {}, 허용: {}", currentInstance, allowedInstances);
return;
}
}
log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
long execMinute = 1; //default 배치 실행주기(분)
@@ -5,20 +5,14 @@ import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.onl.apim.portalInquiry.PortalInquiryClosingService;
import com.eactive.eai.rms.onl.common.util.DateUtil;
import org.apache.commons.lang3.StringUtils;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 매일 00:05 실행 - RESPONDED 상태 문의글 자동 종료 배치
*
@@ -50,21 +44,6 @@ public class PortalInquiryClosingJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
// execute.instances 가 지정된 경우 해당 인스턴스에서만 실행
String allowedInstances = jobDataMap.getString("execute.instances");
if (StringUtils.isNotEmpty(allowedInstances)) {
Set<String> instanceSet = Arrays.stream(allowedInstances.split(","))
.map(s -> s.trim().toLowerCase())
.collect(Collectors.toSet());
String currentInstance = StringUtils.defaultString(System.getProperty("inst.Name")).trim().toLowerCase();
if (!instanceSet.contains(currentInstance)) {
log.debug("Job 실행 인스턴스 제한 - 현재: {}, 허용: {}", currentInstance, allowedInstances);
return;
}
}
log.info("*** START PortalInquiryCommentClosingService run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
int closingDays = DEFAULT_CLOSING_DAYS;
@@ -85,7 +85,7 @@ public class ApiUseStatsService
+ " ORDER BY ORGNAME";
} else if ("API".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", A.API_NAME"
+ ", C.EAISVCDESC"
+ ", 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 A.API_NAME"
+ " ORDER BY API_NAME";
+ " GROUP BY C.EAISVCDESC"
+ " ORDER BY EAISVCDESC";
} else if ("DATE".equals(search.getSearchType())) {
dataSql = "SELECT COUNT(*) OVER () AS RECORDS "
+ ", TO_CHAR(STAT_TIME,'YYYY-MM-DD') AS STAT_TIME "
@@ -267,14 +267,17 @@ public class SmsAuthService {
}
/**
* 전화번호 마스킹 (표준 규칙: 2·3번째 세그먼트 각각 뒤 2자리)
* 예: 010-1234-5678 → 010-12**-56**
* 전화번호 마스킹
*/
public String maskPhoneNumber(String phone) {
if (StringUtils.isEmpty(phone)) {
return "****";
}
return com.eactive.eai.common.util.MaskingUtils.maskPhoneNumber(phone);
String cleaned = phone.replaceAll("[^0-9]", "");
if (cleaned.length() < 7) {
return "****";
}
return cleaned.substring(0, 3) + "****" + cleaned.substring(cleaned.length() - 4);
}
/**
@@ -134,8 +134,8 @@ public class SsoController implements InterceptorSkipController {
log.debug("SSO 사용자 정보 - userId: {}, name: {}, phone: {}, email: {}",
userId,
MaskingUtils.maskName(name),
MaskingUtils.maskPhoneNumber(cellPhoneNo),
MaskingUtils.maskEmailId(email));
maskPhoneNumber(cellPhoneNo),
maskEmail(email));
// 사용자 생성 또는 업데이트
UserInfo userInfo = findOrCreateUser(info, extendedInfo);
@@ -341,4 +341,33 @@ public class SsoController implements InterceptorSkipController {
}
return phone.replaceAll("[^0-9]", "");
}
/**
* 휴대폰번호 마스킹 (로그용)
*/
private String maskPhoneNumber(String phone) {
if (StringUtils.isEmpty(phone) || phone.length() < 4) {
return "****";
}
// 010-1234-5678 → 010-****-5678
String cleaned = phone.replaceAll("[^0-9]", "");
if (cleaned.length() < 7) {
return "****";
}
return cleaned.substring(0, 3) + "****" + cleaned.substring(cleaned.length() - 4);
}
/**
* 이메일 마스킹 (로그용)
*/
private String maskEmail(String email) {
if (StringUtils.isEmpty(email) || !email.contains("@")) {
return "****";
}
int atIndex = email.indexOf("@");
if (atIndex <= 2) {
return "**" + email.substring(atIndex);
}
return email.substring(0, 2) + "**" + email.substring(atIndex);
}
}
@@ -1,52 +1,83 @@
package com.eactive.eai.rms.onl.apim.masking;
/**
* 개인정보 마스킹 유틸리티 (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>
*/
import com.eactive.eai.rms.common.util.StringUtils;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MaskingUtils {
private MaskingUtils() {}
/**
* 이메일 주소의 ID 부분을 마스킹 처리
* 예: aajjbacc@gmail.com => **jjba**@gmail.com
* 예: eactive@gmail.com => eact***@gmail.com
*/
public static String maskEmailId(String email) {
return com.eactive.eai.common.util.MaskingUtils.maskEmail(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;
}
/**
* 이름 마스킹 처리 (2자: 뒤 1자리, 3자 이상: 앞뒤 제외 가운데 전체)
* 예: 홍길동 => 홍*동
* 이름의 마지막 글자를 마스킹 처리
*/
public static String maskName(String name) {
return com.eactive.eai.common.util.MaskingUtils.maskKoreanName(name);
if (name == null || name.isEmpty() || name.length() < 2) {
return name;
}
return name.substring(0, name.length() - 1) + "*";
}
/**
* 전화번호/휴대폰/FAX 마스킹 처리 (2·3번째 세그먼트 각각 뒤 2자리)
* 예: 010-1234-5678 => 010-12**-56**
* 전화번호 마스킹 처리
* 예: 010-1234-5678 => 010-****-5678
*/
public static String maskPhoneNumber(String phoneNumber) {
return com.eactive.eai.common.util.MaskingUtils.maskPhoneNumber(phoneNumber);
if (phoneNumber == null || phoneNumber.isEmpty()) {
return phoneNumber;
}
String[] parts = phoneNumber.split("-");
if (parts.length != 3) {
return phoneNumber;
}
return parts[0] + "-****-" + parts[2];
}
/**
* IP 주소 마스킹 처리 (IPv4 3번째 옥텟 / IPv6 마지막 그룹, 콤마 다중 IP 지원)
* 예: 192.168.1.1 => 192.168.***.1
* IP 주소 마스킹 처리
* 예: 192.168.1.1 => 192.168.*.*
*/
public static String maskIpAddress(String ipAddresses) {
return com.eactive.eai.common.util.MaskingUtils.maskIpAddress(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);
}
}
@@ -80,10 +80,4 @@ public class PortalNoticeManController extends BaseAnnotationController {
return ResponseEntity.ok().build();
}
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=DELETE_BULK")
public ResponseEntity<String> deleteBulk(String ids) {
portalNoticeManService.deleteBulk(ids);
return ResponseEntity.ok().build();
}
}
@@ -33,7 +33,6 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -283,20 +282,4 @@ public class PortalNoticeManService extends BaseService {
portalNoticeService.deleteById(id);
}
public void deleteBulk(String ids) {
if (StringUtils.isBlank(ids)) {
return;
}
List<String> idList = Arrays.asList(ids.split(","));
// 첨부파일 정리 (단건 삭제와 동일한 처리)
idList.forEach(id -> {
PortalNotice portalNotice = portalNoticeService.getById(id);
Optional.ofNullable(portalNotice.getFileId()).ifPresent(fileService::deleteFile);
});
portalNoticeService.deleteByIds(idList);
}
}
@@ -55,16 +55,4 @@ public class PortalPartnershipManController extends BaseAnnotationController {
return ResponseEntity.ok(portalPartnershipUI);
}
@PostMapping(value = "/onl/apim/portalpartnership/portalPartnershipMan.json", params = "cmd=DELETE")
public ResponseEntity<Void> delete(String id) {
portalPartnershipManService.delete(id);
return ResponseEntity.ok().build();
}
@PostMapping(value = "/onl/apim/portalpartnership/portalPartnershipMan.json", params = "cmd=DELETE_BULK")
public ResponseEntity<Void> deleteBulk(String ids) {
portalPartnershipManService.deleteBulk(ids);
return ResponseEntity.ok().build();
}
}
@@ -15,10 +15,6 @@ 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 {
@@ -55,48 +51,18 @@ 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(userName));
partnershipUI.setCreatedById(MaskingUtils.maskEmailId(loginId));
partnershipUI.setCreatedByName(MaskingUtils.maskName(user.getUserName()));
partnershipUI.setCreatedById(MaskingUtils.maskEmailId(user.getLoginId()));
} else {
// 마스킹 해제 상태
partnershipUI.setCreatedByName(userName);
partnershipUI.setCreatedById(loginId);
partnershipUI.setCreatedByName(user.getUserName());
partnershipUI.setCreatedById(user.getLoginId());
}
// 첨부파일 처리
@@ -1,10 +1,10 @@
package com.eactive.eai.rms.onl.common.controller;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
import com.eactive.eai.rms.common.scheduler.EMSScheduler;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterJobInfo;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterSchedulerUtil;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.SpcfTmAdapterDao;
import org.quartz.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,11 +15,9 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
import com.eactive.eai.rms.common.scheduler.EMSScheduler;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterJobInfo;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterSchedulerUtil;
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.SpcfTmAdapterDao;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Controller
public class SchedulerReloadController implements InterceptorSkipController {
@@ -39,11 +37,16 @@ public class SchedulerReloadController implements InterceptorSkipController {
@Qualifier("scheduler")
private Scheduler scheduler;
@Autowired
@Qualifier("clusteredScheduler")
private Scheduler clusteredScheduler;
@RequestMapping("/schedulerReload.do")
public ResponseEntity<Map<String, Object>> rmsSchedulerReload(String jobName) {
Map<String, Object> result = new HashMap<>();
try {
emsScheduler.reloadJob(jobName);
emsScheduler.deleteAndAddJob(scheduler, jobName);
emsScheduler.deleteAndAddJob(clusteredScheduler, jobName);
result.put("status", "success");
} catch (Exception e) {
logger.error("", e);
@@ -1,128 +0,0 @@
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(""));
}
}