diff --git a/WebContent/WEB-INF/auditpoints.dat b/WebContent/WEB-INF/auditpoints.dat index 8c4f211..6b048d5 100644 --- a/WebContent/WEB-INF/auditpoints.dat +++ b/WebContent/WEB-INF/auditpoints.dat @@ -14,4 +14,5 @@ ApiSpecController_API스펙관리_APIGW_INSERT,DELETE MessageTemplateManController_메세지템플릿관리_APIGW_INSERT,UPDATE,DELETE PortalPartnershipManController_피드백/개선요청관리_APIGW_UNMASK,DELETE PortalPropertyController_포탈 프로퍼티 관리_APIGW_UPDATE -PortalUserTermsManController_약관동의 이력_APIGW_UNMASK \ No newline at end of file +PortalUserTermsManController_약관동의 이력_APIGW_UNMASK +MessageRequestManController_메세지발송내역_APIGW_UPDATE_STATUS diff --git a/WebContent/jsp/onl/apim/messagerequest/messageRequestMan.jsp b/WebContent/jsp/onl/apim/messagerequest/messageRequestMan.jsp new file mode 100644 index 0000000..73516a7 --- /dev/null +++ b/WebContent/jsp/onl/apim/messagerequest/messageRequestMan.jsp @@ -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"); +%> + + + + + + + + + + +
+ +
+
+ +
+
메세지 발송 내역발송 요청 이력을 조회합니다. 개인정보는 마스킹되어 표시됩니다.
+
+ + + + + + + + + + + + + + + + + + + + + + +
상태
메세지 코드
수신자명
유형 +
+ +
+
요청기간 + ~ +
+
+
+
+
+
+ + diff --git a/WebContent/jsp/onl/apim/messagerequest/messageRequestManDetail.jsp b/WebContent/jsp/onl/apim/messagerequest/messageRequestManDetail.jsp new file mode 100644 index 0000000..f93e3b5 --- /dev/null +++ b/WebContent/jsp/onl/apim/messagerequest/messageRequestManDetail.jsp @@ -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"); +%> + + + + + + + + + + +
+ +
+
+ + +
+
메세지 발송 내역 상세개인정보는 마스킹되어 표시됩니다.
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
메세지 코드
제목
유형상태
수신자명메신저 ID
이메일휴대폰
요청 시간발송 시간
EAI 인터페이스 ID서비스 ID
UMS UID
메세지 내용
+
+
+ + diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestEmsRepository.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestEmsRepository.java new file mode 100644 index 0000000..0c4eff3 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestEmsRepository.java @@ -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 전용 리포지토리. + * + *

elink-portal-common 의 {@code template.repository.MessageRequestRepository}(JpaRepository) 와 + * 단순 클래스명이 겹치면 Spring Data 빈 이름이 충돌하므로(둘 다 admin 컨텍스트에 스캔됨) + * 이름을 분리한다. 또한 QueryDSL {@code findAll(predicate, pageable)} 를 쓰기 위해 + * {@link BaseRepository} 를 상속한다.

+ */ +@EMSDataSource +public interface MessageRequestEmsRepository extends BaseRepository { +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestService.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestService.java new file mode 100644 index 0000000..ab73dca --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestService.java @@ -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 { + + public Page 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); + } +} diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestUISearch.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestUISearch.java new file mode 100644 index 0000000..8c09973 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/apim/messagerequest/MessageRequestUISearch.java @@ -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; +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtils.java b/src/main/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtils.java new file mode 100644 index 0000000..5ed8049 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtils.java @@ -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 패턴 기반 마스킹 유틸 (발송 내역 화면 전용). + * + *

본문(message) 안에 섞여 있는 개인정보/인증정보를 패턴으로 찾아 마스킹하고, + * 패턴 종류별로 색상 강조 {@code } 으로 감싼 안전한 HTML 을 만든다. + * 매칭되지 않은 영역과 마스킹 결과 모두 HTML escape 하므로 본문에 스크립트가 있어도 무력화된다.

+ * + *
+ *   이메일            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자리)
+ * 
+ * + *

