From cb15584d200979d7e83956c895aa9dc949f99d56 Mon Sep 17 00:00:00 2001 From: k0081915 Date: Sun, 25 Jan 2026 19:45:13 +0900 Subject: [PATCH 1/9] =?UTF-8?q?UPLUS-138=20feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TestNotificationController.java | 41 +++++++++++++++++++ .../infra/entity/enums/Grade.java | 8 ++-- 2 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/project/controller/TestNotificationController.java diff --git a/src/main/java/com/project/controller/TestNotificationController.java b/src/main/java/com/project/controller/TestNotificationController.java new file mode 100644 index 0000000..dce50c2 --- /dev/null +++ b/src/main/java/com/project/controller/TestNotificationController.java @@ -0,0 +1,41 @@ +package com.project.controller; + +import com.project.notification.dto.NotificationRequestEvent; +import com.project.notification.service.NotificationService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; +import java.util.UUID; + +@RestController +@RequiredArgsConstructor +public class TestNotificationController { + + private final NotificationService notificationService; + + // Postman 테스트용 엔드포인트 + @PostMapping("/test/send-notification") + public String sendTest(@RequestBody Map body) { + + // 1. Postman Body를 이벤트 객체로 변환 + String templateCode = (String) body.get("templateCode"); + Long subId = Long.valueOf((Integer) body.get("subId")); + Map variables = (Map) body.get("variables"); + String traceId = UUID.randomUUID().toString(); + + NotificationRequestEvent event = new NotificationRequestEvent( + traceId, + subId, + templateCode, + variables + ); + + // 2. 서비스 호출 (Kafka 컨슈머가 하는 일을 대신 수행) + notificationService.processNotification(event, body); + + return "Test Triggered! TraceID: " + traceId; + } +} \ No newline at end of file diff --git a/src/main/java/com/project/notification/infra/entity/enums/Grade.java b/src/main/java/com/project/notification/infra/entity/enums/Grade.java index 2a9d9d2..e2c7859 100644 --- a/src/main/java/com/project/notification/infra/entity/enums/Grade.java +++ b/src/main/java/com/project/notification/infra/entity/enums/Grade.java @@ -1,9 +1,7 @@ package com.project.notification.infra.entity.enums; public enum Grade { - BRONZE, - SILVER, - GOLD, - PLATINUM, - VIP + GENERAL, + VIP, + VVIP } From cfe644bff6d2b69e3acfde108b026d29d7e02ac7 Mon Sep 17 00:00:00 2001 From: k0081915 Date: Sun, 25 Jan 2026 20:18:31 +0900 Subject: [PATCH 2/9] =?UTF-8?q?UPLUS-138=20feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/project/notification/infra/entity/enums/Grade.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/java/com/project/notification/infra/entity/enums/Grade.java diff --git a/src/main/java/com/project/notification/infra/entity/enums/Grade.java b/src/main/java/com/project/notification/infra/entity/enums/Grade.java deleted file mode 100644 index e69de29..0000000 From 1ea4be7435c9a423c3a83330b59b7e3e1ee78224 Mon Sep 17 00:00:00 2001 From: k0081915 Date: Sun, 25 Jan 2026 20:18:55 +0900 Subject: [PATCH 3/9] =?UTF-8?q?UPLUS-138=20feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TestNotificationController.java | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 src/main/java/com/project/controller/TestNotificationController.java diff --git a/src/main/java/com/project/controller/TestNotificationController.java b/src/main/java/com/project/controller/TestNotificationController.java deleted file mode 100644 index dce50c2..0000000 --- a/src/main/java/com/project/controller/TestNotificationController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.project.controller; - -import com.project.notification.dto.NotificationRequestEvent; -import com.project.notification.service.NotificationService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; -import java.util.UUID; - -@RestController -@RequiredArgsConstructor -public class TestNotificationController { - - private final NotificationService notificationService; - - // Postman 테스트용 엔드포인트 - @PostMapping("/test/send-notification") - public String sendTest(@RequestBody Map body) { - - // 1. Postman Body를 이벤트 객체로 변환 - String templateCode = (String) body.get("templateCode"); - Long subId = Long.valueOf((Integer) body.get("subId")); - Map variables = (Map) body.get("variables"); - String traceId = UUID.randomUUID().toString(); - - NotificationRequestEvent event = new NotificationRequestEvent( - traceId, - subId, - templateCode, - variables - ); - - // 2. 서비스 호출 (Kafka 컨슈머가 하는 일을 대신 수행) - notificationService.processNotification(event, body); - - return "Test Triggered! TraceID: " + traceId; - } -} \ No newline at end of file From e68dcae7e705786b2c2b43edb0aa286e398d9553 Mon Sep 17 00:00:00 2001 From: k0081915 Date: Sun, 25 Jan 2026 22:34:59 +0900 Subject: [PATCH 4/9] =?UTF-8?q?UPLUS-138=20feat:=20=ED=83=80=EC=9E=84?= =?UTF-8?q?=EB=A6=AC=ED=94=84=20=ED=8C=8C=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../TestNotificationController.java | 53 +++++++++++++ .../consumer/UsageNotificationEvent.java | 2 +- .../notification/infra/entity/MessageLog.java | 4 +- .../infra/entity/TemplateGroup.java | 18 ++--- .../infra/entity/TemplateVersion.java | 6 +- .../TemplateGroupJpaRepository.java | 2 +- .../repository/TemplateGroupRepository.java | 2 +- .../TemplateGroupRepositoryImpl.java | 4 +- .../TemplateVersionJpaRepository.java | 12 +-- .../TemplateVersionRepositoryImpl.java | 4 +- .../service/MessageSendService.java | 24 +++--- .../service/MessageTemplateEngine.java | 77 +++++++++++++++++++ .../notification/service/TemplateEngine.java | 47 ----------- 14 files changed, 171 insertions(+), 85 deletions(-) create mode 100644 src/main/java/com/project/controller/TestNotificationController.java create mode 100644 src/main/java/com/project/notification/service/MessageTemplateEngine.java delete mode 100644 src/main/java/com/project/notification/service/TemplateEngine.java diff --git a/build.gradle b/build.gradle index 60e7260..df2b148 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // Kafka implementation 'org.springframework.kafka:spring-kafka' diff --git a/src/main/java/com/project/controller/TestNotificationController.java b/src/main/java/com/project/controller/TestNotificationController.java new file mode 100644 index 0000000..4354ae0 --- /dev/null +++ b/src/main/java/com/project/controller/TestNotificationController.java @@ -0,0 +1,53 @@ +package com.project.controller; + +import java.util.Map; +import java.util.UUID; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.project.notification.consumer.UsageNotificationEvent; +import com.project.notification.service.MessageSendService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class TestNotificationController { + + private final MessageSendService messageSendService; + + /** + * [테스트용] Kafka 없이 HTTP 요청으로 알림 발송 로직 직접 트리거 + */ + @PostMapping("/test/send-notification") + public String sendTest(@RequestBody Map request) { + + // 1. Postman JSON 데이터를 추출 + Long subId = Long.valueOf((Integer) request.get("subId")); + String email = (String) request.get("email"); + String phoneNumber = (String) request.get("phoneNumber"); + Long templateGroupId = Long.valueOf((Integer) request.get("templateGroupId")); + + // variables는 리스트가 포함될 수 있으므로 Object로 캐스팅 + Map variables = (Map) request.get("variables"); + + // 2. 가짜 이벤트 객체(UsageNotificationEvent) 생성 + UsageNotificationEvent event = new UsageNotificationEvent( + UUID.randomUUID(), // 임의의 Event ID 생성 + templateGroupId, + new UsageNotificationEvent.SubscriptionInfo(subId, email, phoneNumber), + variables + ); + + log.info("[TEST TRIGGER] subId={}, groupId={}", subId, templateGroupId); + + // 3. 서비스 로직 실행 (템플릿 조립 -> Mock Server 전송) + messageSendService.processEvent(event); + + return "Test Triggered! EventID: " + event.eventId(); + } +} \ No newline at end of file diff --git a/src/main/java/com/project/notification/consumer/UsageNotificationEvent.java b/src/main/java/com/project/notification/consumer/UsageNotificationEvent.java index cf268ea..77362ec 100644 --- a/src/main/java/com/project/notification/consumer/UsageNotificationEvent.java +++ b/src/main/java/com/project/notification/consumer/UsageNotificationEvent.java @@ -7,7 +7,7 @@ public record UsageNotificationEvent( UUID eventId, Long templateGroupId, SubscriptionInfo subscriptionInfo, - Map variables) { + Map variables) { public record SubscriptionInfo(Long subId, String phoneNumber, String email) {} } diff --git a/src/main/java/com/project/notification/infra/entity/MessageLog.java b/src/main/java/com/project/notification/infra/entity/MessageLog.java index 40f8f0e..d583376 100644 --- a/src/main/java/com/project/notification/infra/entity/MessageLog.java +++ b/src/main/java/com/project/notification/infra/entity/MessageLog.java @@ -23,6 +23,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; @Entity @Getter @@ -68,7 +70,7 @@ public class MessageLog { @Column(name = "error_message", columnDefinition = "TEXT") private String errorMessage; - @Convert(converter = JsonMapConverter.class) + @JdbcTypeCode(SqlTypes.JSON) @Column(name = "request_payload", columnDefinition = "jsonb") private Map requestPayload; diff --git a/src/main/java/com/project/notification/infra/entity/TemplateGroup.java b/src/main/java/com/project/notification/infra/entity/TemplateGroup.java index d7869c6..23b2c68 100644 --- a/src/main/java/com/project/notification/infra/entity/TemplateGroup.java +++ b/src/main/java/com/project/notification/infra/entity/TemplateGroup.java @@ -22,14 +22,14 @@ public class TemplateGroup { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "group_id") - private Long groupId; + @Column(name = "id") + private Long id; - @Column(name = "group_code", nullable = false, unique = true, length = 50) - private String groupCode; + @Column(name = "code", nullable = false, unique = true, length = 50) + private String code; - @Column(name = "group_name", nullable = false, length = 100) - private String groupName; + @Column(name = "name", nullable = false, length = 100) + private String name; @Column(name = "description") private String description; @@ -38,9 +38,9 @@ public class TemplateGroup { private LocalDateTime createdAt; @Builder - public TemplateGroup(String groupCode, String groupName, String description) { - this.groupCode = groupCode; - this.groupName = groupName; + public TemplateGroup(String code, String name, String description) { + this.code = code; + this.name = name; this.description = description; this.createdAt = LocalDateTime.now(); } diff --git a/src/main/java/com/project/notification/infra/entity/TemplateVersion.java b/src/main/java/com/project/notification/infra/entity/TemplateVersion.java index 4a887df..b2f0729 100644 --- a/src/main/java/com/project/notification/infra/entity/TemplateVersion.java +++ b/src/main/java/com/project/notification/infra/entity/TemplateVersion.java @@ -30,8 +30,8 @@ public class TemplateVersion { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "version_id") - private Long versionId; + @Column(name = "id") + private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "group_id", nullable = false) @@ -48,7 +48,7 @@ public class TemplateVersion { private String body; @Enumerated(EnumType.STRING) - @Column(name = "status", nullable = false, length = 10) + @Column(name = "template_status", nullable = false, length = 10) private TemplateStatus status; @Column(name = "version", nullable = false) diff --git a/src/main/java/com/project/notification/infra/repository/TemplateGroupJpaRepository.java b/src/main/java/com/project/notification/infra/repository/TemplateGroupJpaRepository.java index bcceb43..d417225 100644 --- a/src/main/java/com/project/notification/infra/repository/TemplateGroupJpaRepository.java +++ b/src/main/java/com/project/notification/infra/repository/TemplateGroupJpaRepository.java @@ -8,5 +8,5 @@ public interface TemplateGroupJpaRepository extends JpaRepository { - Optional findByGroupCode(String groupCode); + Optional findByCode(String code); } diff --git a/src/main/java/com/project/notification/infra/repository/TemplateGroupRepository.java b/src/main/java/com/project/notification/infra/repository/TemplateGroupRepository.java index b806acc..f10fa7e 100644 --- a/src/main/java/com/project/notification/infra/repository/TemplateGroupRepository.java +++ b/src/main/java/com/project/notification/infra/repository/TemplateGroupRepository.java @@ -6,5 +6,5 @@ public interface TemplateGroupRepository { - Optional findByGroupCode(String groupCode); + Optional findByCode(String code); } diff --git a/src/main/java/com/project/notification/infra/repository/TemplateGroupRepositoryImpl.java b/src/main/java/com/project/notification/infra/repository/TemplateGroupRepositoryImpl.java index 8ce910f..076c84e 100644 --- a/src/main/java/com/project/notification/infra/repository/TemplateGroupRepositoryImpl.java +++ b/src/main/java/com/project/notification/infra/repository/TemplateGroupRepositoryImpl.java @@ -15,7 +15,7 @@ public class TemplateGroupRepositoryImpl implements TemplateGroupRepository { private final TemplateGroupJpaRepository templateGroupJpaRepository; @Override - public Optional findByGroupCode(String groupCode) { - return templateGroupJpaRepository.findByGroupCode(groupCode); + public Optional findByCode(String code) { + return templateGroupJpaRepository.findByCode(code); } } diff --git a/src/main/java/com/project/notification/infra/repository/TemplateVersionJpaRepository.java b/src/main/java/com/project/notification/infra/repository/TemplateVersionJpaRepository.java index 1279575..a692468 100644 --- a/src/main/java/com/project/notification/infra/repository/TemplateVersionJpaRepository.java +++ b/src/main/java/com/project/notification/infra/repository/TemplateVersionJpaRepository.java @@ -14,25 +14,25 @@ public interface TemplateVersionJpaRepository extends JpaRepository findLatestByGroupCodeAndChannelAndStatus( - @Param("groupCode") String groupCode, + Optional findLatestByCodeAndChannelAndStatus( + @Param("code") String code, @Param("channel") Channel channel, @Param("status") TemplateStatus status); @Query( "SELECT tv FROM TemplateVersion tv " - + "WHERE tv.templateGroup.groupId = :groupId " + + "WHERE tv.templateGroup.id = :id " + "AND tv.channel = :channel " + "AND tv.status = :status " + "ORDER BY tv.version DESC " + "LIMIT 1") - Optional findLatestByGroupIdAndChannelAndStatus( - @Param("groupId") Long groupId, + Optional findLatestByIdAndChannelAndStatus( + @Param("id") Long id, @Param("channel") Channel channel, @Param("status") TemplateStatus status); } diff --git a/src/main/java/com/project/notification/infra/repository/TemplateVersionRepositoryImpl.java b/src/main/java/com/project/notification/infra/repository/TemplateVersionRepositoryImpl.java index 9490207..9867186 100644 --- a/src/main/java/com/project/notification/infra/repository/TemplateVersionRepositoryImpl.java +++ b/src/main/java/com/project/notification/infra/repository/TemplateVersionRepositoryImpl.java @@ -19,14 +19,14 @@ public class TemplateVersionRepositoryImpl implements TemplateVersionRepository @Override public Optional findLatestActiveByGroupCodeAndChannel( String groupCode, Channel channel) { - return templateVersionJpaRepository.findLatestByGroupCodeAndChannelAndStatus( + return templateVersionJpaRepository.findLatestByCodeAndChannelAndStatus( groupCode, channel, TemplateStatus.ACTIVE); } @Override public Optional findLatestActiveByGroupIdAndChannel( Long groupId, Channel channel) { - return templateVersionJpaRepository.findLatestByGroupIdAndChannelAndStatus( + return templateVersionJpaRepository.findLatestByIdAndChannelAndStatus( groupId, channel, TemplateStatus.ACTIVE); } } diff --git a/src/main/java/com/project/notification/service/MessageSendService.java b/src/main/java/com/project/notification/service/MessageSendService.java index 740a2a7..959b353 100644 --- a/src/main/java/com/project/notification/service/MessageSendService.java +++ b/src/main/java/com/project/notification/service/MessageSendService.java @@ -33,7 +33,7 @@ public class MessageSendService { private final MessageLogRepository messageLogRepository; private final TemplateService templateService; - private final TemplateEngine templateEngine; + private final MessageTemplateEngine messageTemplateEngine; private final EmailSender emailSender; private final SmsSender smsSender; @@ -99,11 +99,11 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { TemplateVersion template = templateOpt.get(); log.debug( "[EMAIL] Template loaded. versionId={}, groupId={}", - template.getVersionId(), + template.getId(), event.templateGroupId()); - String subject = templateEngine.render(template.getSubject(), event.variables()); - String body = templateEngine.render(template.getBody(), event.variables()); + String subject = messageTemplateEngine.renderHtml(template.getSubject(), event.variables()); + String body = messageTemplateEngine.renderHtml(template.getBody(), event.variables()); log.info( "[EMAIL] Rendered. eventId={}, subId={}, subject={}, bodyLength={}", @@ -125,7 +125,7 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { if (response != null && response.isSuccess()) { saveMessageLog( event, - template.getVersionId(), + template.getId(), Channel.EMAIL, MessageStatus.SUCCESS, null, @@ -137,7 +137,7 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { String errorMsg = response != null ? response.status() : "No response"; saveMessageLog( event, - template.getVersionId(), + template.getId(), Channel.EMAIL, MessageStatus.FAIL, errorMsg, @@ -149,7 +149,7 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { log.error("Email send failed. eventId: {}", event.eventId(), e); saveMessageLog( event, - template.getVersionId(), + template.getId(), Channel.EMAIL, MessageStatus.FAIL, e.getMessage(), @@ -190,10 +190,10 @@ private void trySmsFallback(UsageNotificationEvent event) { TemplateVersion template = templateOpt.get(); log.debug( "[SMS] Template loaded. versionId={}, groupId={}", - template.getVersionId(), + template.getId(), event.templateGroupId()); - String body = templateEngine.render(template.getBody(), event.variables()); + String body = messageTemplateEngine.renderSms(template.getBody(), event.variables()); log.info( "[SMS] Rendered. eventId={}, subId={}, bodyLength={}", @@ -214,7 +214,7 @@ private void trySmsFallback(UsageNotificationEvent event) { if (response != null && response.isSuccess()) { saveMessageLog( event, - template.getVersionId(), + template.getId(), Channel.SMS, MessageStatus.SUCCESS_FALLBACK, null, @@ -225,7 +225,7 @@ private void trySmsFallback(UsageNotificationEvent event) { String errorMsg = response != null ? response.status() : "No response"; saveMessageLog( event, - template.getVersionId(), + template.getId(), Channel.SMS, MessageStatus.FAIL, errorMsg, @@ -236,7 +236,7 @@ private void trySmsFallback(UsageNotificationEvent event) { log.error("SMS fallback failed. eventId: {}", event.eventId(), e); saveMessageLog( event, - template.getVersionId(), + template.getId(), Channel.SMS, MessageStatus.FAIL, e.getMessage(), diff --git a/src/main/java/com/project/notification/service/MessageTemplateEngine.java b/src/main/java/com/project/notification/service/MessageTemplateEngine.java new file mode 100644 index 0000000..d45ec8a --- /dev/null +++ b/src/main/java/com/project/notification/service/MessageTemplateEngine.java @@ -0,0 +1,77 @@ +package com.project.notification.service; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; +import org.thymeleaf.templateresolver.StringTemplateResolver; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MessageTemplateEngine { + + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{(\\w+)}}"); + private final SpringTemplateEngine springTemplateEngine; + + @PostConstruct + public void init() { + StringTemplateResolver resolver = new StringTemplateResolver(); + resolver.setTemplateMode("HTML"); + resolver.setCacheable(false); // 동적 템플릿이므로 캐시 끔 + springTemplateEngine.setTemplateResolver(resolver); + } + + public String renderHtml(String template, Map variables) { + if (template == null) return ""; + if (variables == null) return template; + + Context context = new Context(); + // HTML에서 'variables.name' 등으로 접근할 수 있게 통째로 넣음 + context.setVariable("variables", variables); + + // 편의상 최상위 레벨에서도 접근 가능하게 풀어서 넣음 (선택사항) + context.setVariables(variables); + + return springTemplateEngine.process(template, context); + } + + public String renderSms(String template, Map variables) { + if (template == null) { + return null; + } + + if (variables == null || variables.isEmpty()) { + return template; + } + + StringBuffer result = new StringBuffer(); + Matcher matcher = VARIABLE_PATTERN.matcher(template); + + while (matcher.find()) { + String variableName = matcher.group(1); + Object valueObj = variables.get(variableName); + String replacement = valueObj != null ? String.valueOf(valueObj) : ""; + + if (replacement == null) { + log.warn( + "Template variable '{}' not found in variables map, replacing with empty" + + " string", + variableName); + replacement = ""; + } + + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + + matcher.appendTail(result); + return result.toString(); + } +} diff --git a/src/main/java/com/project/notification/service/TemplateEngine.java b/src/main/java/com/project/notification/service/TemplateEngine.java deleted file mode 100644 index 6d4f297..0000000 --- a/src/main/java/com/project/notification/service/TemplateEngine.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.project.notification.service; - -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.springframework.stereotype.Component; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Component -public class TemplateEngine { - - private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{(\\w+)}}"); - - public String render(String template, Map variables) { - if (template == null) { - return null; - } - - if (variables == null || variables.isEmpty()) { - return template; - } - - StringBuffer result = new StringBuffer(); - Matcher matcher = VARIABLE_PATTERN.matcher(template); - - while (matcher.find()) { - String variableName = matcher.group(1); - String replacement = variables.get(variableName); - - if (replacement == null) { - log.warn( - "Template variable '{}' not found in variables map, replacing with empty" - + " string", - variableName); - replacement = ""; - } - - matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); - } - - matcher.appendTail(result); - return result.toString(); - } -} From 439251f7cd12bed0f59cf2a5545b7cd7c24fd49e Mon Sep 17 00:00:00 2001 From: k0081915 Date: Mon, 26 Jan 2026 02:31:13 +0900 Subject: [PATCH 5/9] =?UTF-8?q?UPLUS-138=20feat:=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=9C=EB=AA=A9=20=EC=B9=98=ED=99=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=201%=20=EC=8B=A4=ED=8C=A8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/MessageSendService.java | 20 +++++++++++++------ .../service/MessageTemplateEngine.java | 15 ++++++++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/project/notification/service/MessageSendService.java b/src/main/java/com/project/notification/service/MessageSendService.java index 959b353..71b4662 100644 --- a/src/main/java/com/project/notification/service/MessageSendService.java +++ b/src/main/java/com/project/notification/service/MessageSendService.java @@ -3,6 +3,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import org.springframework.stereotype.Service; @@ -58,7 +59,7 @@ public void processEvent(UsageNotificationEvent event) { boolean emailSuccess = tryEmailSend(event, startTime); if (!emailSuccess) { - trySmsFallback(event); + trySmsSend(event, true); } } @@ -102,7 +103,7 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { template.getId(), event.templateGroupId()); - String subject = messageTemplateEngine.renderHtml(template.getSubject(), event.variables()); + String subject = messageTemplateEngine.renderText(template.getSubject(), event.variables()); String body = messageTemplateEngine.renderHtml(template.getBody(), event.variables()); log.info( @@ -116,6 +117,10 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { EmailSendRequest.of(subInfo.subId(), email, subInfo.phoneNumber(), subject, body); try { + if (ThreadLocalRandom.current().nextInt(100) == 0) { + throw new RuntimeException("Simulated 1% Email Failure (Test)"); + } + log.debug("[EMAIL] Sending request to mock-server..."); SendResponse response = emailSender.send(request); @@ -159,7 +164,7 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { } } - private void trySmsFallback(UsageNotificationEvent event) { + private void trySmsSend(UsageNotificationEvent event, boolean isFallback) { long smsStartTime = System.currentTimeMillis(); UsageNotificationEvent.SubscriptionInfo subInfo = event.subscriptionInfo(); @@ -193,7 +198,7 @@ private void trySmsFallback(UsageNotificationEvent event) { template.getId(), event.templateGroupId()); - String body = messageTemplateEngine.renderSms(template.getBody(), event.variables()); + String body = messageTemplateEngine.renderText(template.getBody(), event.variables()); log.info( "[SMS] Rendered. eventId={}, subId={}, bodyLength={}", @@ -211,15 +216,18 @@ private void trySmsFallback(UsageNotificationEvent event) { long processingTime = System.currentTimeMillis() - smsStartTime; smsProcessingTimer.record(processingTime, TimeUnit.MILLISECONDS); + MessageStatus statusOnSuccess = isFallback ? MessageStatus.SUCCESS_FALLBACK : MessageStatus.SUCCESS; + Counter counterOnSuccess = isFallback ? smsFallbackCounter : smsSuccessCounter; + if (response != null && response.isSuccess()) { saveMessageLog( event, template.getId(), Channel.SMS, - MessageStatus.SUCCESS_FALLBACK, + statusOnSuccess, null, processingTime); - smsFallbackCounter.increment(); + counterOnSuccess.increment(); log.info("SMS fallback sent successfully. eventId: {}", event.eventId()); } else { String errorMsg = response != null ? response.status() : "No response"; diff --git a/src/main/java/com/project/notification/service/MessageTemplateEngine.java b/src/main/java/com/project/notification/service/MessageTemplateEngine.java index d45ec8a..afa32bd 100644 --- a/src/main/java/com/project/notification/service/MessageTemplateEngine.java +++ b/src/main/java/com/project/notification/service/MessageTemplateEngine.java @@ -30,8 +30,15 @@ public void init() { } public String renderHtml(String template, Map variables) { - if (template == null) return ""; - if (variables == null) return template; + if (template == null){ + return ""; + } + + String preProcessedTemplate = renderText(template, variables); + + if (variables == null || variables.isEmpty()) { + return preProcessedTemplate; + } Context context = new Context(); // HTML에서 'variables.name' 등으로 접근할 수 있게 통째로 넣음 @@ -40,10 +47,10 @@ public String renderHtml(String template, Map variables) { // 편의상 최상위 레벨에서도 접근 가능하게 풀어서 넣음 (선택사항) context.setVariables(variables); - return springTemplateEngine.process(template, context); + return springTemplateEngine.process(preProcessedTemplate, context); } - public String renderSms(String template, Map variables) { + public String renderText(String template, Map variables) { if (template == null) { return null; } From 3307352cbfcb3fc92c8f9f705a34f2bbdffba868 Mon Sep 17 00:00:00 2001 From: k0081915 Date: Mon, 26 Jan 2026 10:19:19 +0900 Subject: [PATCH 6/9] =?UTF-8?q?UPLUS-138=20feat:=20=EC=B2=AD=EA=B5=AC?= =?UTF-8?q?=EC=84=9C=EB=A7=8C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/MessageSendService.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/project/notification/service/MessageSendService.java b/src/main/java/com/project/notification/service/MessageSendService.java index 71b4662..3849f17 100644 --- a/src/main/java/com/project/notification/service/MessageSendService.java +++ b/src/main/java/com/project/notification/service/MessageSendService.java @@ -6,6 +6,8 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; +import com.project.notification.infra.entity.TemplateGroup; +import com.project.notification.infra.repository.TemplateGroupJpaRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,7 +34,10 @@ @RequiredArgsConstructor public class MessageSendService { + private static final String BILLING_GROUP_CODE = "BILL_NOTICE"; + private final MessageLogRepository messageLogRepository; + private final TemplateGroupJpaRepository templateGroupJpaRepository; private final TemplateService templateService; private final MessageTemplateEngine messageTemplateEngine; private final EmailSender emailSender; @@ -50,16 +55,24 @@ public class MessageSendService { public void processEvent(UsageNotificationEvent event) { long startTime = System.currentTimeMillis(); + String groupCode = templateGroupJpaRepository.findById(event.templateGroupId()) + .map(TemplateGroup::getCode) + .orElse(""); + log.info( "Processing notification event. eventId: {}, templateGroupId: {}, subId: {}", event.eventId(), event.templateGroupId(), event.subscriptionInfo().subId()); - boolean emailSuccess = tryEmailSend(event, startTime); + if (BILLING_GROUP_CODE.equals(groupCode)) { + boolean emailSuccess = tryEmailSend(event, startTime); - if (!emailSuccess) { - trySmsSend(event, true); + if (!emailSuccess) { + trySmsSend(event, true); + } + } else { + trySmsSend(event, false); } } From 67d79582e39a81f2f69380a0040aadb95c35172f Mon Sep 17 00:00:00 2001 From: k0081915 Date: Mon, 26 Jan 2026 10:20:37 +0900 Subject: [PATCH 7/9] =?UTF-8?q?UPLUS-138=20style:=20spotless=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TestNotificationController.java | 18 ++++++++---------- .../notification/infra/entity/MessageLog.java | 7 +++---- .../service/MessageSendService.java | 15 +++++++++------ .../service/MessageTemplateEngine.java | 9 +++++---- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/project/controller/TestNotificationController.java b/src/main/java/com/project/controller/TestNotificationController.java index 4354ae0..aec7b8c 100644 --- a/src/main/java/com/project/controller/TestNotificationController.java +++ b/src/main/java/com/project/controller/TestNotificationController.java @@ -20,9 +20,7 @@ public class TestNotificationController { private final MessageSendService messageSendService; - /** - * [테스트용] Kafka 없이 HTTP 요청으로 알림 발송 로직 직접 트리거 - */ + /** [테스트용] Kafka 없이 HTTP 요청으로 알림 발송 로직 직접 트리거 */ @PostMapping("/test/send-notification") public String sendTest(@RequestBody Map request) { @@ -36,12 +34,12 @@ public String sendTest(@RequestBody Map request) { Map variables = (Map) request.get("variables"); // 2. 가짜 이벤트 객체(UsageNotificationEvent) 생성 - UsageNotificationEvent event = new UsageNotificationEvent( - UUID.randomUUID(), // 임의의 Event ID 생성 - templateGroupId, - new UsageNotificationEvent.SubscriptionInfo(subId, email, phoneNumber), - variables - ); + UsageNotificationEvent event = + new UsageNotificationEvent( + UUID.randomUUID(), // 임의의 Event ID 생성 + templateGroupId, + new UsageNotificationEvent.SubscriptionInfo(subId, email, phoneNumber), + variables); log.info("[TEST TRIGGER] subId={}, groupId={}", subId, templateGroupId); @@ -50,4 +48,4 @@ public String sendTest(@RequestBody Map request) { return "Test Triggered! EventID: " + event.eventId(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/project/notification/infra/entity/MessageLog.java b/src/main/java/com/project/notification/infra/entity/MessageLog.java index d583376..0d65499 100644 --- a/src/main/java/com/project/notification/infra/entity/MessageLog.java +++ b/src/main/java/com/project/notification/infra/entity/MessageLog.java @@ -4,7 +4,6 @@ import java.util.Map; import jakarta.persistence.Column; -import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -15,7 +14,9 @@ import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; -import com.project.global.util.JsonMapConverter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + import com.project.notification.infra.entity.enums.Channel; import com.project.notification.infra.entity.enums.MessageStatus; @@ -23,8 +24,6 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.JdbcTypeCode; -import org.hibernate.type.SqlTypes; @Entity @Getter diff --git a/src/main/java/com/project/notification/service/MessageSendService.java b/src/main/java/com/project/notification/service/MessageSendService.java index 3849f17..688764f 100644 --- a/src/main/java/com/project/notification/service/MessageSendService.java +++ b/src/main/java/com/project/notification/service/MessageSendService.java @@ -6,8 +6,6 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; -import com.project.notification.infra.entity.TemplateGroup; -import com.project.notification.infra.repository.TemplateGroupJpaRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,10 +14,12 @@ import com.project.notification.dto.SendResponse; import com.project.notification.dto.SmsSendRequest; import com.project.notification.infra.entity.MessageLog; +import com.project.notification.infra.entity.TemplateGroup; import com.project.notification.infra.entity.TemplateVersion; import com.project.notification.infra.entity.enums.Channel; import com.project.notification.infra.entity.enums.MessageStatus; import com.project.notification.infra.repository.MessageLogRepository; +import com.project.notification.infra.repository.TemplateGroupJpaRepository; import com.project.notification.sender.EmailSender; import com.project.notification.sender.SmsSender; @@ -55,9 +55,11 @@ public class MessageSendService { public void processEvent(UsageNotificationEvent event) { long startTime = System.currentTimeMillis(); - String groupCode = templateGroupJpaRepository.findById(event.templateGroupId()) - .map(TemplateGroup::getCode) - .orElse(""); + String groupCode = + templateGroupJpaRepository + .findById(event.templateGroupId()) + .map(TemplateGroup::getCode) + .orElse(""); log.info( "Processing notification event. eventId: {}, templateGroupId: {}, subId: {}", @@ -229,7 +231,8 @@ private void trySmsSend(UsageNotificationEvent event, boolean isFallback) { long processingTime = System.currentTimeMillis() - smsStartTime; smsProcessingTimer.record(processingTime, TimeUnit.MILLISECONDS); - MessageStatus statusOnSuccess = isFallback ? MessageStatus.SUCCESS_FALLBACK : MessageStatus.SUCCESS; + MessageStatus statusOnSuccess = + isFallback ? MessageStatus.SUCCESS_FALLBACK : MessageStatus.SUCCESS; Counter counterOnSuccess = isFallback ? smsFallbackCounter : smsSuccessCounter; if (response != null && response.isSuccess()) { diff --git a/src/main/java/com/project/notification/service/MessageTemplateEngine.java b/src/main/java/com/project/notification/service/MessageTemplateEngine.java index afa32bd..dc30e40 100644 --- a/src/main/java/com/project/notification/service/MessageTemplateEngine.java +++ b/src/main/java/com/project/notification/service/MessageTemplateEngine.java @@ -5,14 +5,15 @@ import java.util.regex.Pattern; import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; import org.thymeleaf.context.Context; import org.thymeleaf.spring6.SpringTemplateEngine; import org.thymeleaf.templateresolver.StringTemplateResolver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + @Slf4j @Component @RequiredArgsConstructor @@ -30,7 +31,7 @@ public void init() { } public String renderHtml(String template, Map variables) { - if (template == null){ + if (template == null) { return ""; } From e5cb4b760f8d000eee6d320ea25b1264403870fc Mon Sep 17 00:00:00 2001 From: k0081915 Date: Mon, 26 Jan 2026 11:36:40 +0900 Subject: [PATCH 8/9] =?UTF-8?q?UPLUS-138=20rename:=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85,=20=EB=A1=9C=EA=B7=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/project/notification/service/MessageSendService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/project/notification/service/MessageSendService.java b/src/main/java/com/project/notification/service/MessageSendService.java index 688764f..7bd6f38 100644 --- a/src/main/java/com/project/notification/service/MessageSendService.java +++ b/src/main/java/com/project/notification/service/MessageSendService.java @@ -34,7 +34,7 @@ @RequiredArgsConstructor public class MessageSendService { - private static final String BILLING_GROUP_CODE = "BILL_NOTICE"; + private static final String BILLING_GROUP_CODE = "BILLING_NOTICE"; private final MessageLogRepository messageLogRepository; private final TemplateGroupJpaRepository templateGroupJpaRepository; @@ -133,7 +133,7 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { try { if (ThreadLocalRandom.current().nextInt(100) == 0) { - throw new RuntimeException("Simulated 1% Email Failure (Test)"); + throw new RuntimeException("Simulated 1% Email Failure"); } log.debug("[EMAIL] Sending request to mock-server..."); From 3e4284b736f0b8d0d7550aa87e96f05f2838293d Mon Sep 17 00:00:00 2001 From: k0081915 Date: Mon, 26 Jan 2026 14:28:50 +0900 Subject: [PATCH 9/9] =?UTF-8?q?UPLUS-138=20feat:=20usage-noti=20request=20?= =?UTF-8?q?=EC=95=94=ED=98=B8=ED=99=94,=20=EB=B3=B5=ED=98=B8=ED=99=94=20?= =?UTF-8?q?=ED=9B=84=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TestNotificationController.java | 2 +- .../com/project/global/util/MaskingUtil.java | 60 +++++++++++++++ .../notification/sender/EmailSender.java | 14 +--- .../notification/sender/SmsSender.java | 10 +-- .../service/MessageSendService.java | 76 ++++++++++++++++--- 5 files changed, 129 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/project/global/util/MaskingUtil.java diff --git a/src/main/java/com/project/controller/TestNotificationController.java b/src/main/java/com/project/controller/TestNotificationController.java index aec7b8c..61cbe03 100644 --- a/src/main/java/com/project/controller/TestNotificationController.java +++ b/src/main/java/com/project/controller/TestNotificationController.java @@ -38,7 +38,7 @@ public String sendTest(@RequestBody Map request) { new UsageNotificationEvent( UUID.randomUUID(), // 임의의 Event ID 생성 templateGroupId, - new UsageNotificationEvent.SubscriptionInfo(subId, email, phoneNumber), + new UsageNotificationEvent.SubscriptionInfo(subId, phoneNumber, email), variables); log.info("[TEST TRIGGER] subId={}, groupId={}", subId, templateGroupId); diff --git a/src/main/java/com/project/global/util/MaskingUtil.java b/src/main/java/com/project/global/util/MaskingUtil.java new file mode 100644 index 0000000..8c7f899 --- /dev/null +++ b/src/main/java/com/project/global/util/MaskingUtil.java @@ -0,0 +1,60 @@ +package com.project.global.util; + +public final class MaskingUtil { + + private MaskingUtil() {} + + // 마스킹 010-**12-**12의 형식 + public static String maskPhone(String phone) { + if (phone == null || phone.isBlank()) { + return phone; + } + + // 숫자만 추출 + String digits = phone.replaceAll("\\D", ""); + + // 휴대폰 번호 길이 최소 검증 (010XXXXXXXX 기준) + if (digits.length() != 11) { + return "***"; + } + + String first = digits.substring(0, 3); + String middle = digits.substring(3, 7); + String last = digits.substring(7, 11); + + // 010-**34-**12 + return String.format("%s-**%s-**%s", first, middle.substring(2), last.substring(2)); + } + + public static String maskEmail(String email) { + if (email == null || email.isBlank()) { + return email; + } + + int at = email.indexOf('@'); + if (at < 0) { + return null; + } // @ 없으면 null + + String local = email.substring(0, at); + String domain = email.substring(at); // "@domain.com" + + // local 비어있으면 "***@domain.com" + if (local.isEmpty()) { + return "***" + domain; + } + + // local 1글자 이상이면 "첫 글자 + *** + @domain.com" + return local.substring(0, 1) + "***" + domain; + } + + public static Object maskByFieldName(String key, Object value) { + if (!(value instanceof String strVal)) return value; + String lowerKey = key.toLowerCase(); + + if (lowerKey.contains("email")) return maskEmail(strVal); + if (lowerKey.contains("phone") || lowerKey.contains("contact")) return maskPhone(strVal); + + return value; + } +} diff --git a/src/main/java/com/project/notification/sender/EmailSender.java b/src/main/java/com/project/notification/sender/EmailSender.java index 88e2b23..f38a933 100644 --- a/src/main/java/com/project/notification/sender/EmailSender.java +++ b/src/main/java/com/project/notification/sender/EmailSender.java @@ -8,6 +8,7 @@ import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClientException; +import com.project.global.util.MaskingUtil; import com.project.notification.dto.EmailSendRequest; import com.project.notification.dto.SendResponse; @@ -31,7 +32,7 @@ public SendResponse send(EmailSendRequest request) { log.info( "[EMAIL] Sending to subId: {}, email: {}, subject: {}", request.subId(), - maskEmail(request.email()), + MaskingUtil.maskEmail(request.email()), request.subject()); if (!mockServerEnabled) { @@ -62,15 +63,4 @@ public SendResponse send(EmailSendRequest request) { return new SendResponse(null, "FAIL"); } } - - private String maskEmail(String email) { - if (email == null || email.length() < 5) { - return "***"; - } - int atIndex = email.indexOf('@'); - if (atIndex <= 1) { - return "***"; - } - return email.substring(0, 2) + "***" + email.substring(atIndex); - } } diff --git a/src/main/java/com/project/notification/sender/SmsSender.java b/src/main/java/com/project/notification/sender/SmsSender.java index 2b41098..9ef41fc 100644 --- a/src/main/java/com/project/notification/sender/SmsSender.java +++ b/src/main/java/com/project/notification/sender/SmsSender.java @@ -8,6 +8,7 @@ import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClientException; +import com.project.global.util.MaskingUtil; import com.project.notification.dto.SendResponse; import com.project.notification.dto.SmsSendRequest; @@ -31,7 +32,7 @@ public SendResponse send(SmsSendRequest request) { log.info( "[SMS] Sending to subId: {}, phone: {}", request.subId(), - maskPhone(request.phone())); + MaskingUtil.maskPhone(request.phone())); if (!mockServerEnabled) { String mockMessageId = "mock-sms-" + UUID.randomUUID(); @@ -61,11 +62,4 @@ public SendResponse send(SmsSendRequest request) { return new SendResponse(null, "FAIL"); } } - - private String maskPhone(String phone) { - if (phone == null || phone.length() < 4) { - return "***"; - } - return phone.substring(0, phone.length() - 4) + "****"; - } } diff --git a/src/main/java/com/project/notification/service/MessageSendService.java b/src/main/java/com/project/notification/service/MessageSendService.java index 7bd6f38..e49bb16 100644 --- a/src/main/java/com/project/notification/service/MessageSendService.java +++ b/src/main/java/com/project/notification/service/MessageSendService.java @@ -3,12 +3,15 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.project.global.util.AesUtil; +import com.project.global.util.MaskingUtil; import com.project.notification.consumer.UsageNotificationEvent; import com.project.notification.dto.EmailSendRequest; import com.project.notification.dto.SendResponse; @@ -42,6 +45,7 @@ public class MessageSendService { private final MessageTemplateEngine messageTemplateEngine; private final EmailSender emailSender; private final SmsSender smsSender; + private final AesUtil aesUtil; private final Counter emailSuccessCounter; private final Counter emailFailCounter; @@ -51,10 +55,15 @@ public class MessageSendService { private final Timer emailProcessingTimer; private final Timer smsProcessingTimer; + // variables 중 암호화된 필드 키 목록 + private static final Set ENCRYPTED_KEYS = Set.of("phone_number", "email"); + @Transactional public void processEvent(UsageNotificationEvent event) { long startTime = System.currentTimeMillis(); + Map maskedVariables = prepareVariablesForSending(event.variables()); + String groupCode = templateGroupJpaRepository .findById(event.templateGroupId()) @@ -68,25 +77,49 @@ public void processEvent(UsageNotificationEvent event) { event.subscriptionInfo().subId()); if (BILLING_GROUP_CODE.equals(groupCode)) { - boolean emailSuccess = tryEmailSend(event, startTime); + boolean emailSuccess = tryEmailSend(event, maskedVariables, startTime); if (!emailSuccess) { - trySmsSend(event, true); + trySmsSend(event, true, maskedVariables); } } else { - trySmsSend(event, false); + trySmsSend(event, false, maskedVariables); } } - private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { + // 암호화된 필드 복호화, 마스킹 + private Map prepareVariablesForSending(Map rawVariables) { + Map processed = new HashMap<>(rawVariables); + + processed.forEach( + (key, value) -> { + if (ENCRYPTED_KEYS.contains(key)) { + try { + // 복호화 시도 + String decrypted = aesUtil.decrypt(value.toString()); + // 마스킹 + processed.put(key, MaskingUtil.maskByFieldName(key, decrypted)); + } catch (Exception e) { + // 복호화 실패 시 (이미 평문이거나 깨진 값) -> 원본 유지 또는 로그 경고 + log.warn("Failed to decrypt field '{}'. Keeping original value.", key); + // processed.put(key, strVal); // 원본 유지 + } + } + }); + return processed; + } + + private boolean tryEmailSend( + UsageNotificationEvent event, Map maskedVariables, long startTime) { long emailStartTime = System.currentTimeMillis(); UsageNotificationEvent.SubscriptionInfo subInfo = event.subscriptionInfo(); - String email = subInfo.email(); + String email = aesUtil.decrypt(subInfo.email()); if (email == null || email.isBlank()) { log.warn("Email is empty for subId: {}", subInfo.subId()); saveMessageLog( event, + maskedVariables, null, Channel.EMAIL, MessageStatus.FAIL, @@ -103,6 +136,7 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { log.warn("Email template not found for groupId: {}", event.templateGroupId()); saveMessageLog( event, + maskedVariables, null, Channel.EMAIL, MessageStatus.FAIL, @@ -118,8 +152,8 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { template.getId(), event.templateGroupId()); - String subject = messageTemplateEngine.renderText(template.getSubject(), event.variables()); - String body = messageTemplateEngine.renderHtml(template.getBody(), event.variables()); + String subject = messageTemplateEngine.renderText(template.getSubject(), maskedVariables); + String body = messageTemplateEngine.renderHtml(template.getBody(), maskedVariables); log.info( "[EMAIL] Rendered. eventId={}, subId={}, subject={}, bodyLength={}", @@ -129,7 +163,12 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { body != null ? body.length() : 0); EmailSendRequest request = - EmailSendRequest.of(subInfo.subId(), email, subInfo.phoneNumber(), subject, body); + EmailSendRequest.of( + subInfo.subId(), + MaskingUtil.maskEmail(email), + MaskingUtil.maskPhone(aesUtil.decrypt(subInfo.phoneNumber())), + subject, + body); try { if (ThreadLocalRandom.current().nextInt(100) == 0) { @@ -145,6 +184,7 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { if (response != null && response.isSuccess()) { saveMessageLog( event, + maskedVariables, template.getId(), Channel.EMAIL, MessageStatus.SUCCESS, @@ -157,6 +197,7 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { String errorMsg = response != null ? response.status() : "No response"; saveMessageLog( event, + maskedVariables, template.getId(), Channel.EMAIL, MessageStatus.FAIL, @@ -169,6 +210,7 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { log.error("Email send failed. eventId: {}", event.eventId(), e); saveMessageLog( event, + maskedVariables, template.getId(), Channel.EMAIL, MessageStatus.FAIL, @@ -179,15 +221,17 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { } } - private void trySmsSend(UsageNotificationEvent event, boolean isFallback) { + private void trySmsSend( + UsageNotificationEvent event, boolean isFallback, Map maskedVariables) { long smsStartTime = System.currentTimeMillis(); UsageNotificationEvent.SubscriptionInfo subInfo = event.subscriptionInfo(); - String phoneNumber = subInfo.phoneNumber(); + String phoneNumber = aesUtil.decrypt(subInfo.phoneNumber()); if (phoneNumber == null || phoneNumber.isBlank()) { log.warn("Phone number is empty for subId: {}", subInfo.subId()); saveMessageLog( event, + maskedVariables, null, Channel.SMS, MessageStatus.FAIL, @@ -222,7 +266,11 @@ private void trySmsSend(UsageNotificationEvent event, boolean isFallback) { body != null ? body.length() : 0); SmsSendRequest request = - SmsSendRequest.of(subInfo.subId(), subInfo.email(), phoneNumber, body); + SmsSendRequest.of( + subInfo.subId(), + MaskingUtil.maskEmail(aesUtil.decrypt(subInfo.email())), + MaskingUtil.maskPhone(phoneNumber), + body); try { log.debug("[SMS] Sending request to mock-server..."); @@ -238,6 +286,7 @@ private void trySmsSend(UsageNotificationEvent event, boolean isFallback) { if (response != null && response.isSuccess()) { saveMessageLog( event, + maskedVariables, template.getId(), Channel.SMS, statusOnSuccess, @@ -249,6 +298,7 @@ private void trySmsSend(UsageNotificationEvent event, boolean isFallback) { String errorMsg = response != null ? response.status() : "No response"; saveMessageLog( event, + maskedVariables, template.getId(), Channel.SMS, MessageStatus.FAIL, @@ -260,6 +310,7 @@ private void trySmsSend(UsageNotificationEvent event, boolean isFallback) { log.error("SMS fallback failed. eventId: {}", event.eventId(), e); saveMessageLog( event, + maskedVariables, template.getId(), Channel.SMS, MessageStatus.FAIL, @@ -271,6 +322,7 @@ private void trySmsSend(UsageNotificationEvent event, boolean isFallback) { private void saveMessageLog( UsageNotificationEvent event, + Map maskedVariables, Long templateVersionId, Channel channel, MessageStatus status, @@ -283,7 +335,7 @@ private void saveMessageLog( Map payload = new HashMap<>(); payload.put("eventId", event.eventId().toString()); payload.put("templateGroupId", event.templateGroupId()); - payload.put("variables", event.variables()); + payload.put("variables", maskedVariables); MessageLog messageLog = MessageLog.builder()