-
Notifications
You must be signed in to change notification settings - Fork 0
SCRUM-125 알림 템플릿 구현 #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.wellmeet.notification.template; | ||
|
|
||
| import com.wellmeet.notification.consumer.dto.NotificationType; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
||
| public interface NotificationTemplate { | ||
|
|
||
| boolean supports(NotificationType type); | ||
|
|
||
| NotificationTemplateData create(Map<String, Object> payload); | ||
|
|
||
| default String getStringOrDefault(Map<String, Object> payload, String key, String defaultValue) { | ||
| Object value = payload.get(key); | ||
| return Objects.toString(value, defaultValue); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.wellmeet.notification.template; | ||
|
|
||
| public record NotificationTemplateData( | ||
| String title, | ||
| String body, | ||
| String url, | ||
| boolean requireInteraction | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.wellmeet.notification.template; | ||
|
|
||
| import com.wellmeet.exception.ErrorCode; | ||
| import com.wellmeet.exception.WellMeetNotificationException; | ||
| import com.wellmeet.notification.consumer.dto.NotificationType; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class NotificationTemplateFactory { | ||
|
|
||
| private final List<NotificationTemplate> templates; | ||
|
|
||
| public NotificationTemplateData createTemplateData(NotificationType type, Map<String, Object> payload) { | ||
| NotificationTemplate template = templates.stream() | ||
| .filter(low -> low.supports(type)) | ||
| .findFirst() | ||
| .orElseThrow(() -> new WellMeetNotificationException(ErrorCode.TEMPLATE_NOT_FOUND)); | ||
|
Comment on lines
+18
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| return template.create(payload); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.wellmeet.notification.template.impl; | ||
|
|
||
| import com.wellmeet.notification.consumer.dto.NotificationType; | ||
| import com.wellmeet.notification.template.NotificationTemplate; | ||
| import com.wellmeet.notification.template.NotificationTemplateData; | ||
| import java.util.Map; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| public class ReservationCanceledTemplate implements NotificationTemplate { | ||
|
|
||
| @Override | ||
| public boolean supports(NotificationType type) { | ||
| return NotificationType.RESERVATION_CANCELED == type; | ||
| } | ||
|
|
||
| @Override | ||
| public NotificationTemplateData create(Map<String, Object> payload) { | ||
| String restaurantName = getStringOrDefault(payload, "restaurantName", "식당"); | ||
| String reservationTime = getStringOrDefault(payload, "reservationTime", ""); | ||
| String reservationId = getStringOrDefault(payload, "reservationId", ""); | ||
|
Comment on lines
+19
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 페이로드에서 값을 가져오기 위해 사용되는 키 문자열들("restaurantName", "reservationTime", "reservationId")이 하드코딩되어 있습니다. 이러한 '매직 스트링'은 오타에 취약하고 유지보수를 어렵게 만듭니다. 이 키들을 별도의 상수 클래스(e.g., |
||
|
|
||
| String title = "예약이 취소되었습니다"; | ||
| String body = String.format("%s 예약이 취소되었습니다. 예약 시간: %s", | ||
| restaurantName, reservationTime); | ||
| String url = "/reservations/" + reservationId; | ||
|
|
||
| return new NotificationTemplateData(title, body, url, false); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.wellmeet.notification.template.impl; | ||
|
|
||
| import com.wellmeet.notification.consumer.dto.NotificationType; | ||
| import com.wellmeet.notification.template.NotificationTemplate; | ||
| import com.wellmeet.notification.template.NotificationTemplateData; | ||
| import java.util.Map; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| public class ReservationConfirmedTemplate implements NotificationTemplate { | ||
|
|
||
| @Override | ||
| public boolean supports(NotificationType type) { | ||
| return NotificationType.RESERVATION_CONFIRMED == type; | ||
| } | ||
|
|
||
| @Override | ||
| public NotificationTemplateData create(Map<String, Object> payload) { | ||
| String restaurantName = getStringOrDefault(payload, "restaurantName", "식당"); | ||
| String reservationTime = getStringOrDefault(payload, "reservationTime", ""); | ||
| String reservationId = getStringOrDefault(payload, "reservationId", ""); | ||
|
|
||
| String title = "예약이 확정되었습니다"; | ||
| String body = String.format("%s 예약이 확정되었습니다. 예약 시간: %s", | ||
| restaurantName, reservationTime); | ||
| String url = "/reservations/" + reservationId; | ||
|
|
||
| return new NotificationTemplateData(title, body, url, false); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.wellmeet.notification.template.impl; | ||
|
|
||
| import com.wellmeet.notification.consumer.dto.NotificationType; | ||
| import com.wellmeet.notification.template.NotificationTemplate; | ||
| import com.wellmeet.notification.template.NotificationTemplateData; | ||
| import java.util.Map; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| public class ReservationCreatedTemplate implements NotificationTemplate { | ||
|
|
||
| @Override | ||
| public boolean supports(NotificationType type) { | ||
| return NotificationType.RESERVATION_CREATED == type; | ||
| } | ||
|
|
||
| @Override | ||
| public NotificationTemplateData create(Map<String, Object> payload) { | ||
| String restaurantName = getStringOrDefault(payload, "restaurantName", "식당"); | ||
| String reservationTime = getStringOrDefault(payload, "reservationTime", ""); | ||
| String reservationId = getStringOrDefault(payload, "reservationId", ""); | ||
|
|
||
| String title = "새로운 예약이 접수되었습니다"; | ||
| String body = String.format("%s에 새로운 예약이 접수되었습니다. 예약 시간: %s", | ||
| restaurantName, reservationTime); | ||
| String url = "/reservations/" + reservationId; | ||
|
|
||
| return new NotificationTemplateData(title, body, url, true); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package com.wellmeet.notification.template.impl; | ||
|
|
||
| import com.wellmeet.notification.consumer.dto.NotificationType; | ||
| import com.wellmeet.notification.template.NotificationTemplate; | ||
| import com.wellmeet.notification.template.NotificationTemplateData; | ||
| import java.util.Map; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| public class ReservationReminderTemplate implements NotificationTemplate { | ||
|
|
||
| @Override | ||
| public boolean supports(NotificationType type) { | ||
| return NotificationType.RESERVATION_REMINDER == type; | ||
| } | ||
|
|
||
| @Override | ||
| public NotificationTemplateData create(Map<String, Object> payload) { | ||
| String restaurantName = getStringOrDefault(payload, "restaurantName", "식당"); | ||
| String reservationTime = getStringOrDefault(payload, "reservationTime", ""); | ||
| String reservationId = getStringOrDefault(payload, "reservationId", ""); | ||
| String minutesBefore = getStringOrDefault(payload, "minutesBefore", "30"); | ||
|
|
||
| String title = "곧 예약 시간입니다"; | ||
| String body = String.format("%s 예약 시간이 %s분 남았습니다. 예약 시간: %s", | ||
| restaurantName, minutesBefore, reservationTime); | ||
| String url = "/reservations/" + reservationId; | ||
|
|
||
| return new NotificationTemplateData(title, body, url, true); | ||
| } | ||
|
Comment on lines
+17
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain알림 데이터 구성이 적절합니다. 예약 리마인더 알림의 제목, 본문, URL이 올바르게 구성되었고, 단, 빈 문자열 기본값 처리를 확인해주세요. 다음 스크립트로 페이로드 검증 로직이 upstream에 존재하는지 확인할 수 있습니다: 🏁 Script executed: #!/bin/bash
# 알림 전송 시 페이로드 필수 필드 검증이 있는지 확인
# NotificationTemplateFactory를 사용하는 코드 검색
rg -n -C5 "createTemplateData|NotificationTemplateFactory" --type=java
# 페이로드 생성 및 검증 로직 검색
rg -n -C5 "restaurantName|reservationTime|reservationId" --type=java -g "!**/template/**"Length of output: 6016 페이로드 필수 필드 검증 필요 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.wellmeet.notification.template.impl; | ||
|
|
||
| import com.wellmeet.notification.consumer.dto.NotificationType; | ||
| import com.wellmeet.notification.template.NotificationTemplate; | ||
| import com.wellmeet.notification.template.NotificationTemplateData; | ||
| import java.util.Map; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| public class ReservationUpdatedTemplate implements NotificationTemplate { | ||
|
|
||
| @Override | ||
| public boolean supports(NotificationType type) { | ||
| return NotificationType.RESERVATION_UPDATED == type; | ||
| } | ||
|
|
||
| @Override | ||
| public NotificationTemplateData create(Map<String, Object> payload) { | ||
| String restaurantName = getStringOrDefault(payload, "restaurantName", "식당"); | ||
| String reservationTime = getStringOrDefault(payload, "reservationTime", ""); | ||
| String reservationId = getStringOrDefault(payload, "reservationId", ""); | ||
|
|
||
| String title = "예약 정보가 변경되었습니다"; | ||
| String body = String.format("%s 예약 정보가 변경되었습니다. 변경된 예약 시간: %s", | ||
| restaurantName, reservationTime); | ||
| String url = "/reservations/" + reservationId; | ||
|
|
||
| return new NotificationTemplateData(title, body, url, false); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,8 +6,9 @@ | |
| import com.wellmeet.notification.Sender; | ||
| import com.wellmeet.notification.consumer.dto.NotificationMessage; | ||
| import com.wellmeet.notification.domain.NotificationChannel; | ||
| import com.wellmeet.notification.template.NotificationTemplateData; | ||
| import com.wellmeet.notification.template.NotificationTemplateFactory; | ||
| import com.wellmeet.notification.webpush.domain.PushSubscription; | ||
| import com.wellmeet.notification.webpush.dto.TestPushRequest; | ||
| import com.wellmeet.notification.webpush.repository.PushSubscriptionRepository; | ||
| import java.io.IOException; | ||
| import java.security.GeneralSecurityException; | ||
|
|
@@ -29,6 +30,7 @@ public class WebPushSender implements Sender { | |
|
|
||
| private final PushSubscriptionRepository pushSubscriptionRepository; | ||
| private final PushService pushService; | ||
| private final NotificationTemplateFactory templateFactory; | ||
| private final ObjectMapper objectMapper = new ObjectMapper(); | ||
|
|
||
| @Override | ||
|
|
@@ -53,39 +55,21 @@ public void send(NotificationMessage message) { | |
| } | ||
|
|
||
| private Map<String, Object> getNotificationPayload(NotificationMessage message) { | ||
| Map<String, Object> notificationPayload = new HashMap<>(); | ||
| notificationPayload.put("title", "WellMeet 알림"); | ||
| notificationPayload.put("body", message.getPayload()); | ||
| notificationPayload.put("icon", "/icon-192x192.png"); | ||
| notificationPayload.put("badge", "/badge-72x72.png"); | ||
| notificationPayload.put("vibrate", new int[]{100, 50, 100}); | ||
| notificationPayload.put("requireInteraction", false); | ||
|
|
||
| Map<String, Object> defaultData = new HashMap<>(); | ||
| defaultData.put("url", "/notifications"); | ||
| defaultData.put("timestamp", System.currentTimeMillis()); | ||
| notificationPayload.put("data", defaultData); | ||
| return notificationPayload; | ||
| } | ||
|
|
||
| public void send(PushSubscription subscription, TestPushRequest request) { | ||
| Keys keys = new Keys(subscription.getP256dh(), subscription.getAuth()); | ||
| Subscription sub = new Subscription(subscription.getEndpoint(), keys); | ||
| Map<String, Object> notificationPayload = getNotificationPayload(request); | ||
| webPushSend(notificationPayload, sub); | ||
| } | ||
| NotificationTemplateData templateData = templateFactory.createTemplateData( | ||
| message.getNotification().getType(), | ||
| message.getPayload() | ||
| ); | ||
|
|
||
| private Map<String, Object> getNotificationPayload(TestPushRequest request) { | ||
| Map<String, Object> notificationPayload = new HashMap<>(); | ||
| notificationPayload.put("title", request.title()); | ||
| notificationPayload.put("body", request.body()); | ||
| notificationPayload.put("title", templateData.title()); | ||
| notificationPayload.put("body", templateData.body()); | ||
| notificationPayload.put("icon", "/icon-192x192.png"); | ||
| notificationPayload.put("badge", "/badge-72x72.png"); | ||
| notificationPayload.put("vibrate", new int[]{100, 50, 100}); | ||
| notificationPayload.put("requireInteraction", false); | ||
| notificationPayload.put("requireInteraction", templateData.requireInteraction()); | ||
|
|
||
| Map<String, Object> defaultData = new HashMap<>(); | ||
| defaultData.put("url", "/notifications"); | ||
| defaultData.put("url", templateData.url()); | ||
| defaultData.put("timestamp", System.currentTimeMillis()); | ||
| notificationPayload.put("data", defaultData); | ||
| return notificationPayload; | ||
|
Comment on lines
63
to
75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
람다 표현식의 파라미터 이름
low는 의미가 명확하지 않습니다. 가독성을 높이기 위해template과 같이 더 설명적인 이름으로 변경하는 것이 좋습니다.