- 본문·수신자 마스킹 유틸(MessagePatternMaskingUtils) 개발 - 안전 HTML 생성 마스킹/검증 테스트 및 JSP 바인딩
This commit is contained in:
@@ -14,4 +14,5 @@ ApiSpecController_API스펙관리_APIGW_INSERT,DELETE
|
||||
MessageTemplateManController_메세지템플릿관리_APIGW_INSERT,UPDATE,DELETE
|
||||
PortalPartnershipManController_피드백/개선요청관리_APIGW_UNMASK,DELETE
|
||||
PortalPropertyController_포탈 프로퍼티 관리_APIGW_UPDATE
|
||||
PortalUserTermsManController_약관동의 이력_APIGW_UNMASK
|
||||
PortalUserTermsManController_약관동의 이력_APIGW_UNMASK
|
||||
MessageRequestManController_메세지발송내역_APIGW_UPDATE_STATUS
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<style>
|
||||
.mask-email { color:#1a73e8; font-weight:bold; }
|
||||
.mask-phone { color:#d93025; font-weight:bold; }
|
||||
.mask-digits { color:#9334e6; font-weight:bold; }
|
||||
.msg-cell { display:inline-block; max-width:380px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; vertical-align:middle; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/messagerequest/messageRequestMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/messagerequest/messageRequestMan.view" />';
|
||||
|
||||
function escapeHtmlJs(s) { return $('<div>').text(s == null ? '' : s).html(); }
|
||||
function escapeAttr(s) { return escapeHtmlJs(s).replace(/"/g, '"'); }
|
||||
|
||||
// 제목: 없으면 messageCode, 마우스오버에 코드명(코드) 표기
|
||||
function formatSubject(cellvalue, options, rowObject) {
|
||||
var code = rowObject.messageCode || '';
|
||||
var subject = (cellvalue && cellvalue !== '') ? cellvalue : '';
|
||||
var title = rowObject.messageCodeName ? (rowObject.messageCodeName + ' (' + code + ')') : code;
|
||||
if (subject) {
|
||||
// 제목이 있으면 2줄: 제목 + 하단에 메세지코드
|
||||
var codeLine = code ? '<br><span style="color:#888;font-size:0.85em;">' + escapeHtmlJs(code) + '</span>' : '';
|
||||
return '<span title="' + escapeAttr(title) + '">' + escapeHtmlJs(subject) + codeLine + '</span>';
|
||||
}
|
||||
// 제목이 없으면 메세지코드만
|
||||
return '<span title="' + escapeAttr(title) + '">' + escapeHtmlJs(code) + '</span>';
|
||||
}
|
||||
|
||||
// 본문: 서버에서 마스킹+escape 된 안전 HTML 그대로 렌더
|
||||
function formatMessage(cellvalue, options, rowObject) {
|
||||
return '<span class="msg-cell">' + (rowObject.messageHtml || '') + '</span>';
|
||||
}
|
||||
|
||||
// 수신자 정보: 휴대폰 / 이메일 / 메신저ID (모두 마스킹됨)
|
||||
function formatReceiverInfo(cellvalue, options, rowObject) {
|
||||
var parts = [];
|
||||
if (rowObject.phone) parts.push(escapeHtmlJs(rowObject.phone));
|
||||
if (rowObject.email) parts.push(escapeHtmlJs(rowObject.email));
|
||||
if (rowObject.messengerId) parts.push(escapeHtmlJs(rowObject.messengerId));
|
||||
return parts.join('<br>');
|
||||
}
|
||||
|
||||
function formatRequestDate(cellvalue, options, rowObject) {
|
||||
return '<span title="' + escapeAttr(rowObject.requestDateFull || '') + '">' + escapeHtmlJs(rowObject.requestDateShort || '') + '</span>';
|
||||
}
|
||||
function formatSentDate(cellvalue, options, rowObject) {
|
||||
return '<span title="' + escapeAttr(rowObject.sentDateFull || '') + '">' + escapeHtmlJs(rowObject.sentDateShort || '') + '</span>';
|
||||
}
|
||||
|
||||
function init() {
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'LIST_INIT_COMBO'},
|
||||
success: function (json) {
|
||||
new makeOptions("CODE", "NAME").setObj($("select[name=searchRequestStatus]"))
|
||||
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
|
||||
.setData(json.statusList).rendering();
|
||||
new makeOptions("CODE", "NAME").setObj($("select[name=searchMessageCode]"))
|
||||
.setNoValueInclude(true).setNoValue("", "<%=localeMessage.getString("combo.all")%>")
|
||||
.setData(json.messageCodeList).rendering();
|
||||
|
||||
putSelectFromParam();
|
||||
|
||||
// 콤보 로드 후 그리드 생성 (경합 방지)
|
||||
buildGrid();
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
function buildGrid() {
|
||||
$('#grid').jqGrid({
|
||||
datatype: "json", mtype: 'POST', url: url,
|
||||
postData: getSearchForJqgrid("cmd", "LIST"),
|
||||
colNames: ['id', 'No.', '제목', '메세지 내용', '수신자명', '수신자 정보', '유형', '상태', '요청시간', '발송시간'],
|
||||
colModel: [
|
||||
{name: 'id', align: 'center', key: true, hidden: true},
|
||||
{name: 'rowNum', align: 'center', width: 45, sortable: false},
|
||||
{name: 'subject', align: 'left', width: 160, formatter: formatSubject},
|
||||
{name: 'messageHtml', align: 'left', width: 300, formatter: formatMessage},
|
||||
{name: 'username', align: 'center', width: 80},
|
||||
{name: 'receiverInfo', align: 'left', width: 150, formatter: formatReceiverInfo},
|
||||
{name: 'messageType', align: 'center', width: 60},
|
||||
{name: 'requestStatus', align: 'center', width: 70},
|
||||
{name: 'requestDateShort', align: 'center', width: 110, formatter: formatRequestDate},
|
||||
{name: 'sentDateShort', align: 'center', width: 110, formatter: formatSentDate}
|
||||
],
|
||||
jsonReader: {repeatitems: false},
|
||||
pager: $('#pager'),
|
||||
page: '${param.page}',
|
||||
rowNum: '${rmsDefaultRowNum}',
|
||||
autoheight: true,
|
||||
height: $("#container").height(),
|
||||
autowidth: true,
|
||||
viewrecords: true,
|
||||
rowList: eval('[${rmsDefaultRowList}]'),
|
||||
ondblClickRow: function (rowId) {
|
||||
var rowData = $(this).getRowData(rowId);
|
||||
var id = rowData['id'];
|
||||
var url2 = url_view + '?cmd=DETAIL';
|
||||
url2 += '&page=' + $(this).getGridParam("page");
|
||||
url2 += '&returnUrl=' + getReturnUrl();
|
||||
url2 += '&menuId=' + '${param.menuId}';
|
||||
url2 += '&id=' + id;
|
||||
goNav(url2);
|
||||
},
|
||||
loadComplete: function () {
|
||||
var page = $(this).getGridParam('page');
|
||||
var rowNum = $(this).getGridParam('rowNum');
|
||||
var rows = $(this).getDataIDs();
|
||||
var colModel = $(this).getGridParam("colModel");
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var number = ((page - 1) * rowNum + i) + 1;
|
||||
$(this).setCell(rows[i], 'rowNum', number);
|
||||
}
|
||||
// 서버 고정 정렬(요청일 역순) — 컬럼 정렬 비활성
|
||||
for (var i = 0; i < colModel.length; i++) {
|
||||
$(this).setColProp(colModel[i].name, {sortable: false});
|
||||
}
|
||||
},
|
||||
loadError: function (jqXHR, textStatus, errorThrown) {
|
||||
var location = '<%=request.getContextPath()%>/';
|
||||
comloadError(jqXHR, textStatus, errorThrown, location);
|
||||
}
|
||||
});
|
||||
resizeJqGridWidth('grid', 'content_middle', '1000');
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
init();
|
||||
|
||||
$("#btn_search").click(function () {
|
||||
var postData = getSearchForJqgrid("cmd", "LIST");
|
||||
$("#grid").setGridParam({url: url, postData: postData, page: 1}).trigger("reloadGrid");
|
||||
});
|
||||
|
||||
$("select[name^=search]").change(function () { $("#btn_search").click(); });
|
||||
$("input[name^=search]").keydown(function (key) {
|
||||
if (key.keyCode == 13) { $("#btn_search").click(); }
|
||||
});
|
||||
|
||||
buttonControl();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
|
||||
</div><!-- end content_top -->
|
||||
<div class="content_middle" id="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||
</div>
|
||||
<div class="title">메세지 발송 내역<span class="tooltip">발송 요청 이력을 조회합니다. 개인정보는 마스킹되어 표시됩니다.</span></div>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
<table class="search_condition" cellspacing=0;>
|
||||
<colgroup>
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
<col style="width:120px;"><col style="width:200px;">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>상태</th>
|
||||
<td><div class="select-style"><select name="searchRequestStatus"></select></div></td>
|
||||
<th>메세지 코드</th>
|
||||
<td><div class="select-style"><select name="searchMessageCode"></select></div></td>
|
||||
<th>수신자명</th>
|
||||
<td><input type="text" name="searchRecipient" autocomplete="off"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>유형</th>
|
||||
<td>
|
||||
<div class="select-style">
|
||||
<select name="searchMessageType">
|
||||
<option value="">전체</option>
|
||||
<option value="EMAIL">EMAIL</option>
|
||||
<option value="SMS">SMS</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<th>요청기간</th>
|
||||
<td colspan="3">
|
||||
<input type="date" name="searchFromDate"> ~ <input type="date" name="searchToDate">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<table id="grid"></table>
|
||||
<div id="pager"></div>
|
||||
</div><!-- end content_middle -->
|
||||
</div><!-- end right_box -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,149 @@
|
||||
<%@ page language="java" contentType="text/html; charset=utf-8" %>
|
||||
<%@ page import="java.io.*" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
|
||||
<%@ include file="/jsp/common/include/localemessage.jsp" %>
|
||||
<%
|
||||
response.setHeader("Pragma", "No-cache");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Expires", "0");
|
||||
%>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<jsp:include page="/jsp/common/include/css.jsp"/>
|
||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
||||
<style>
|
||||
.mask-email { color:#1a73e8; font-weight:bold; }
|
||||
.mask-phone { color:#d93025; font-weight:bold; }
|
||||
.mask-digits { color:#9334e6; font-weight:bold; }
|
||||
.msg-detail { white-space:pre-wrap; word-break:break-all; min-height:80px; line-height:1.6; }
|
||||
</style>
|
||||
<script language="javascript">
|
||||
var url = '<c:url value="/onl/apim/messagerequest/messageRequestMan.json" />';
|
||||
var url_view = '<c:url value="/onl/apim/messagerequest/messageRequestMan.view" />';
|
||||
var key = '${param.id}';
|
||||
|
||||
function detail() {
|
||||
if (!key) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'DETAIL', id: key},
|
||||
success: function (data) {
|
||||
var codeText = (data.messageCodeName || '') + (data.messageCode ? ' (' + data.messageCode + ')' : '');
|
||||
$('#messageCodeName').text(codeText);
|
||||
$('#subject').text((data.subject && data.subject !== '') ? data.subject : (data.messageCode || ''));
|
||||
$('#messageType').text(data.messageType || '');
|
||||
$('#requestStatus').text(data.requestStatus || '');
|
||||
$('#username').text(data.username || '');
|
||||
$('#email').text(data.email || '');
|
||||
$('#phone').text(data.phone || '');
|
||||
$('#messengerId').text(data.messengerId || '');
|
||||
$('#umsUid').text(data.umsUid || '');
|
||||
$('#eaiInterfaceId').text(data.eaiInterfaceId || '');
|
||||
$('#serviceId').text(data.serviceId || '');
|
||||
$('#requestDate').text(data.requestDateFull || '');
|
||||
$('#sentDate').text(data.sentDateFull || '');
|
||||
// 서버에서 마스킹+escape 된 안전 HTML
|
||||
$('#messageHtml').html(data.messageHtml || '');
|
||||
|
||||
// PENDING 건만 '실패 처리' 노출 (권한 제어는 buttonControl 이 담당)
|
||||
if (data.requestStatus !== 'PENDING') {
|
||||
$('#btn_fail').hide();
|
||||
}
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
var returnUrl = getReturnUrlForReturn();
|
||||
|
||||
buttonControl();
|
||||
detail();
|
||||
|
||||
$('#btn_fail').click(function () {
|
||||
if (!confirm('이 건을 FAILED(발송 실패) 상태로 변경하시겠습니까?')) return;
|
||||
$.ajax({
|
||||
type: "POST", url: url, dataType: "json",
|
||||
data: {cmd: 'UPDATE_STATUS', id: key},
|
||||
success: function () {
|
||||
alert("변경되었습니다.");
|
||||
detail();
|
||||
},
|
||||
error: function (e) { alert(e.responseText); }
|
||||
});
|
||||
});
|
||||
|
||||
$('#btn_previous').click(function () { goNav(returnUrl); });
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="right_box">
|
||||
<div class="content_top">
|
||||
<ul class="path"><li><a href="#">${rmsMenuPath}</a></li></ul>
|
||||
</div>
|
||||
<div class="content_middle">
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_fail" level="W" status="DETAIL" style="background-color:#dc3545;border-color:#dc3545;color:#fff;"><i class="material-icons">report</i> 실패 처리</button>
|
||||
<button type="button" class="cssbtn" id="btn_previous" level="R" status="DETAIL,NEW"><i class="material-icons">arrow_back</i> <%= localeMessage.getString("button.previous") %></button>
|
||||
</div>
|
||||
<div class="title" id="title">메세지 발송 내역 상세<span class="tooltip">개인정보는 마스킹되어 표시됩니다.</span></div>
|
||||
<table class="table_row" cellspacing="0">
|
||||
<colgroup>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
<col style="width:12%"/><col style="width:38%"/>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>메세지 코드</th>
|
||||
<td colspan="3"><span id="messageCodeName"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>제목</th>
|
||||
<td colspan="3"><span id="subject"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>유형</th>
|
||||
<td><span id="messageType"></span></td>
|
||||
<th>상태</th>
|
||||
<td><span id="requestStatus"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>수신자명</th>
|
||||
<td><span id="username"></span></td>
|
||||
<th>메신저 ID</th>
|
||||
<td><span id="messengerId"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>이메일</th>
|
||||
<td><span id="email"></span></td>
|
||||
<th>휴대폰</th>
|
||||
<td><span id="phone"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>요청 시간</th>
|
||||
<td><span id="requestDate"></span></td>
|
||||
<th>발송 시간</th>
|
||||
<td><span id="sentDate"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>EAI 인터페이스 ID</th>
|
||||
<td><span id="eaiInterfaceId"></span></td>
|
||||
<th>서비스 ID</th>
|
||||
<td><span id="serviceId"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>UMS UID</th>
|
||||
<td colspan="3"><span id="umsUid"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>메세지 내용</th>
|
||||
<td colspan="3"><div id="messageHtml" class="msg-detail"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+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