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..61cbe03 --- /dev/null +++ b/src/main/java/com/project/controller/TestNotificationController.java @@ -0,0 +1,51 @@ +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, phoneNumber, email), + variables); + + log.info("[TEST TRIGGER] subId={}, groupId={}", subId, templateGroupId); + + // 3. 서비스 로직 실행 (템플릿 조립 -> Mock Server 전송) + messageSendService.processEvent(event); + + return "Test Triggered! EventID: " + event.eventId(); + } +} 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/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..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; @@ -68,7 +69,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/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 740a2a7..e49bb16 100644 --- a/src/main/java/com/project/notification/service/MessageSendService.java +++ b/src/main/java/com/project/notification/service/MessageSendService.java @@ -3,20 +3,26 @@ 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; 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; @@ -31,11 +37,15 @@ @RequiredArgsConstructor public class MessageSendService { + private static final String BILLING_GROUP_CODE = "BILLING_NOTICE"; + private final MessageLogRepository messageLogRepository; + private final TemplateGroupJpaRepository templateGroupJpaRepository; private final TemplateService templateService; - private final TemplateEngine templateEngine; + private final MessageTemplateEngine messageTemplateEngine; private final EmailSender emailSender; private final SmsSender smsSender; + private final AesUtil aesUtil; private final Counter emailSuccessCounter; private final Counter emailFailCounter; @@ -45,32 +55,71 @@ 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()) + .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, maskedVariables, startTime); - if (!emailSuccess) { - trySmsFallback(event); + if (!emailSuccess) { + trySmsSend(event, true, maskedVariables); + } + } else { + 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, @@ -87,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, @@ -99,11 +149,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.renderText(template.getSubject(), maskedVariables); + String body = messageTemplateEngine.renderHtml(template.getBody(), maskedVariables); log.info( "[EMAIL] Rendered. eventId={}, subId={}, subject={}, bodyLength={}", @@ -113,9 +163,18 @@ 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) { + throw new RuntimeException("Simulated 1% Email Failure"); + } + log.debug("[EMAIL] Sending request to mock-server..."); SendResponse response = emailSender.send(request); @@ -125,7 +184,8 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { if (response != null && response.isSuccess()) { saveMessageLog( event, - template.getVersionId(), + maskedVariables, + template.getId(), Channel.EMAIL, MessageStatus.SUCCESS, null, @@ -137,7 +197,8 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { String errorMsg = response != null ? response.status() : "No response"; saveMessageLog( event, - template.getVersionId(), + maskedVariables, + template.getId(), Channel.EMAIL, MessageStatus.FAIL, errorMsg, @@ -149,7 +210,8 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { log.error("Email send failed. eventId: {}", event.eventId(), e); saveMessageLog( event, - template.getVersionId(), + maskedVariables, + template.getId(), Channel.EMAIL, MessageStatus.FAIL, e.getMessage(), @@ -159,15 +221,17 @@ private boolean tryEmailSend(UsageNotificationEvent event, long startTime) { } } - private void trySmsFallback(UsageNotificationEvent event) { + 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, @@ -190,10 +254,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.renderText(template.getBody(), event.variables()); log.info( "[SMS] Rendered. eventId={}, subId={}, bodyLength={}", @@ -202,7 +266,11 @@ private void trySmsFallback(UsageNotificationEvent event) { 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..."); @@ -211,21 +279,27 @@ 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.getVersionId(), + maskedVariables, + 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"; saveMessageLog( event, - template.getVersionId(), + maskedVariables, + template.getId(), Channel.SMS, MessageStatus.FAIL, errorMsg, @@ -236,7 +310,8 @@ private void trySmsFallback(UsageNotificationEvent event) { log.error("SMS fallback failed. eventId: {}", event.eventId(), e); saveMessageLog( event, - template.getVersionId(), + maskedVariables, + template.getId(), Channel.SMS, MessageStatus.FAIL, e.getMessage(), @@ -247,6 +322,7 @@ private void trySmsFallback(UsageNotificationEvent event) { private void saveMessageLog( UsageNotificationEvent event, + Map maskedVariables, Long templateVersionId, Channel channel, MessageStatus status, @@ -259,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() 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..dc30e40 --- /dev/null +++ b/src/main/java/com/project/notification/service/MessageTemplateEngine.java @@ -0,0 +1,85 @@ +package com.project.notification.service; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.annotation.PostConstruct; + +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 +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 ""; + } + + String preProcessedTemplate = renderText(template, variables); + + if (variables == null || variables.isEmpty()) { + return preProcessedTemplate; + } + + Context context = new Context(); + // HTML에서 'variables.name' 등으로 접근할 수 있게 통째로 넣음 + context.setVariable("variables", variables); + + // 편의상 최상위 레벨에서도 접근 가능하게 풀어서 넣음 (선택사항) + context.setVariables(variables); + + return springTemplateEngine.process(preProcessedTemplate, context); + } + + public String renderText(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(); - } -}