실제 이메일/휴대폰 마스킹 규칙은 표준 구현 + * {@link com.eactive.eai.common.util.MaskingUtils}(elink-online-core)에 위임한다.

+ */ +public final class MessagePatternMaskingUtils { + + private MessagePatternMaskingUtils() {} + + private static final int DIGITS_KEEP_HEAD = 3; + + /** + * 매칭 우선순위(이메일 > 하이픈 휴대폰 > 6자리+ 숫자)를 단일 패스로 보장하는 결합 정규식. + * 휴대폰 하이픈 형식이 "6자리 이상 숫자" 규칙에 이중 적용되지 않도록 한 Matcher 로 순회한다. + */ + private static final Pattern COMBINED = Pattern.compile( + "(?[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})" + + "|(?01[016789]-\\d{3,4}-\\d{4})" + + "|(?\\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 "" + escapeHtml(inner) + ""; + } + + /** + * 최소 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); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManController.java b/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManController.java new file mode 100644 index 0000000..22234d2 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManController.java @@ -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) 조회 화면 컨트롤러. + * + *

조회 전용 + PENDING→FAILED 상태 전환만 제공한다. (등록/수정/삭제 없음)

+ */ +@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> selectList( + @SortDefault(sort = "requestDate", direction = Sort.Direction.DESC) Pageable pageable, + MessageRequestUISearch search) { + Page 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> initCombo() { + Map 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 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 updateStatus(String id) { + messageRequestManService.updateStatusToFailed(id); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManService.java b/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManService.java new file mode 100644 index 0000000..e78fab6 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestManService.java @@ -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 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 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 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()); + } +} diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestUI.java b/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestUI.java new file mode 100644 index 0000000..3762170 --- /dev/null +++ b/src/main/java/com/eactive/eai/rms/onl/apim/messagerequest/MessageRequestUI.java @@ -0,0 +1,42 @@ +package com.eactive.eai.rms.onl.apim.messagerequest; + +import lombok.Data; + +/** + * 발송 내역(PTL_MESSAGE_REQUEST) 화면 응답 DTO. + * + *

개인정보는 모두 마스킹된 값으로 채워지며, 본문(messageHtml)은 패턴 마스킹 + 색상강조가 적용된 + * 안전한 HTML 이다. 날짜는 표시용(short)/툴팁용(full) 두 가지 포맷 문자열로 제공한다.

+ */ +@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; +} diff --git a/src/test/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtilsTest.java b/src/test/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtilsTest.java new file mode 100644 index 0000000..81d5b3f --- /dev/null +++ b/src/test/java/com/eactive/eai/rms/onl/apim/masking/MessagePatternMaskingUtilsTest.java @@ -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("**st1**@test.com", + MessagePatternMaskingUtils.maskAndHighlight("test123@test.com")); + } + + // --- 휴대폰(하이픈) --- + + @Test + @DisplayName("본문 휴대폰(하이픈) — mask-phone span + 세그먼트별 뒤2자리 마스킹") + void maskAndHighlight_phoneHyphen() { + assertEquals("010-12**-56**", + MessagePatternMaskingUtils.maskAndHighlight("010-1234-5678")); + } + + // --- 6자리 이상 숫자 (앞 3자리 제외 전체 마스킹) --- + + @Test + @DisplayName("본문 6자리 숫자 — 앞 3자리 제외 전체 마스킹(mask-digits)") + void maskAndHighlight_digits6() { + assertEquals("123***", + MessagePatternMaskingUtils.maskAndHighlight("123456")); + } + + @Test + @DisplayName("본문 하이픈 없는 휴대폰(11자리) — 6자리+ 숫자 규칙 적용") + void maskAndHighlight_phoneNoHyphen() { + assertEquals("010********", + 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("**st1**@test.com")); + assertTrue(result.contains("010-12**-56**")); + assertTrue(result.contains("123***")); + 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("hi")); + } + + @Test + @DisplayName("script 태그 escape") + void maskAndHighlight_escapesScript() { + String result = MessagePatternMaskingUtils.maskAndHighlight(""); + assertFalse(result.contains("