diff --git a/src/main/java/com/eactive/apim/portal/djb/notitest/NotiTestHistoryEntry.java b/src/main/java/com/eactive/apim/portal/djb/notitest/NotiTestHistoryEntry.java new file mode 100644 index 0000000..f49c10a --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/djb/notitest/NotiTestHistoryEntry.java @@ -0,0 +1,73 @@ +package com.eactive.apim.portal.djb.notitest; + +import com.eactive.apim.portal.djb.notitest.catalog.NotiTestTarget; +import com.eactive.apim.portal.template.entity.MessageRequest; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Getter; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 알림 발송 테스트 이력 1건. HttpSession(속성 {@code NOTI_TEST_HISTORY})에 리스트로 보관한다. + * + *
DB 테이블을 신설하지 않는 요구사항에 따라, 상태는 저장하지 않고 매 폴링마다 + * {@code requestIds}로 PTL_MESSAGE_REQUEST를 다시 조회해 {@link #refreshStatus} 로 갱신한다.
+ */ +@Getter +public class NotiTestHistoryEntry implements Serializable { + private static final long serialVersionUID = 1L; + + private final String entryId; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private final LocalDateTime requestedAt; + private final NotiTestTarget.Category category; + private final String targetLabel; + private final String messageCode; + private final NotiTestTarget.Strategy strategy; + private final String recipient; + private final List그룹 {@code Portal}, 점 구분 소문자 키 관례를 따른다({@link com.eactive.apim.portal.djb.swing.SwingNotifyProperties} 참고). + * {@link PortalPropertyService#getOrCreateProperty} 는 최초 접근 시 기본값으로 DB row 를 생성하므로 + * 별도 초기 데이터 없이도 동작하며, 운영자는 PTL_PROPERTY 값만 바꿔 재배포 없이 on/off·계정 추가가 가능하다.
+ */ +@Component +@RequiredArgsConstructor +public class NotiTestProperties { + + public static final String GROUP = "Portal"; + + public static final String KEY_ENABLED = "notification-test.enabled"; + public static final String KEY_ALLOWED_EMAILS = "notification-test.allowed-emails"; + + private static final String LIST_DELIMITERS = "[,;\\r\\n]"; + + private final PortalPropertyService portalPropertyService; + + public boolean isEnabled() { + String value = resolve(KEY_ENABLED, "false", "알림 발송 테스트 페이지 활성화 여부(true/false)"); + return "true".equalsIgnoreCase(value == null ? null : value.trim()); + } + + public boolean isAllowed(String email) { + if (email == null || email.trim().isEmpty()) { + return false; + } + String allowList = resolve(KEY_ALLOWED_EMAILS, "", + "알림 발송 테스트 페이지 접근 허용 이메일 목록 (콤마/세미콜론/줄바꿈 구분)"); + if (allowList == null || allowList.trim().isEmpty()) { + return false; + } + String target = email.trim(); + for (String token : allowList.split(LIST_DELIMITERS)) { + String candidate = token.trim(); + if (!candidate.isEmpty() && candidate.equalsIgnoreCase(target)) { + return true; + } + } + return false; + } + + private String resolve(String key, String defaultValue, String description) { + return portalPropertyService.getOrCreateProperty(GROUP, key, defaultValue, description); + } +} diff --git a/src/main/java/com/eactive/apim/portal/djb/notitest/NotiTestResult.java b/src/main/java/com/eactive/apim/portal/djb/notitest/NotiTestResult.java new file mode 100644 index 0000000..c98878f --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/djb/notitest/NotiTestResult.java @@ -0,0 +1,35 @@ +package com.eactive.apim.portal.djb.notitest; + +import com.eactive.apim.portal.template.entity.MessageRequest; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link NotiTestService#send} 한 번 호출의 결과. 화면/세션에는 이 값을 그대로 담지 않고 + * {@link NotiTestHistoryEntry}로 변환해서 보관한다(엔티티를 세션에 직렬화하지 않기 위함). + */ +@Getter +public class NotiTestResult { + + private final boolean success; + private final ListHANDLER 전략은 {@code MessageHandlerService.publishEvent}(이벤트버스, fire-and-forget)를 쓰지 않는다 — + * 반환값을 받을 방법이 없기 때문이다. 대신 핸들러의 {@code createEvent}만 재사용해 파라미터 매핑 로직은 + * 그대로 살리고, {@code MessageSendService.sendMessage}를 직접 호출해 생성된 {@link MessageRequest} 목록을 + * 돌려받는다. {@code MessageSendEvent} 구독자는 {@code MessageHandlerService.handleMessageSendEvent} 한 + * 곳뿐이라 이 우회는 안전하다.
+ */ +@Service +@RequiredArgsConstructor +public class NotiTestService { + + private final MessageHandlerService messageHandlerService; + private final MessageSendService messageSendService; + private final SwingMessageWriter swingMessageWriter; + private final MessageTemplateRepository messageTemplateRepository; + + public NotiTestResult send(String messageCodeName, Map발송 경로가 코드 위치에 따라 세 갈래로 갈린다({@link Strategy} 참고). 항목을 추가/변경할 때는 + * 이 enum 하나만 고치면 된다 — 컨트롤러/서비스/화면은 이 카탈로그를 그대로 순회한다.
+ */ +@Getter +public enum NotiTestTarget { + + // ---- 전략 HANDLER: elink-portal-common 의 MessageEventHandler 재사용 ---- + EMAIL_VERIFICATION(Category.EMAIL, "이메일 인증코드", MessageCode.USER_VERIFICATION_EMAIL, + Strategy.HANDLER, params("authNumber", "123456")), + SMS_VERIFICATION(Category.SMS, "SMS 인증코드", MessageCode.USER_VERIFICATION_MOBILEPHONE, + Strategy.HANDLER, params("authNumber", "123456")), + SMS_ACCOUNT_LOCKED(Category.SMS, "계정 잠김", MessageCode.USER_ACCOUNT_LOCKED, + Strategy.HANDLER, params("reason", "로그인 5회 실패")), + SMS_PASSWORD_CHANGED(Category.SMS, "비밀번호 변경", MessageCode.USER_PASSWORD_CHANGED, + Strategy.HANDLER, params()), + SMS_INVITATION(Category.SMS, "법인 내 초대", MessageCode.USER_INVITATION, + Strategy.HANDLER, params( + "corpName", "(주)테스트법인", + "managerName", "홍길동", + "url", "https://example.com/invite", + "authNumber", "123456")), + SMS_INVITATION_CANCELED(Category.SMS, "법인 내 초대 취소", MessageCode.USER_INVITATION_CANCELED, + Strategy.HANDLER, params( + "corpName", "(주)테스트법인", + "managerName", "홍길동")), + + // ---- 전략 DIRECT: eapim-admin 전용 핸들러(클래스패스 미공유) — MessageSendService 직접 호출 ---- + SMS_ORG_APPROVED(Category.SMS, "서비스 가입 승인", MessageCode.MANAGER_WITH_ORG_REGISTER_APPROVED, + Strategy.DIRECT, params("ORG_NAME", "(주)테스트법인")), + SMS_ORG_REJECTED(Category.SMS, "서비스 가입 거절", MessageCode.MANAGER_WITH_ORG_REGISTER_REJECTED, + Strategy.DIRECT, params("ORG_NAME", "(주)테스트법인", "REASON", "서류 미비")), + SMS_APP_APPROVED(Category.SMS, "클라이언트 승인", MessageCode.APP_APPROVE, + Strategy.DIRECT, params("APP_NAME", "테스트앱")), + SMS_APP_REJECTED(Category.SMS, "클라이언트 거절", MessageCode.APP_REJECTED, + Strategy.DIRECT, params("APP_NAME", "테스트앱", "REASON", "정책 위반")), + SMS_ADMIN_LOGIN(Category.SMS, "관리자포탈 로그인", MessageCode.ADMIN_VERIFICATION_MOBILEPHONE, + Strategy.DIRECT, params("authNumber", "123456", "userName", "관리자")), + MSG_API_STATUS(Category.MESSENGER, "API 상태 감시", MessageCode.API_STATUS_CHANGED, + Strategy.DIRECT, params("message", "[테스트] API 상태 변화 알림")), + MSG_INFLOW_TOKEN(Category.MESSENGER, "이상 징후 감시", MessageCode.INFLOW_TOKEN_FAILED, + Strategy.DIRECT, params("message", "[테스트] 유량제어 토큰 획득 실패")), + + // ---- 전략 BROADCAST: SwingMessageWriter 직접 호출(PTL_PROPERTY 역할의 내부직원 전원에게 발송) ---- + MSG_QNA_CREATED(Category.MESSENGER, "QnA 질문등록", MessageCode.INQUIRY_CREATED, + Strategy.BROADCAST, params( + "inquiryId", "TEST-0001", + "inquirySubject", "[테스트] 문의 제목", + "writerName", "홍길동")), + MSG_QNA_COMMENT(Category.MESSENGER, "QnA 댓글 등록", MessageCode.INQUIRY_COMMENT_CREATED, + Strategy.BROADCAST, params( + "inquiryId", "TEST-0001", + "inquirySubject", "[테스트] 문의 제목", + "commentContent", "테스트 댓글입니다", + "writerName", "홍길동")), + MSG_PARTNERSHIP(Category.MESSENGER, "개선요청 새글 등록", MessageCode.PARTNERSHIP_CREATED, + Strategy.BROADCAST, params( + "partnershipId", "TEST-0001", + "bizSubject", "[테스트] 개선요청 제목", + "writerName", "홍길동")), + ; + + @Getter + public enum Category { + EMAIL("이메일"), SMS("SMS"), MESSENGER("메신저"); + + private final String label; + + Category(String label) { + this.label = label; + } + } + + public enum Strategy { HANDLER, DIRECT, BROADCAST } + + private final Category category; + private final String label; + private final MessageCode messageCode; + private final Strategy strategy; + private final MapPTL_PROPERTY({@link NotiTestProperties})로 기능 on/off 와 접근 허용 이메일 목록을 관리한다. + * 운영에서도 상시 켜둘 수 있도록 GNB 에는 노출하지 않고 직접 URL 로만 접근한다(page.yml/portal.pages + * 미등록). 발송 이력은 별도 테이블 없이 HttpSession 에 보관하고, 상태는 기존 PTL_MESSAGE_REQUEST 를 + * 3초 폴링으로 재조회해 갱신한다.
+ */ +@Slf4j +@Controller +@RequestMapping("/djb/notitest") +@RequiredArgsConstructor +public class NotiTestController { + + private static final String SESSION_HISTORY_KEY = "NOTI_TEST_HISTORY"; + private static final int MAX_HISTORY = 50; + + private final NotiTestProperties notiTestProperties; + private final NotiTestService notiTestService; + private final MessageRequestRepository messageRequestRepository; + + /** 폼 + 이력 렌더 */ + @GetMapping + public ModelAndView index(HttpSession session, HttpServletResponse response) { + if (!notiTestProperties.isEnabled()) { + response.setStatus(HttpStatus.NOT_FOUND.value()); + return null; + } + if (!isAllowedForCurrentUser()) { + response.setStatus(HttpStatus.FORBIDDEN.value()); + return null; + } + + PortalAuthenticatedUser me = SecurityUtil.getPortalAuthenticatedUser(); + + ModelAndView mav = new ModelAndView("djb/notitest/index"); + mav.addObject("targetsByCategory", groupByCategory()); + mav.addObject("history", getHistory(session)); + mav.addObject("defaultUsername", me.getUserName()); + mav.addObject("defaultEmail", me.getEmailAddr()); + mav.addObject("defaultPhone", me.getMobileNumber()); + return mav; + } + + /** 발송(AJAX) */ + @PostMapping("/send") + @ResponseBody + public ResponseEntity> send(@RequestParam Map기존 발송 인프라(PTL_MESSAGE_REQUEST → eapim-admin UmsDispatchJob)를 그대로 통과시켜 + 16종 알림 경로가 운영에서도 살아있는지 확인합니다. 접근 허용 계정만 사용할 수 있는 내부 도구입니다.
+3초 간격으로 상태를 다시 조회합니다(PENDING → PROCESSING → SENT/FAILED).
+| 발송시각 | +채널 | +항목 | +수신자 | +메시지 | +상태 | +
|---|---|---|---|---|---|
| 발송 이력이 없습니다. | +|||||