From 3c21e40ac3614d4642f7e8515fd0f619c8a61dd8 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:34:53 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20FCM=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=9E=AC=EC=8B=9C=EB=8F=84=EC=8B=9C=20=EC=9E=AC=EC=8B=9C?= =?UTF-8?q?=EB=8F=84=20=EA=B0=80=EB=8A=A5=20=EC=97=AC=EB=B6=80=20=ED=8C=90?= =?UTF-8?q?=EB=8B=A8=EC=9D=84=20=EB=B3=84=EB=8F=84=EC=9D=98=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../implement/fcm/FcmResponseHandler.java | 37 +++-------- .../implement/fcm/FcmRetryableChecker.java | 63 +++++++++++++++++++ 2 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/FcmRetryableChecker.java diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmResponseHandler.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmResponseHandler.java index 8393a1e6..8afc5f25 100644 --- a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmResponseHandler.java +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmResponseHandler.java @@ -18,22 +18,18 @@ import mouda.backend.notification.domain.CommonNotification; import mouda.backend.notification.domain.FcmFailedResponse; import mouda.backend.notification.domain.FcmToken; -import mouda.backend.notification.implement.fcm.token.FcmTokenFinder; -import mouda.backend.notification.implement.fcm.token.FcmTokenWriter; @Component @RequiredArgsConstructor @Slf4j public class FcmResponseHandler { - private static final int MAX_ATTEMPT = 3; private static final int BACKOFF_DELAY_FOR_SECONDS = 10; private static final int BACKOFF_MULTIPLIER = 1; private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5); + private final FcmRetryableChecker fcmRetryableChecker; private final FcmMessageFactory fcmMessageFactory; - private final FcmTokenFinder fcmTokenFinder; - private final FcmTokenWriter fcmTokenWriter; @PreDestroy public void destroy() { @@ -41,10 +37,9 @@ public void destroy() { } public void handleBatchResponse( - BatchResponse batchResponse, CommonNotification notification, List initialTokens + BatchResponse batchResponse, CommonNotification notification, List initialTokens ) { - List tokens = fcmTokenFinder.readAllByTokensIn(initialTokens); - FcmFailedResponse failedResponse = FcmFailedResponse.from(batchResponse, tokens); + FcmFailedResponse failedResponse = FcmFailedResponse.from(batchResponse, initialTokens); int attempt = 1; retryAsync(notification, failedResponse, attempt, BACKOFF_DELAY_FOR_SECONDS); @@ -53,30 +48,12 @@ public void handleBatchResponse( private void retryAsync( CommonNotification notification, FcmFailedResponse failedResponse, int attempt, int backoffDelayForSeconds ) { - if (attempt > MAX_ATTEMPT) { - log.info("Max attempt reached for title: {}, body: {}, failed: {}", notification.getTitle(), - notification.getBody(), failedResponse.getFinallyFailedTokens()); - removeAllUnregisteredTokens(failedResponse.getFailedWith404Tokens()); - return; - } - if (failedResponse.hasNoRetryableTokens()) { - log.info("No Retryable tokens for title: {}, body: {}, failed: {}.", notification.getTitle(), - notification.getBody(), failedResponse.getNonRetryableFailedTokens()); - removeAllUnregisteredTokens(failedResponse.getFailedWith404Tokens()); - return; - } - retryUsingRetryAfter(notification, failedResponse, attempt, backoffDelayForSeconds); - retryUsingBackoff(notification, failedResponse, attempt, backoffDelayForSeconds); - } + boolean canRetry = fcmRetryableChecker.check(notification, failedResponse, attempt); - private void removeAllUnregisteredTokens(List failedWith404Tokens) { - if (failedWith404Tokens.isEmpty()) { - return; + if (canRetry) { + retryUsingRetryAfter(notification, failedResponse, attempt, backoffDelayForSeconds); + retryUsingBackoff(notification, failedResponse, attempt, backoffDelayForSeconds); } - log.info("Removing all unregistered tokens: {}", failedWith404Tokens); - List tokens = failedWith404Tokens.stream().map(FcmToken::getToken).toList(); - - fcmTokenWriter.deleteAll(tokens); } private void retryUsingRetryAfter( diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmRetryableChecker.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmRetryableChecker.java new file mode 100644 index 00000000..fcfcf319 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmRetryableChecker.java @@ -0,0 +1,63 @@ +package mouda.backend.notification.implement.fcm; + +import java.util.List; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.FcmFailedResponse; +import mouda.backend.notification.domain.FcmToken; +import mouda.backend.notification.implement.fcm.token.FcmTokenWriter; + +@Component +@RequiredArgsConstructor +@Slf4j +public class FcmRetryableChecker { + + private static final int MAX_ATTEMPT = 3; + + private final FcmTokenWriter fcmTokenWriter; + + @Transactional + public boolean check(CommonNotification notification, FcmFailedResponse failedResponse, int attempt) { + handleNonRetryableTokens(notification, failedResponse); + if (failedResponse.hasNoFailedTokens()) { + log.info("No failed tokens for title: {}, body: {}.", notification.getTitle(), notification.getBody()); + return false; + } + if (attempt > MAX_ATTEMPT) { + log.info("Max attempt reached for title: {}, body: {}, failed: {}", notification.getTitle(), + notification.getBody(), failedResponse.getFinallyFailedTokens()); + return false; + } + if (failedResponse.hasNoRetryableTokens()) { + log.info("No retryable tokens for title: {}, body: {}.", notification.getTitle(), notification.getBody()); + return false; + } + return true; + } + + private void handleNonRetryableTokens(CommonNotification notification, FcmFailedResponse failedResponse) { + List nonRetryableFailedTokens = failedResponse.getNonRetryableFailedTokens(); + if (nonRetryableFailedTokens.isEmpty()) { + return; + } + + log.info("Cannot Retry for title: {}, body: {}, failed: {}.", notification.getTitle(), + notification.getBody(), nonRetryableFailedTokens); + removeAllUnregisteredTokens(failedResponse.getFailedWith404Tokens()); + } + + private void removeAllUnregisteredTokens(List failedWith404Tokens) { + if (failedWith404Tokens.isEmpty()) { + return; + } + log.info("Removing all unregistered tokens: {}", failedWith404Tokens); + List tokens = failedWith404Tokens.stream().map(FcmToken::getToken).toList(); + + fcmTokenWriter.deleteAll(tokens); + } +} From 78f4c1f4f72312db4106b19df3f3e3a4654d78be Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:35:35 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=EB=B9=84=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=EB=A1=9C=20=EC=95=8C=EB=A6=BC=EC=9D=84=20=EC=A0=84=EC=86=A1?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=B3=84=EB=8F=84=EC=9D=98=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fcm/AsyncFcmNotificationSender.java | 69 +++++++++++++++++++ .../implement/fcm/FcmNotificationSender.java | 67 ++---------------- .../implement/fcm/token/FcmTokenFinder.java | 29 +++----- 3 files changed, 87 insertions(+), 78 deletions(-) create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/AsyncFcmNotificationSender.java diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/AsyncFcmNotificationSender.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/AsyncFcmNotificationSender.java new file mode 100644 index 00000000..aaa427a5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/AsyncFcmNotificationSender.java @@ -0,0 +1,69 @@ +package mouda.backend.notification.implement.fcm; + +import java.util.List; +import java.util.concurrent.Executors; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.MulticastMessage; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.FcmToken; + +@Component +@Slf4j +@RequiredArgsConstructor +public class AsyncFcmNotificationSender { + + private static final int THREAD_POOL_SIZE_FOR_CALLBACK = 5; + + private final FcmResponseHandler fcmResponseHandler; + + @Async + public void sendAllMulticastMessage( + CommonNotification notification, List messages, List tokens + ) { + if (tokens.isEmpty()) { + return; + } + + messages.forEach(multicastMessage -> sendSingleMulticastMessage(notification, multicastMessage, tokens)); + } + + private void sendSingleMulticastMessage( + CommonNotification notification, MulticastMessage message, List initialTokens + ) { + ApiFuture future = FirebaseMessaging.getInstance().sendEachForMulticastAsync(message); + ApiFutures.addCallback(future, new ApiFutureCallback<>() { + @Override + public void onFailure(Throwable t) { + if (t instanceof FirebaseMessagingException exception) { + log.error( + "Error Sending Message. title: {}, body: {}, error code: {}, messaging error code: {}, error message: {}", + notification.getTitle(), notification.getBody(), exception.getErrorCode(), + exception.getMessagingErrorCode(), exception.getMessage() + ); + } + } + + @Override + public void onSuccess(BatchResponse result) { + if (result.getFailureCount() == 0) { + log.info("All messages were sent successfully. title: {}, body: {}", notification.getTitle(), + notification.getBody()); + return; + } + fcmResponseHandler.handleBatchResponse(result, notification, initialTokens); + } + }, Executors.newFixedThreadPool(THREAD_POOL_SIZE_FOR_CALLBACK)); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmNotificationSender.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmNotificationSender.java index 21c6b327..13373857 100644 --- a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmNotificationSender.java +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmNotificationSender.java @@ -1,20 +1,12 @@ package mouda.backend.notification.implement.fcm; import java.util.List; -import java.util.concurrent.Executors; import org.springframework.stereotype.Component; -import com.google.api.core.ApiFuture; -import com.google.api.core.ApiFutureCallback; -import com.google.api.core.ApiFutures; -import com.google.firebase.messaging.BatchResponse; -import com.google.firebase.messaging.FirebaseMessaging; -import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.MulticastMessage; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import mouda.backend.notification.domain.CommonNotification; import mouda.backend.notification.domain.FcmToken; import mouda.backend.notification.domain.Recipient; @@ -22,66 +14,21 @@ import mouda.backend.notification.implement.fcm.token.FcmTokenFinder; @Component -@Slf4j @RequiredArgsConstructor public class FcmNotificationSender implements NotificationSender { - private static final int MAX_ATTEMPT = 3; - private static final int THREAD_POOL_SIZE_FOR_CALLBACK = 5; - private final FcmMessageFactory fcmMessageFactory; - private final FcmResponseHandler fcmResponseHandler; private final FcmTokenFinder fcmTokenFinder; + private final AsyncFcmNotificationSender asyncFcmNotificationSender; @Override public void sendNotification(CommonNotification notification, List recipients) { - List tokens = fcmTokenFinder.findAllTokensByMember(recipients); - sendAllMulticastMessage(notification, tokens); - } - - private void sendAllMulticastMessage(CommonNotification notification, List tokens) { - if (tokens.isEmpty()) { - return; - } - - int attempt = 1; - fcmMessageFactory.createMessage(notification, tokens) - .forEach(multicastMessage -> sendMulticastMessage(notification, multicastMessage, tokens, attempt)); - } - - private void sendMulticastMessage( - CommonNotification notification, MulticastMessage message, List initialTokens, int attempt - ) { - if (attempt > MAX_ATTEMPT) { - List tokens = fcmTokenFinder.readAllByTokensIn(initialTokens); - log.info("Max attempt reached for title: {}, body: {}, failed: {}", notification.getTitle(), - notification.getBody(), tokens); - return; - } - - ApiFuture future = FirebaseMessaging.getInstance().sendEachForMulticastAsync(message); - ApiFutures.addCallback(future, new ApiFutureCallback<>() { - @Override - public void onFailure(Throwable t) { - if (t instanceof FirebaseMessagingException exception) { - log.error( - "Error Sending Message. title: {}, body: {}, error code: {}, messaging error code: {}, error message: {}", - notification.getTitle(), notification.getBody(), exception.getMessagingErrorCode(), - exception.getMessagingErrorCode(), exception.getMessage() - ); - } - sendMulticastMessage(notification, message, initialTokens, attempt + 1); - } + List tokens = fcmTokenFinder.findAllTokensByMemberIn(recipients); + List tokenStrings = tokens.stream() + .map(FcmToken::getToken) + .toList(); - @Override - public void onSuccess(BatchResponse result) { - if (result.getFailureCount() == 0) { - log.info("All messages were sent successfully. title: {}, body: {}", notification.getTitle(), - notification.getBody()); - return; - } - fcmResponseHandler.handleBatchResponse(result, notification, initialTokens); - } - }, Executors.newFixedThreadPool(THREAD_POOL_SIZE_FOR_CALLBACK)); + List messages = fcmMessageFactory.createMessage(notification, tokenStrings); + asyncFcmNotificationSender.sendAllMulticastMessage(notification, messages, tokens); } } diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenFinder.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenFinder.java index 1800d63e..57b41782 100644 --- a/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenFinder.java +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenFinder.java @@ -1,6 +1,5 @@ package mouda.backend.notification.implement.fcm.token; -import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; @@ -17,24 +16,18 @@ public class FcmTokenFinder { private final FcmTokenRepository fcmTokenRepository; - public List findAllTokensByMember(List recipients) { - List fcmTokens = new ArrayList<>(); - for (Recipient recipient : recipients) { - List tokens = fcmTokenRepository.findAllByMemberId(recipient.getMemberId()) - .stream() - .map(FcmTokenEntity::getToken).toList(); - fcmTokens.addAll(tokens); - } - return fcmTokens; + public List findAllTokensByMemberIn(List recipients) { + return recipients.stream() + .flatMap(recipient -> fcmTokenRepository.findAllByMemberId(recipient.getMemberId()).stream()) + .map(this::createByEntity) + .toList(); } - public List readAllByTokensIn(List tokens) { - return fcmTokenRepository.findAllByTokenIn(tokens).stream() - .map(entity -> FcmToken.builder() - .memberId(entity.getMemberId()) - .token(entity.getToken()) - .build() - ) - .toList(); + private FcmToken createByEntity(FcmTokenEntity entity) { + return FcmToken.builder() + .tokenId(entity.getId()) + .memberId(entity.getMemberId()) + .token(entity.getToken()) + .build(); } } From c80d039e6414c008decf3deb8bb76649a870a74d Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:36:27 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B0=9D=EC=B2=B4=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 --- .../domain/NotificationSendEvent.java | 44 +++++++++++++ .../notification/domain/NotificationType.java | 4 ++ .../domain/NotificationSendEventTest.java | 66 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 backend/src/main/java/mouda/backend/notification/domain/NotificationSendEvent.java create mode 100644 backend/src/test/java/mouda/backend/notification/domain/NotificationSendEventTest.java diff --git a/backend/src/main/java/mouda/backend/notification/domain/NotificationSendEvent.java b/backend/src/main/java/mouda/backend/notification/domain/NotificationSendEvent.java new file mode 100644 index 00000000..c22d1d4b --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/NotificationSendEvent.java @@ -0,0 +1,44 @@ +package mouda.backend.notification.domain; + +import java.util.List; + +import org.springframework.http.HttpStatus; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.exception.NotificationErrorMessage; +import mouda.backend.notification.exception.NotificationException; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class NotificationSendEvent { + + private final CommonNotification notification; + private final List recipients; + private final Long darakbangId; + private final Long chatRoomId; + + public static NotificationSendEvent from(NotificationPayload payload) { + validate(payload); + return NotificationSendEvent.builder() + .notification(payload.toCommonNotification()) + .recipients(payload.getRecipients()) + .darakbangId(payload.getDarakbangId()) + .chatRoomId(payload.getChatRoomId()) + .build(); + } + + private static void validate(NotificationPayload payload) { + NotificationType notificationType = payload.getNotificationType(); + Long chatRoomId = payload.getChatRoomId(); + Long darakbangId = payload.getDarakbangId(); + + if (notificationType.isChatType() && (chatRoomId == null || darakbangId == null)) { + throw new NotificationException(HttpStatus.BAD_REQUEST, + NotificationErrorMessage.NULL_ID_VALUES_FOR_CHAT_NOTIFICATION); + } + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/NotificationType.java b/backend/src/main/java/mouda/backend/notification/domain/NotificationType.java index a3ff1daa..aacaa15d 100644 --- a/backend/src/main/java/mouda/backend/notification/domain/NotificationType.java +++ b/backend/src/main/java/mouda/backend/notification/domain/NotificationType.java @@ -23,4 +23,8 @@ public enum NotificationType { public boolean isConfirmedType() { return this == MOIM_PLACE_CONFIRMED || this == MOIM_TIME_CONFIRMED; } + + public boolean isChatType() { + return this == NEW_CHAT || isConfirmedType(); + } } diff --git a/backend/src/test/java/mouda/backend/notification/domain/NotificationSendEventTest.java b/backend/src/test/java/mouda/backend/notification/domain/NotificationSendEventTest.java new file mode 100644 index 00000000..a73c35b1 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/domain/NotificationSendEventTest.java @@ -0,0 +1,66 @@ +package mouda.backend.notification.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import mouda.backend.notification.exception.NotificationException; + +class NotificationSendEventTest { + + @DisplayName("알림 타입이 채팅이라면 다락방과 채팅방 ID가 null이 아니어야 한다.") + @Test + void fromPayload() { + NotificationPayload payload = NotificationPayload.createChatPayload( + NotificationType.MOIM_PLACE_CONFIRMED, + "모임 제목", + "메시지", + "url", + List.of(), + 1L, + 1L + ); + + assertThatCode(() -> NotificationSendEvent.from(payload)).doesNotThrowAnyException(); + + NotificationSendEvent event = NotificationSendEvent.from(payload); + assertThat(event).isNotNull(); + } + + @DisplayName("알림 타입이 채팅일 때 다락방 ID가 null이면 예외를 던진다.") + @Test + void nullDarakbangId() { + NotificationPayload payload = NotificationPayload.createChatPayload( + NotificationType.MOIM_PLACE_CONFIRMED, + "모임 제목", + "메시지", + "url", + List.of(), + null, + 1L + ); + + assertThatThrownBy(() -> NotificationSendEvent.from(payload)) + .isInstanceOf(NotificationException.class); + } + + @DisplayName("알림 타입이 채팅일 때 채팅방 ID가 null이면 예외를 던진다.") + @Test + void nullChatRoomId() { + NotificationPayload payload = NotificationPayload.createChatPayload( + NotificationType.MOIM_PLACE_CONFIRMED, + "모임 제목", + "메시지", + "url", + List.of(), + 1L, + null + ); + + assertThatThrownBy(() -> NotificationSendEvent.from(payload)) + .isInstanceOf(NotificationException.class); + } +} From c3b7aa19b67e23baf69af3afcbb882ef79ade706 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:37:57 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationSendEventHandler.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 backend/src/main/java/mouda/backend/notification/implement/NotificationSendEventHandler.java diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationSendEventHandler.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationSendEventHandler.java new file mode 100644 index 00000000..8c7ed6fe --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/NotificationSendEventHandler.java @@ -0,0 +1,35 @@ +package mouda.backend.notification.implement; + +import java.util.List; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationSendEvent; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.implement.filter.SubscriptionFilterRegistry; + +@Component +@RequiredArgsConstructor +public class NotificationSendEventHandler { + + private final SubscriptionFilterRegistry subscriptionFilterRegistry; + private final NotificationSender notificationSender; + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = NotificationSendEvent.class, phase = TransactionPhase.AFTER_COMMIT) + public void handle(NotificationSendEvent event) { + List filteredRecipients = filterRecipientsBySubscription(event); + notificationSender.sendNotification(event.getNotification(), filteredRecipients); + } + + private List filterRecipientsBySubscription(NotificationSendEvent event) { + return subscriptionFilterRegistry.getFilter(event.getNotification().getType()).filter(event); + } +} From 7a04bb62a28ddd43549f55304ba302c2c8529ac3 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:39:26 +0900 Subject: [PATCH 05/17] =?UTF-8?q?refactor:=20=EA=B5=AC=EB=8F=85=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=95=84=ED=84=B0=EC=9D=98=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=ED=83=80=EC=9E=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../implement/filter/ChatRoomSubscriptionFilter.java | 12 ++++++------ .../filter/MoimCreatedSubscriptionFilter.java | 6 +++--- .../implement/filter/NonSubscriptionFilter.java | 6 +++--- .../implement/filter/SubscriptionFilter.java | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilter.java b/backend/src/main/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilter.java index a4e78314..1144bdcd 100644 --- a/backend/src/main/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilter.java +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilter.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; -import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationSendEvent; import mouda.backend.notification.domain.NotificationType; import mouda.backend.notification.domain.Recipient; import mouda.backend.notification.domain.Subscription; @@ -24,15 +24,15 @@ public boolean support(NotificationType notificationType) { } @Override - public List filter(NotificationEvent notificationEvent) { - return notificationEvent.getRecipients().stream() + public List filter(NotificationSendEvent notificationSendEvent) { + return notificationSendEvent.getRecipients().stream() .filter(recipient -> { - // todo: 장소(시간) 확정 채팅은 알림이 가야함. - if (notificationEvent.getNotificationType().isConfirmedType()) { + NotificationType type = notificationSendEvent.getNotification().getType(); + if (type.isConfirmedType()) { return true; } Subscription subscription = subscriptionFinder.readSubscription(recipient.getMemberId()); - return subscription.isSubscribedChatRoom(notificationEvent.getDarakbangId(), notificationEvent.getChatRoomId()); + return subscription.isSubscribedChatRoom(notificationSendEvent.getDarakbangId(), notificationSendEvent.getChatRoomId()); }) .toList(); } diff --git a/backend/src/main/java/mouda/backend/notification/implement/filter/MoimCreatedSubscriptionFilter.java b/backend/src/main/java/mouda/backend/notification/implement/filter/MoimCreatedSubscriptionFilter.java index cf61d972..9543ed01 100644 --- a/backend/src/main/java/mouda/backend/notification/implement/filter/MoimCreatedSubscriptionFilter.java +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/MoimCreatedSubscriptionFilter.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; -import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationSendEvent; import mouda.backend.notification.domain.NotificationType; import mouda.backend.notification.domain.Recipient; import mouda.backend.notification.domain.Subscription; @@ -23,8 +23,8 @@ public boolean support(NotificationType notificationType) { } @Override - public List filter(NotificationEvent notificationEvent) { - return notificationEvent.getRecipients().stream() + public List filter(NotificationSendEvent notificationSendEvent) { + return notificationSendEvent.getRecipients().stream() .filter(recipient -> { Subscription subscription = subscriptionFinder.readSubscription(recipient.getMemberId()); return subscription.isSubscribedMoimCreate(); diff --git a/backend/src/main/java/mouda/backend/notification/implement/filter/NonSubscriptionFilter.java b/backend/src/main/java/mouda/backend/notification/implement/filter/NonSubscriptionFilter.java index f34d6d6f..6b8e846f 100644 --- a/backend/src/main/java/mouda/backend/notification/implement/filter/NonSubscriptionFilter.java +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/NonSubscriptionFilter.java @@ -4,7 +4,7 @@ import org.springframework.stereotype.Component; -import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationSendEvent; import mouda.backend.notification.domain.NotificationType; import mouda.backend.notification.domain.Recipient; @@ -19,7 +19,7 @@ public boolean support(NotificationType notificationType) { } @Override - public List filter(NotificationEvent notificationEvent) { - return notificationEvent.getRecipients(); + public List filter(NotificationSendEvent notificationSendEvent) { + return notificationSendEvent.getRecipients(); } } diff --git a/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilter.java b/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilter.java index 7e51a3ca..64593eb2 100644 --- a/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilter.java +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilter.java @@ -2,7 +2,7 @@ import java.util.List; -import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationSendEvent; import mouda.backend.notification.domain.NotificationType; import mouda.backend.notification.domain.Recipient; @@ -10,5 +10,5 @@ public interface SubscriptionFilter { boolean support(NotificationType notificationType); - List filter(NotificationEvent notificationEvent); + List filter(NotificationSendEvent notificationSendEvent); } From ed69d790a8d186cfe185008b27b860591d6133e3 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:39:42 +0900 Subject: [PATCH 06/17] =?UTF-8?q?refactor:=20=EA=B5=AC=EB=8F=85=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=95=84=ED=84=B0=EB=A5=BC=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=AC=20=EB=95=8C=EC=9D=98=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/NotificationErrorMessage.java | 6 +++++- .../filter/SubscriptionFilterRegistry.java | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/mouda/backend/notification/exception/NotificationErrorMessage.java b/backend/src/main/java/mouda/backend/notification/exception/NotificationErrorMessage.java index fa1694a7..3a27a1b6 100644 --- a/backend/src/main/java/mouda/backend/notification/exception/NotificationErrorMessage.java +++ b/backend/src/main/java/mouda/backend/notification/exception/NotificationErrorMessage.java @@ -7,7 +7,11 @@ @RequiredArgsConstructor public enum NotificationErrorMessage { - NOT_ALLOWED_NOTIFICATION_TYPE("지원하지 않는 알림 타입이에요."); + NOT_ALLOWED_NOTIFICATION_TYPE("지원하지 않는 알림 타입이에요."), + FILTER_NOT_FOUND("입력된 알림 타입에 대한 구독 필터를 찾을 수 없어요."), + FILTER_NOT_UNIQUE("입력된 알림 타입에 대한 구독 필터가 유일하지 않아요."), + NULL_ID_VALUES_FOR_CHAT_NOTIFICATION("채팅 알림을 보내기 위해선 다락방과 채팅방 ID가 필요해요."), + ; private final String message; } diff --git a/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilterRegistry.java b/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilterRegistry.java index 79fffe2b..5f6d742c 100644 --- a/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilterRegistry.java +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilterRegistry.java @@ -2,10 +2,13 @@ import java.util.List; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.exception.NotificationErrorMessage; +import mouda.backend.notification.exception.NotificationException; @Component @RequiredArgsConstructor @@ -14,9 +17,16 @@ public class SubscriptionFilterRegistry { private final List subscriptionFilters; public SubscriptionFilter getFilter(NotificationType notificationType) { - return subscriptionFilters.stream() + List filters = subscriptionFilters.stream() .filter(subscriptionFilter -> subscriptionFilter.support(notificationType)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("no such filter")); + .toList(); + + if (filters.isEmpty()) { + throw new NotificationException(HttpStatus.NOT_FOUND, NotificationErrorMessage.FILTER_NOT_FOUND); + } + if (filters.size() > 1) { + throw new NotificationException(HttpStatus.INTERNAL_SERVER_ERROR, NotificationErrorMessage.FILTER_NOT_UNIQUE); + } + return filters.get(0); } } From 4bf8b4262da7cd958ce4fe0fa74f0a32c57033db Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:41:14 +0900 Subject: [PATCH 07/17] =?UTF-8?q?rename:=20NotificationEvent=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EB=AA=85=20=EC=88=98=EC=A0=95(->Notification?= =?UTF-8?q?Payload)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...NotificationEvent.java => NotificationPayload.java} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename backend/src/main/java/mouda/backend/notification/domain/{NotificationEvent.java => NotificationPayload.java} (83%) diff --git a/backend/src/main/java/mouda/backend/notification/domain/NotificationEvent.java b/backend/src/main/java/mouda/backend/notification/domain/NotificationPayload.java similarity index 83% rename from backend/src/main/java/mouda/backend/notification/domain/NotificationEvent.java rename to backend/src/main/java/mouda/backend/notification/domain/NotificationPayload.java index 5aa43b74..2a2baaa6 100644 --- a/backend/src/main/java/mouda/backend/notification/domain/NotificationEvent.java +++ b/backend/src/main/java/mouda/backend/notification/domain/NotificationPayload.java @@ -8,7 +8,7 @@ @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class NotificationEvent { +public class NotificationPayload { private final NotificationType notificationType; private final String title; @@ -18,19 +18,19 @@ public class NotificationEvent { private final Long darakbangId; private final Long chatRoomId; - public static NotificationEvent nonChatEvent( + public static NotificationPayload createNonChatPayload( NotificationType notificationType, String title, String body, String redirectUrl, List recipients ) { - return new NotificationEvent( + return new NotificationPayload( notificationType, title, body, redirectUrl, recipients, null, null ); } - public static NotificationEvent chatEvent( + public static NotificationPayload createChatPayload( NotificationType notificationType, String title, String body, @@ -39,7 +39,7 @@ public static NotificationEvent chatEvent( Long darakbangId, Long chatRoomId ) { - return new NotificationEvent( + return new NotificationPayload( notificationType, title, body, redirectUrl, recipients, darakbangId, chatRoomId ); } From 6634ccf60e83b6dad808a429871d2f1fbf58f657 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:42:11 +0900 Subject: [PATCH 08/17] =?UTF-8?q?refactor:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20&=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=9C=ED=96=89=20=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B4=EC=97=90=EB=94=B0=EB=A5=B8=20Notification?= =?UTF-8?q?Service=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/NotificationService.java | 39 ------------------- .../implement/NotificationProcessor.java | 26 +++++++++++++ .../implement/NotificationWriter.java | 7 +++- 3 files changed, 32 insertions(+), 40 deletions(-) delete mode 100644 backend/src/main/java/mouda/backend/notification/business/NotificationService.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/NotificationProcessor.java diff --git a/backend/src/main/java/mouda/backend/notification/business/NotificationService.java b/backend/src/main/java/mouda/backend/notification/business/NotificationService.java deleted file mode 100644 index 3bd2a2b8..00000000 --- a/backend/src/main/java/mouda/backend/notification/business/NotificationService.java +++ /dev/null @@ -1,39 +0,0 @@ -package mouda.backend.notification.business; - -import java.util.List; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; - -import lombok.RequiredArgsConstructor; -import mouda.backend.notification.domain.CommonNotification; -import mouda.backend.notification.domain.NotificationEvent; -import mouda.backend.notification.domain.Recipient; -import mouda.backend.notification.implement.NotificationSender; -import mouda.backend.notification.implement.NotificationWriter; -import mouda.backend.notification.implement.filter.SubscriptionFilter; -import mouda.backend.notification.implement.filter.SubscriptionFilterRegistry; - -@Service -@RequiredArgsConstructor -public class NotificationService { - - private final NotificationWriter notificationWriter; - private final SubscriptionFilterRegistry subscriptionFilterRegistry; - private final NotificationSender notificationSender; - - @TransactionalEventListener(classes = NotificationEvent.class, phase = TransactionPhase.AFTER_COMMIT) - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void sendNotification(NotificationEvent notificationEvent) { - CommonNotification commonNotification = notificationEvent.toCommonNotification(); - notificationWriter.saveAllMemberNotification(commonNotification, notificationEvent.getRecipients()); - - SubscriptionFilter subscriptionFilter = subscriptionFilterRegistry.getFilter(notificationEvent.getNotificationType()); - List filteredRecipients = subscriptionFilter.filter(notificationEvent); - - notificationSender.sendNotification(commonNotification, filteredRecipients); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationProcessor.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationProcessor.java new file mode 100644 index 00000000..3d0c6d49 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/NotificationProcessor.java @@ -0,0 +1,26 @@ +package mouda.backend.notification.implement; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationSendEvent; + +@Component +@RequiredArgsConstructor +public class NotificationProcessor { + + private final NotificationWriter notificationWriter; + private final ApplicationEventPublisher notificationSendEventPublisher; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void process(NotificationPayload payload) { + notificationWriter.saveMemberNotification(payload); + + NotificationSendEvent event = NotificationSendEvent.from(payload); + notificationSendEventPublisher.publishEvent(event); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationWriter.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationWriter.java index 2a8c3b22..e4f80cc7 100644 --- a/backend/src/main/java/mouda/backend/notification/implement/NotificationWriter.java +++ b/backend/src/main/java/mouda/backend/notification/implement/NotificationWriter.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.NotificationPayload; import mouda.backend.notification.domain.NotificationType; import mouda.backend.notification.domain.Recipient; import mouda.backend.notification.infrastructure.entity.MemberNotificationEntity; @@ -17,10 +18,14 @@ public class NotificationWriter { private final MemberNotificationRepository memberNotificationRepository; - public void saveAllMemberNotification(CommonNotification notification, List recipients) { + public void saveMemberNotification(NotificationPayload notificationPayload) { + CommonNotification notification = notificationPayload.toCommonNotification(); + List recipients = notificationPayload.getRecipients(); + if (notification.getType() == NotificationType.NEW_CHAT) { return; } + List memberNotifications = recipients.stream() .map(recipient -> createEntity(notification, recipient)) .toList(); From 244bc899aad685a9851c40c8495f18ca4d7da137 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:42:48 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=95=88=EC=97=90=EC=84=9C=EC=9D=98=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=A0=84=EC=86=A1(=3D=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=9C=ED=96=89)=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/moim/business/MoimService.java | 14 ++-- .../MoimRelatedNotificationSender.java | 70 +++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRelatedNotificationSender.java diff --git a/backend/src/main/java/mouda/backend/moim/business/MoimService.java b/backend/src/main/java/mouda/backend/moim/business/MoimService.java index 2590b8ea..bddb1d9d 100644 --- a/backend/src/main/java/mouda/backend/moim/business/MoimService.java +++ b/backend/src/main/java/mouda/backend/moim/business/MoimService.java @@ -15,7 +15,7 @@ import mouda.backend.moim.domain.ParentComment; import mouda.backend.moim.implement.finder.CommentFinder; import mouda.backend.moim.implement.finder.MoimFinder; -import mouda.backend.moim.implement.sender.MoimNotificationSender; +import mouda.backend.moim.implement.notificiation.MoimRelatedNotificationSender; import mouda.backend.moim.implement.writer.MoimWriter; import mouda.backend.moim.presentation.request.moim.MoimCreateRequest; import mouda.backend.moim.presentation.request.moim.MoimEditRequest; @@ -32,8 +32,8 @@ public class MoimService { private final MoimWriter moimWriter; private final MoimFinder moimFinder; private final CommentFinder commentFinder; - private final MoimNotificationSender moimNotificationSender; private final ChatRoomFinder chatRoomFinder; + private final MoimRelatedNotificationSender notificationSender; @Transactional(readOnly = true) public MoimDetailsFindResponse findMoimDetails(long darakbangId, long moimId) { @@ -76,7 +76,7 @@ public MoimFindAllResponses findZzimedMoim(DarakbangMember darakbangMember) { public Moim createMoim(Long darakbangId, DarakbangMember darakbangMember, MoimCreateRequest moimCreateRequest) { Moim moim = moimWriter.save(moimCreateRequest.toEntity(darakbangId), darakbangMember); - moimNotificationSender.sendMoimCreatedNotification(moim, darakbangMember, NotificationType.MOIM_CREATED); + notificationSender.sendMoimCreatedNotification(moim, darakbangMember, NotificationType.MOIM_CREATED); return moim; } @@ -84,21 +84,21 @@ public void completeMoim(Long darakbangId, Long moimId, DarakbangMember darakban Moim moim = moimFinder.read(moimId, darakbangId); moimWriter.completeMoim(moim, darakbangMember); - moimNotificationSender.sendMoimStatusChangedNotification(moim, NotificationType.MOIMING_COMPLETED); + notificationSender.sendMoimStatusChangeNotification(moim, NotificationType.MOIMING_COMPLETED); } public void cancelMoim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { Moim moim = moimFinder.read(moimId, darakbangId); moimWriter.cancelMoim(moim, darakbangMember); - moimNotificationSender.sendMoimStatusChangedNotification(moim, NotificationType.MOIM_CANCELLED); + notificationSender.sendMoimStatusChangeNotification(moim, NotificationType.MOIM_CANCELLED); } public void reopenMoim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { Moim moim = moimFinder.read(moimId, darakbangId); moimWriter.reopenMoim(moim, darakbangMember); - moimNotificationSender.sendMoimStatusChangedNotification(moim, NotificationType.MOINING_REOPENED); + notificationSender.sendMoimStatusChangeNotification(moim, NotificationType.MOINING_REOPENED); } public void editMoim(Long darakbangId, MoimEditRequest request, DarakbangMember darakbangMember) { @@ -107,6 +107,6 @@ public void editMoim(Long darakbangId, MoimEditRequest request, DarakbangMember moimWriter.updateMoim(moim, darakbangMember, request.title(), request.date(), request.time(), request.place(), request.maxPeople(), request.description()); - moimNotificationSender.sendMoimInfoModifiedNotification(moim, oldTitle, NotificationType.MOIM_MODIFIED); + notificationSender.sendMoimEditedNotification(moim, oldTitle, NotificationType.MOIM_MODIFIED); } } diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRelatedNotificationSender.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRelatedNotificationSender.java new file mode 100644 index 00000000..0061c379 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRelatedNotificationSender.java @@ -0,0 +1,70 @@ +package mouda.backend.moim.implement.notificiation; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.notificiation.event.ChamyoNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.CommentNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.MoimCreateNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.MoimEditedNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.MoimStatusChangeNotificationEvent; +import mouda.backend.notification.domain.NotificationType; + +@Component +@RequiredArgsConstructor +public class MoimRelatedNotificationSender { + + private final ApplicationEventPublisher eventPublisher; + + public void sendMoimCreatedNotification(Moim moim, DarakbangMember host, NotificationType notificationType) { + MoimCreateNotificationEvent event = MoimCreateNotificationEvent.builder() + .moim(moim) + .host(host) + .notificationType(notificationType) + .build(); + + eventPublisher.publishEvent(event); + } + + public void sendMoimEditedNotification(Moim moim, String oldMoimTitle, NotificationType notificationType) { + MoimEditedNotificationEvent event = MoimEditedNotificationEvent.builder() + .moim(moim) + .oldMoimTitle(oldMoimTitle) + .notificationType(notificationType) + .build(); + + eventPublisher.publishEvent(event); + } + + public void sendMoimStatusChangeNotification(Moim moim, NotificationType notificationType) { + MoimStatusChangeNotificationEvent event = MoimStatusChangeNotificationEvent.builder() + .moim(moim) + .notificationType(notificationType) + .build(); + + eventPublisher.publishEvent(event); + } + + public void sendChamyoNotification(Chamyo chamyo, NotificationType notificationType) { + ChamyoNotificationEvent event = ChamyoNotificationEvent.builder() + .chamyo(chamyo) + .notificationType(notificationType) + .build(); + + eventPublisher.publishEvent(event); + } + + public void sendCommentNotification(Comment comment, DarakbangMember darakbangMember) { + CommentNotificationEvent event = CommentNotificationEvent.builder() + .comment(comment) + .author(darakbangMember) + .build(); + + eventPublisher.publishEvent(event); + } +} From 6a8577cf31b766813167b9567519816061e3a221 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:44:29 +0900 Subject: [PATCH 10/17] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=95=88=EC=97=90=EC=84=9C=EC=9D=98=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9D=EC=B2=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...AbstractMoimRelatedNotificationEventHandler.java} | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) rename backend/src/main/java/mouda/backend/moim/implement/{sender/AbstractMoimNotificationSender.java => notificiation/AbstractMoimRelatedNotificationEventHandler.java} (59%) diff --git a/backend/src/main/java/mouda/backend/moim/implement/sender/AbstractMoimNotificationSender.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/AbstractMoimRelatedNotificationEventHandler.java similarity index 59% rename from backend/src/main/java/mouda/backend/moim/implement/sender/AbstractMoimNotificationSender.java rename to backend/src/main/java/mouda/backend/moim/implement/notificiation/AbstractMoimRelatedNotificationEventHandler.java index 713e2dc8..b5837da4 100644 --- a/backend/src/main/java/mouda/backend/moim/implement/sender/AbstractMoimNotificationSender.java +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/AbstractMoimRelatedNotificationEventHandler.java @@ -1,23 +1,21 @@ -package mouda.backend.moim.implement.sender; +package mouda.backend.moim.implement.notificiation; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; import mouda.backend.common.config.UrlConfig; +import mouda.backend.notification.implement.NotificationProcessor; @Component @RequiredArgsConstructor @EnableConfigurationProperties(UrlConfig.class) -public abstract class AbstractMoimNotificationSender { +public abstract class AbstractMoimRelatedNotificationEventHandler { - private final UrlConfig urlConfig; + protected final UrlConfig urlConfig; + protected final NotificationProcessor notificationProcessor; protected String getMoimUrl(long darakbangId, long moimId) { return urlConfig.getMoimUrl(darakbangId, moimId); } - - protected String getChatRoomUrl(long darakbangId, long moimId) { - return urlConfig.getChatRoomUrl(darakbangId, moimId); - } } From 63b51137a1573d8f278ca128dc8bde818c3a74ce Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:45:01 +0900 Subject: [PATCH 11/17] =?UTF-8?q?feat:=20=EC=B0=B8=EC=97=AC=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20=EA=B0=9D=EC=B2=B4=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 --- .../backend/moim/business/ChamyoService.java | 8 +-- .../ChamyoNotificationEventHandler.java | 71 +++++++++++++++++++ .../ChamyoRecipientFinder.java | 7 +- .../event/ChamyoNotificationEvent.java | 26 +++++++ .../sender/ChamyoNotificationSender.java | 58 --------------- .../finder/ChamyoRecipientFinderTest.java | 3 +- 6 files changed, 107 insertions(+), 66 deletions(-) create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoNotificationEventHandler.java rename backend/src/main/java/mouda/backend/moim/implement/{finder => notificiation}/ChamyoRecipientFinder.java (74%) create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/event/ChamyoNotificationEvent.java delete mode 100644 backend/src/main/java/mouda/backend/moim/implement/sender/ChamyoNotificationSender.java diff --git a/backend/src/main/java/mouda/backend/moim/business/ChamyoService.java b/backend/src/main/java/mouda/backend/moim/business/ChamyoService.java index d625ea34..fb5f6bd5 100644 --- a/backend/src/main/java/mouda/backend/moim/business/ChamyoService.java +++ b/backend/src/main/java/mouda/backend/moim/business/ChamyoService.java @@ -12,7 +12,7 @@ import mouda.backend.moim.domain.MoimRole; import mouda.backend.moim.implement.finder.ChamyoFinder; import mouda.backend.moim.implement.finder.MoimFinder; -import mouda.backend.moim.implement.sender.ChamyoNotificationSender; +import mouda.backend.moim.implement.notificiation.MoimRelatedNotificationSender; import mouda.backend.moim.implement.writer.ChamyoWriter; import mouda.backend.moim.implement.writer.MoimWriter; import mouda.backend.moim.presentation.response.chamyo.ChamyoFindAllResponses; @@ -28,7 +28,7 @@ public class ChamyoService { private final MoimWriter moimWriter; private final ChamyoFinder chamyoFinder; private final ChamyoWriter chamyoWriter; - private final ChamyoNotificationSender chamyoNotificationSender; + private final MoimRelatedNotificationSender notificationSender; @Transactional(readOnly = true) public MoimRoleFindResponse findMoimRole(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { @@ -51,7 +51,7 @@ public void chamyoMoim(Long darakbangId, Long moimId, DarakbangMember darakbangM Chamyo chamyo = chamyoWriter.saveAsMoimee(moim, darakbangMember); moimWriter.updateMoimStatusIfFull(moim); - chamyoNotificationSender.sendChamyoNotification(moimId, darakbangMember, NotificationType.NEW_MOIMEE_JOINED); + notificationSender.sendChamyoNotification(chamyo, NotificationType.NEW_MOIMEE_JOINED); } public void cancelChamyo(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { @@ -59,6 +59,6 @@ public void cancelChamyo(Long darakbangId, Long moimId, DarakbangMember darakban Chamyo chamyo = chamyoFinder.read(moim, darakbangMember); chamyoWriter.delete(chamyo); - chamyoNotificationSender.sendChamyoNotification(moimId, darakbangMember, NotificationType.MOIMEE_LEFT); + notificationSender.sendChamyoNotification(chamyo, NotificationType.MOIMEE_LEFT); } } diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoNotificationEventHandler.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoNotificationEventHandler.java new file mode 100644 index 00000000..545398a2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoNotificationEventHandler.java @@ -0,0 +1,71 @@ +package mouda.backend.moim.implement.notificiation; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import mouda.backend.common.config.UrlConfig; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.notificiation.event.ChamyoNotificationEvent; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.exception.NotificationErrorMessage; +import mouda.backend.notification.exception.NotificationException; +import mouda.backend.notification.implement.NotificationProcessor; + +@Component +public class ChamyoNotificationEventHandler extends AbstractMoimRelatedNotificationEventHandler { + + private final ChamyoRecipientFinder chamyoRecipientFinder; + + public ChamyoNotificationEventHandler( + UrlConfig urlConfig, NotificationProcessor notificationProcessor, ChamyoRecipientFinder chamyoRecipientFinder + ) { + super(urlConfig, notificationProcessor); + this.chamyoRecipientFinder = chamyoRecipientFinder; + } + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = ChamyoNotificationEvent.class, phase = TransactionPhase.AFTER_COMMIT) + public void handleChamyoNotificationEvent(ChamyoNotificationEvent event) { + Moim moim = event.getMoim(); + DarakbangMember updatedMember = event.getUpdatedMember(); + Darakbang darakbang = updatedMember.getDarakbang(); + + List recipients = chamyoRecipientFinder.getChamyoNotificationRecipients(moim, updatedMember); + + NotificationPayload payload = NotificationPayload.createNonChatPayload( + event.getNotificationType(), + darakbang.getName(), + ChamyoNotificationMessage.create(updatedMember.getNickname(), moim.getTitle(), event.getNotificationType()), + getMoimUrl(darakbang.getId(), moim.getId()), + recipients + ); + notificationProcessor.process(payload); + } + + static class ChamyoNotificationMessage { + + public static String create(String updatedMemberName, String moimTitle, NotificationType type) { + if (type == NotificationType.NEW_MOIMEE_JOINED) { + return updatedMemberName + "님이 " + moimTitle + " 모임에 참여했어요!"; + } + if (type == NotificationType.MOIMEE_LEFT) { + return updatedMemberName + "님이 " + moimTitle + " 모임 참여를 취소했어요!"; + } + throw new NotificationException( + HttpStatus.BAD_REQUEST, NotificationErrorMessage.NOT_ALLOWED_NOTIFICATION_TYPE + ); + } + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinder.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoRecipientFinder.java similarity index 74% rename from backend/src/main/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinder.java rename to backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoRecipientFinder.java index 0063ae6d..fe244847 100644 --- a/backend/src/main/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinder.java +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoRecipientFinder.java @@ -1,4 +1,4 @@ -package mouda.backend.moim.implement.finder; +package mouda.backend.moim.implement.notificiation; import java.util.List; @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import mouda.backend.darakbangmember.domain.DarakbangMember; import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; import mouda.backend.moim.infrastructure.ChamyoRepository; import mouda.backend.notification.domain.Recipient; @@ -16,8 +17,8 @@ public class ChamyoRecipientFinder { private final ChamyoRepository chamyoRepository; - public List getChamyoNotificationRecipients(long moimId, DarakbangMember updatedMember) { - List chamyos = chamyoRepository.findAllByMoimId(moimId); + public List getChamyoNotificationRecipients(Moim moim, DarakbangMember updatedMember) { + List chamyos = chamyoRepository.findAllByMoimId(moim.getId()); return chamyos.stream() .filter(chamyo -> chamyo.isNotSameMember(updatedMember)) .map(Chamyo::getDarakbangMember) diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/ChamyoNotificationEvent.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/ChamyoNotificationEvent.java new file mode 100644 index 00000000..6d0f1703 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/ChamyoNotificationEvent.java @@ -0,0 +1,26 @@ +package mouda.backend.moim.implement.notificiation.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.notification.domain.NotificationType; + +@Getter +@RequiredArgsConstructor +@Builder +public class ChamyoNotificationEvent { + + private final Chamyo chamyo; + private final NotificationType notificationType; + + public Moim getMoim() { + return chamyo.getMoim(); + } + + public DarakbangMember getUpdatedMember() { + return chamyo.getDarakbangMember(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/sender/ChamyoNotificationSender.java b/backend/src/main/java/mouda/backend/moim/implement/sender/ChamyoNotificationSender.java deleted file mode 100644 index 71f777e6..00000000 --- a/backend/src/main/java/mouda/backend/moim/implement/sender/ChamyoNotificationSender.java +++ /dev/null @@ -1,58 +0,0 @@ -package mouda.backend.moim.implement.sender; - -import java.util.List; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; - -import mouda.backend.common.config.UrlConfig; -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.implement.finder.ChamyoRecipientFinder; -import mouda.backend.notification.domain.NotificationEvent; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.Recipient; -import mouda.backend.notification.exception.NotificationErrorMessage; -import mouda.backend.notification.exception.NotificationException; - -@Component -public class ChamyoNotificationSender extends AbstractMoimNotificationSender { - - private final ChamyoRecipientFinder chamyoRecipientFinder; - private final ApplicationEventPublisher eventPublisher; - - public ChamyoNotificationSender(UrlConfig urlConfig, ChamyoRecipientFinder chamyoRecipientFinder, - ApplicationEventPublisher eventPublisher) { - super(urlConfig); - this.chamyoRecipientFinder = chamyoRecipientFinder; - this.eventPublisher = eventPublisher; - } - - public void sendChamyoNotification(long moimId, DarakbangMember updatedMember, NotificationType notificationType) { - List recipients = chamyoRecipientFinder.getChamyoNotificationRecipients(moimId, updatedMember); - NotificationEvent notificationEvent = NotificationEvent.nonChatEvent( - notificationType, - updatedMember.getDarakbang().getName(), - ChamyoNotificationMessage.create(updatedMember.getNickname(), notificationType), - getMoimUrl(updatedMember.getDarakbang().getId(), moimId), - recipients - ); - - eventPublisher.publishEvent(notificationEvent); - } - - static class ChamyoNotificationMessage { - - public static String create(String updatedMemberName, NotificationType type) { - if (type == NotificationType.NEW_MOIMEE_JOINED) { - return updatedMemberName + "님이 모임에 참여했어요!"; - } - if (type == NotificationType.MOIMEE_LEFT) { - return updatedMemberName + "님이 참여를 취소했어요!"; - } - throw new NotificationException( - HttpStatus.BAD_REQUEST, NotificationErrorMessage.NOT_ALLOWED_NOTIFICATION_TYPE - ); - } - } -} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinderTest.java index ace3c200..8bebb0e1 100644 --- a/backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinderTest.java +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinderTest.java @@ -14,6 +14,7 @@ import mouda.backend.moim.domain.Chamyo; import mouda.backend.moim.domain.Moim; import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.implement.notificiation.ChamyoRecipientFinder; import mouda.backend.moim.infrastructure.ChamyoRepository; import mouda.backend.moim.infrastructure.MoimRepository; import mouda.backend.notification.domain.Recipient; @@ -44,7 +45,7 @@ void getChamyoNotificationRecipients() { chamyoRepository.save(chamyoWithMoimerAnna); // when - List recipients = chamyoRecipientFinder.getChamyoNotificationRecipients(savedMoim.getId(), darakbangHogee); + List recipients = chamyoRecipientFinder.getChamyoNotificationRecipients(savedMoim, darakbangHogee); //then assertThat(recipients).hasSize(1); From e9614967e2f5854a30ffa565ba7eb3025c7f53b6 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:45:48 +0900 Subject: [PATCH 12/17] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20=EA=B0=9D=EC=B2=B4=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 --- .../backend/moim/business/CommentService.java | 6 +- .../CommentNotificationEventHandler.java | 78 ++++++++++++ .../CommentRecipientFinder.java | 3 +- .../notificiation}/CommentRecipients.java | 2 +- .../event/CommentNotificationEvent.java | 16 +++ .../sender/CommentNotificationSender.java | 69 ----------- .../moim/business/CommentAsyncTest.java | 113 ++++++++++++++++++ .../finder/CommentRecipientsFinderTest.java | 3 +- 8 files changed, 214 insertions(+), 76 deletions(-) create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentNotificationEventHandler.java rename backend/src/main/java/mouda/backend/moim/implement/{finder => notificiation}/CommentRecipientFinder.java (97%) rename backend/src/main/java/mouda/backend/moim/{domain => implement/notificiation}/CommentRecipients.java (95%) create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/event/CommentNotificationEvent.java delete mode 100644 backend/src/main/java/mouda/backend/moim/implement/sender/CommentNotificationSender.java create mode 100644 backend/src/test/java/mouda/backend/moim/business/CommentAsyncTest.java diff --git a/backend/src/main/java/mouda/backend/moim/business/CommentService.java b/backend/src/main/java/mouda/backend/moim/business/CommentService.java index f36ce619..010fd8c7 100644 --- a/backend/src/main/java/mouda/backend/moim/business/CommentService.java +++ b/backend/src/main/java/mouda/backend/moim/business/CommentService.java @@ -8,7 +8,7 @@ import mouda.backend.moim.domain.Comment; import mouda.backend.moim.domain.Moim; import mouda.backend.moim.implement.finder.MoimFinder; -import mouda.backend.moim.implement.sender.CommentNotificationSender; +import mouda.backend.moim.implement.notificiation.MoimRelatedNotificationSender; import mouda.backend.moim.implement.writer.CommentWriter; import mouda.backend.moim.presentation.request.comment.CommentCreateRequest; @@ -19,7 +19,7 @@ public class CommentService { private final MoimFinder moimFinder; private final CommentWriter commentWriter; - private final CommentNotificationSender commentNotificationSender; + private final MoimRelatedNotificationSender notificationSender; public void createComment( Long darakbangId, Long moimId, DarakbangMember darakbangMember, CommentCreateRequest request @@ -27,6 +27,6 @@ public void createComment( Moim moim = moimFinder.read(moimId, darakbangId); Comment comment = commentWriter.saveComment(moim, darakbangMember, request.parentId(), request.content()); - commentNotificationSender.sendCommentNotification(comment, darakbangMember); + notificationSender.sendCommentNotification(comment, darakbangMember); } } diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentNotificationEventHandler.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentNotificationEventHandler.java new file mode 100644 index 00000000..f3c6b036 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentNotificationEventHandler.java @@ -0,0 +1,78 @@ +package mouda.backend.moim.implement.notificiation; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import mouda.backend.common.config.UrlConfig; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.notificiation.event.CommentNotificationEvent; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.exception.NotificationErrorMessage; +import mouda.backend.notification.exception.NotificationException; +import mouda.backend.notification.implement.NotificationProcessor; + +@Component +public class CommentNotificationEventHandler extends AbstractMoimRelatedNotificationEventHandler { + + private final CommentRecipientFinder commentRecipientFinder; + + public CommentNotificationEventHandler(UrlConfig urlConfig, NotificationProcessor notificationProcessor, + CommentRecipientFinder commentRecipientFinder) { + super(urlConfig, notificationProcessor); + this.commentRecipientFinder = commentRecipientFinder; + } + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = CommentNotificationEvent.class, phase = TransactionPhase.AFTER_COMMIT) + public void handleCommentNotificationEvent(CommentNotificationEvent event) { + Comment comment = event.getComment(); + DarakbangMember author = event.getAuthor(); + + CommentRecipients commentRecipients = commentRecipientFinder.getAllRecipient(comment); + + commentRecipients.getRecipients() + .forEach((type, recipients) -> processNotification(type, recipients, comment, author)); + } + + private void processNotification( + NotificationType notificationType, List recipients, Comment comment, DarakbangMember author + ) { + Moim moim = comment.getMoim(); + NotificationPayload payload = NotificationPayload.createNonChatPayload( + notificationType, + moim.getTitle(), + CommentNotificationMessage.create(author.getNickname(), notificationType), + getMoimUrl(moim.getDarakbangId(), moim.getId()), + recipients + ); + + notificationProcessor.process(payload); + } + + static class CommentNotificationMessage { + + public static String create(String author, NotificationType type) { + if (type == NotificationType.NEW_COMMENT) { + return author + "님이 댓글을 남겼어요!"; + } + if (type == NotificationType.NEW_REPLY) { + return author + "님이 답글을 남겼어요!"; + } + throw new NotificationException( + HttpStatus.BAD_REQUEST, NotificationErrorMessage.NOT_ALLOWED_NOTIFICATION_TYPE + ); + } + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/CommentRecipientFinder.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipientFinder.java similarity index 97% rename from backend/src/main/java/mouda/backend/moim/implement/finder/CommentRecipientFinder.java rename to backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipientFinder.java index ac4d883c..36a0f9f8 100644 --- a/backend/src/main/java/mouda/backend/moim/implement/finder/CommentRecipientFinder.java +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipientFinder.java @@ -1,4 +1,4 @@ -package mouda.backend.moim.implement.finder; +package mouda.backend.moim.implement.notificiation; import java.util.Optional; @@ -9,7 +9,6 @@ import mouda.backend.darakbangmember.domain.DarakbangMember; import mouda.backend.moim.domain.Chamyo; import mouda.backend.moim.domain.Comment; -import mouda.backend.moim.domain.CommentRecipients; import mouda.backend.moim.domain.Moim; import mouda.backend.moim.exception.ChamyoErrorMessage; import mouda.backend.moim.exception.ChamyoException; diff --git a/backend/src/main/java/mouda/backend/moim/domain/CommentRecipients.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipients.java similarity index 95% rename from backend/src/main/java/mouda/backend/moim/domain/CommentRecipients.java rename to backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipients.java index e1470a32..c148e03a 100644 --- a/backend/src/main/java/mouda/backend/moim/domain/CommentRecipients.java +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipients.java @@ -1,4 +1,4 @@ -package mouda.backend.moim.domain; +package mouda.backend.moim.implement.notificiation; import java.util.ArrayList; import java.util.List; diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/CommentNotificationEvent.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/CommentNotificationEvent.java new file mode 100644 index 00000000..2c9e12a7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/CommentNotificationEvent.java @@ -0,0 +1,16 @@ +package mouda.backend.moim.implement.notificiation.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Comment; + +@Getter +@RequiredArgsConstructor +@Builder +public class CommentNotificationEvent { + + private final Comment comment; + private final DarakbangMember author; +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/sender/CommentNotificationSender.java b/backend/src/main/java/mouda/backend/moim/implement/sender/CommentNotificationSender.java deleted file mode 100644 index a14f7ec3..00000000 --- a/backend/src/main/java/mouda/backend/moim/implement/sender/CommentNotificationSender.java +++ /dev/null @@ -1,69 +0,0 @@ -package mouda.backend.moim.implement.sender; - -import java.util.List; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; - -import mouda.backend.common.config.UrlConfig; -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Comment; -import mouda.backend.moim.domain.CommentRecipients; -import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.implement.finder.CommentRecipientFinder; -import mouda.backend.notification.domain.NotificationEvent; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.Recipient; -import mouda.backend.notification.exception.NotificationErrorMessage; -import mouda.backend.notification.exception.NotificationException; - -@Component -public class CommentNotificationSender extends AbstractMoimNotificationSender { - - private final CommentRecipientFinder commentRecipientFinder; - private final ApplicationEventPublisher eventPublisher; - - public CommentNotificationSender(UrlConfig urlConfig, CommentRecipientFinder commentRecipientFinder, - ApplicationEventPublisher eventPublisher) { - super(urlConfig); - this.commentRecipientFinder = commentRecipientFinder; - this.eventPublisher = eventPublisher; - } - - public void sendCommentNotification(Comment comment, DarakbangMember author) { - CommentRecipients commentRecipients = commentRecipientFinder.getAllRecipient(comment); - - commentRecipients.getRecipients() - .forEach((type, recipients) -> sendNotification(type, recipients, comment, author)); - } - - private void sendNotification(NotificationType notificationType, List recipients, Comment comment, - DarakbangMember author) { - Moim moim = comment.getMoim(); - NotificationEvent notificationEvent = NotificationEvent.nonChatEvent( - notificationType, - moim.getTitle(), - CommentNotificationMessage.create(author.getNickname(), notificationType), - getMoimUrl(moim.getDarakbangId(), moim.getId()), - recipients - ); - - eventPublisher.publishEvent(notificationEvent); - } - - static class CommentNotificationMessage { - - public static String create(String author, NotificationType type) { - if (type == NotificationType.NEW_COMMENT) { - return author + "님이 댓글을 남겼어요!"; - } - if (type == NotificationType.NEW_REPLY) { - return author + "님이 답글을 남겼어요!"; - } - throw new NotificationException( - HttpStatus.BAD_REQUEST, NotificationErrorMessage.NOT_ALLOWED_NOTIFICATION_TYPE - ); - } - } -} diff --git a/backend/src/test/java/mouda/backend/moim/business/CommentAsyncTest.java b/backend/src/test/java/mouda/backend/moim/business/CommentAsyncTest.java new file mode 100644 index 00000000..6b4dc348 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/business/CommentAsyncTest.java @@ -0,0 +1,113 @@ +package mouda.backend.moim.business; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.common.fixture.CommentFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.implement.notificiation.CommentRecipientFinder; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.CommentRepository; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.moim.presentation.request.comment.CommentCreateRequest; + +@SpringBootTest +public class CommentAsyncTest extends DarakbangSetUp { + + @Autowired + private CommentService commentService; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ChamyoRepository chamyoRepository; + + @MockBean + private CommentRecipientFinder commentRecipientFinder; + + private Moim moim; + private Comment parentComment; + + @BeforeEach + void init() { + moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build()); + parentComment = commentRepository.save(CommentFixture.getCommentWithAnnaAtSoccerMoim(darakbangAnna, moim)); + } + + @DisplayName("댓글 생성시 알림 전송 과정에서 예외가 발생해도 댓글은 생성된다.") + @Test + void asyncWhenCommentCreate() { + // given + String content = "비동기 확인 댓글 ~"; + + // when + when(commentRecipientFinder.getAllRecipient(any(Comment.class))) + .thenThrow(new RuntimeException("삐용12")); + + commentService.createComment(darakbang.getId(), moim.getId(), darakbangHogee, new CommentCreateRequest( + null, + content + )); + + // then + Optional commentOptional = commentRepository.findById(getCommentId(moim, content)); + assertThat(commentOptional).isNotEmpty(); + + Comment comment = commentOptional.get(); + assertThat(comment.getContent()).isEqualTo(content); + } + + @DisplayName("답글 작성시 알림 전송 과정에서 예외가 발생해도 답글은 생성된다.") + @Test + void asyncWhenReplyCreate() { + // given + String content = "비동기 확인 답글 ~"; + + // when + when(commentRecipientFinder.getAllRecipient(any(Comment.class))) + .thenThrow(new RuntimeException("삐용12")); + + commentService.createComment(darakbang.getId(), moim.getId(), darakbangHogee, new CommentCreateRequest( + parentComment.getId(), + content + )); + + // then + Optional commentOptional = commentRepository.findById(getCommentId(moim, content)); + assertThat(commentOptional).isNotEmpty(); + + Comment comment = commentOptional.get(); + assertThat(comment.getContent()).isEqualTo(content); + } + + private long getCommentId(Moim moim, String content) { + return commentRepository.findAllByMoimOrderByCreatedAt(moim).stream() + .filter(comment -> comment.getContent().equals(content)) + .findFirst() + .orElseThrow(RuntimeException::new) + .getId(); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/CommentRecipientsFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/CommentRecipientsFinderTest.java index aab0fe3a..0a54bca2 100644 --- a/backend/src/test/java/mouda/backend/moim/implement/finder/CommentRecipientsFinderTest.java +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/CommentRecipientsFinderTest.java @@ -17,8 +17,9 @@ import mouda.backend.common.fixture.MoimFixture; import mouda.backend.darakbangmember.domain.DarakbangMember; import mouda.backend.moim.domain.Comment; -import mouda.backend.moim.domain.CommentRecipients; import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.notificiation.CommentRecipientFinder; +import mouda.backend.moim.implement.notificiation.CommentRecipients; import mouda.backend.moim.implement.writer.MoimWriter; import mouda.backend.moim.infrastructure.CommentRepository; import mouda.backend.notification.domain.NotificationType; From 2d2001c0b0e980dcc1a1603835cb6cecec7238e8 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:46:12 +0900 Subject: [PATCH 13/17] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8(=EB=AA=A8=EC=9E=84=20=EC=83=9D=EC=84=B1,=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD)=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MoimNotificationEventHandler.java | 124 ++++++++++++++++++ .../MoimRecipientFinder.java | 2 +- .../event/MoimCreateNotificationEvent.java | 18 +++ .../event/MoimEditedNotificationEvent.java | 17 +++ .../MoimStatusChangeNotificationEvent.java | 16 +++ .../sender/MoimNotificationSender.java | 101 -------------- .../finder/MoimRecipientFinderTest.java | 1 + 7 files changed, 177 insertions(+), 102 deletions(-) create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimNotificationEventHandler.java rename backend/src/main/java/mouda/backend/moim/implement/{finder => notificiation}/MoimRecipientFinder.java (96%) create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimCreateNotificationEvent.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimEditedNotificationEvent.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimStatusChangeNotificationEvent.java delete mode 100644 backend/src/main/java/mouda/backend/moim/implement/sender/MoimNotificationSender.java diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimNotificationEventHandler.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimNotificationEventHandler.java new file mode 100644 index 00000000..73e179f5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimNotificationEventHandler.java @@ -0,0 +1,124 @@ +package mouda.backend.moim.implement.notificiation; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +import mouda.backend.common.config.UrlConfig; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.exception.MoimErrorMessage; +import mouda.backend.moim.exception.MoimException; +import mouda.backend.moim.implement.notificiation.event.MoimCreateNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.MoimEditedNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.MoimStatusChangeNotificationEvent; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.exception.NotificationErrorMessage; +import mouda.backend.notification.exception.NotificationException; +import mouda.backend.notification.implement.NotificationProcessor; + +@Component +public class MoimNotificationEventHandler extends AbstractMoimRelatedNotificationEventHandler { + + private final MoimRecipientFinder moimRecipientFinder; + private final DarakbangRepository darakbangRepository; + + public MoimNotificationEventHandler( + UrlConfig urlConfig, NotificationProcessor notificationProcessor, + MoimRecipientFinder moimRecipientFinder, DarakbangRepository darakbangRepository + ) { + super(urlConfig, notificationProcessor); + this.moimRecipientFinder = moimRecipientFinder; + this.darakbangRepository = darakbangRepository; + } + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = MoimCreateNotificationEvent.class) + public void handleMoimCreateNotificationEvent(MoimCreateNotificationEvent event) { + Moim moim = event.getMoim(); + NotificationType notificationType = event.getNotificationType(); + + List recipients = moimRecipientFinder.getMoimCreatedNotificationRecipients(moim.getDarakbangId(), + event.getHost().getId()); + String message = MoimNotificationMessage.create(moim.getTitle(), notificationType); + + processNotification(notificationType, moim, message, recipients); + } + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = MoimStatusChangeNotificationEvent.class) + public void handleMoimStatusChangeNotificationEvent(MoimStatusChangeNotificationEvent event) { + Moim moim = event.getMoim(); + NotificationType notificationType = event.getNotificationType(); + + String message = MoimNotificationMessage.create(moim.getTitle(), notificationType); + processMoimModifiedNotification(moim, notificationType, message); + } + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = MoimEditedNotificationEvent.class) + public void handleMoimEditedNotificationEvent(MoimEditedNotificationEvent event) { + Moim moim = event.getMoim(); + NotificationType notificationType = event.getNotificationType(); + + String message = MoimNotificationMessage.create(event.getOldMoimTitle(), notificationType); + processMoimModifiedNotification(moim, notificationType, message); + } + + private void processMoimModifiedNotification(Moim moim, NotificationType notificationType, String message) { + List recipients = moimRecipientFinder.getMoimModifiedNotificationRecipients(moim.getId()); + processNotification(notificationType, moim, message, recipients); + } + + private void processNotification( + NotificationType notificationType, Moim moim, String message, List recipients + ) { + Darakbang darakbang = darakbangRepository.findById(moim.getDarakbangId()) + .orElseThrow(() -> new MoimException(HttpStatus.NOT_FOUND, MoimErrorMessage.DARAKBANG_NOT_FOUND)); + + NotificationPayload payload = NotificationPayload.createNonChatPayload( + notificationType, + darakbang.getName(), + message, + getMoimUrl(darakbang.getId(), moim.getId()), + recipients + ); + + notificationProcessor.process(payload); + } + + static class MoimNotificationMessage { + + public static String create(String moimTitle, NotificationType type) { + if (type == NotificationType.MOIM_CREATED) { + return moimTitle + " 모임이 만들어졌어요!"; + } + if (type == NotificationType.MOIMING_COMPLETED) { + return moimTitle + " 모집이 마감되었어요!"; + } + if (type == NotificationType.MOINING_REOPENED) { + return moimTitle + " 모집이 재개되었어요!"; + } + if (type == NotificationType.MOIM_CANCELLED) { + return moimTitle + " 모임이 취소되었어요!"; + } + if (type == NotificationType.MOIM_MODIFIED) { + return moimTitle + " 모임 정보가 변경되었어요!"; + } + throw new NotificationException( + HttpStatus.BAD_REQUEST, NotificationErrorMessage.NOT_ALLOWED_NOTIFICATION_TYPE + ); + } + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/MoimRecipientFinder.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRecipientFinder.java similarity index 96% rename from backend/src/main/java/mouda/backend/moim/implement/finder/MoimRecipientFinder.java rename to backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRecipientFinder.java index 2691dd4d..0abf3832 100644 --- a/backend/src/main/java/mouda/backend/moim/implement/finder/MoimRecipientFinder.java +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRecipientFinder.java @@ -1,4 +1,4 @@ -package mouda.backend.moim.implement.finder; +package mouda.backend.moim.implement.notificiation; import java.util.List; diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimCreateNotificationEvent.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimCreateNotificationEvent.java new file mode 100644 index 00000000..e85fd4a1 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimCreateNotificationEvent.java @@ -0,0 +1,18 @@ +package mouda.backend.moim.implement.notificiation.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.notification.domain.NotificationType; + +@Getter +@RequiredArgsConstructor +@Builder +public class MoimCreateNotificationEvent { + + private final Moim moim; + private final DarakbangMember host; + private final NotificationType notificationType; +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimEditedNotificationEvent.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimEditedNotificationEvent.java new file mode 100644 index 00000000..c7fd27a2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimEditedNotificationEvent.java @@ -0,0 +1,17 @@ +package mouda.backend.moim.implement.notificiation.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.moim.domain.Moim; +import mouda.backend.notification.domain.NotificationType; + +@Getter +@RequiredArgsConstructor +@Builder +public class MoimEditedNotificationEvent { + + private final Moim moim; + private final String oldMoimTitle; + private final NotificationType notificationType; +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimStatusChangeNotificationEvent.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimStatusChangeNotificationEvent.java new file mode 100644 index 00000000..a6bedaba --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimStatusChangeNotificationEvent.java @@ -0,0 +1,16 @@ +package mouda.backend.moim.implement.notificiation.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.moim.domain.Moim; +import mouda.backend.notification.domain.NotificationType; + +@Getter +@RequiredArgsConstructor +@Builder +public class MoimStatusChangeNotificationEvent { + + private final Moim moim; + private final NotificationType notificationType; +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/sender/MoimNotificationSender.java b/backend/src/main/java/mouda/backend/moim/implement/sender/MoimNotificationSender.java deleted file mode 100644 index 80715144..00000000 --- a/backend/src/main/java/mouda/backend/moim/implement/sender/MoimNotificationSender.java +++ /dev/null @@ -1,101 +0,0 @@ -package mouda.backend.moim.implement.sender; - -import java.util.List; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; - -import mouda.backend.common.config.UrlConfig; -import mouda.backend.darakbang.domain.Darakbang; -import mouda.backend.darakbang.exception.DarakbangErrorMessage; -import mouda.backend.darakbang.exception.DarakbangException; -import mouda.backend.darakbang.infrastructure.DarakbangRepository; -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.exception.MoimErrorMessage; -import mouda.backend.moim.exception.MoimException; -import mouda.backend.moim.implement.finder.MoimRecipientFinder; -import mouda.backend.notification.domain.NotificationEvent; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.Recipient; -import mouda.backend.notification.exception.NotificationErrorMessage; -import mouda.backend.notification.exception.NotificationException; - -@Component -public class MoimNotificationSender extends AbstractMoimNotificationSender { - - private final ApplicationEventPublisher eventPublisher; - private final MoimRecipientFinder moimRecipientFinder; - private final DarakbangRepository darakbangRepository; - - public MoimNotificationSender(UrlConfig urlConfig, ApplicationEventPublisher eventPublisher, - MoimRecipientFinder moimRecipientFinder, DarakbangRepository darakbangRepository) { - super(urlConfig); - this.eventPublisher = eventPublisher; - this.moimRecipientFinder = moimRecipientFinder; - this.darakbangRepository = darakbangRepository; - } - - public void sendMoimCreatedNotification(Moim moim, DarakbangMember author, NotificationType notificationType) { - List recipients = moimRecipientFinder.getMoimCreatedNotificationRecipients(moim.getDarakbangId(), - author.getId()); - String message = MoimNotificationMessage.create(moim.getTitle(), notificationType); - - createEventAndPublish(notificationType, moim, message, recipients); - } - - public void sendMoimStatusChangedNotification(Moim moim, NotificationType notificationType) { - String message = MoimNotificationMessage.create(moim.getTitle(), notificationType); - sendMoimModifiedNotification(moim, notificationType, message); - } - - public void sendMoimInfoModifiedNotification(Moim moim, String oldTitle, NotificationType notificationType) { - String message = MoimNotificationMessage.create(oldTitle, notificationType); - sendMoimModifiedNotification(moim, notificationType, message); - } - - private void sendMoimModifiedNotification(Moim moim, NotificationType notificationType, String message) { - List recipients = moimRecipientFinder.getMoimModifiedNotificationRecipients(moim.getId()); - createEventAndPublish(notificationType, moim, message, recipients); - } - - private void createEventAndPublish(NotificationType notificationType, Moim moim, String message, - List recipients) { - Darakbang darakbang = darakbangRepository.findById(moim.getDarakbangId()) - .orElseThrow(() -> new MoimException(HttpStatus.NOT_FOUND, MoimErrorMessage.DARAKBANG_NOT_FOUND)); - - NotificationEvent notificationEvent = NotificationEvent.nonChatEvent( - notificationType, - darakbang.getName(), - message, - getMoimUrl(darakbang.getId(), moim.getId()), - recipients - ); - eventPublisher.publishEvent(notificationEvent); - } - - static class MoimNotificationMessage { - - public static String create(String moimTitle, NotificationType type) { - if (type == NotificationType.MOIM_CREATED) { - return moimTitle + " 모임이 만들어졌어요!"; - } - if (type == NotificationType.MOIMING_COMPLETED) { - return moimTitle + " 모집이 마감되었어요!"; - } - if (type == NotificationType.MOINING_REOPENED) { - return moimTitle + " 모집이 재개되었어요!"; - } - if (type == NotificationType.MOIM_CANCELLED) { - return moimTitle + " 모임이 취소되었어요!"; - } - if (type == NotificationType.MOIM_MODIFIED) { - return moimTitle + " 모임 정보가 변경되었어요!"; - } - throw new NotificationException( - HttpStatus.BAD_REQUEST, NotificationErrorMessage.NOT_ALLOWED_NOTIFICATION_TYPE - ); - } - } -} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/MoimRecipientFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/MoimRecipientFinderTest.java index ef43af32..6bd08c70 100644 --- a/backend/src/test/java/mouda/backend/moim/implement/finder/MoimRecipientFinderTest.java +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/MoimRecipientFinderTest.java @@ -14,6 +14,7 @@ import mouda.backend.moim.domain.Chamyo; import mouda.backend.moim.domain.Moim; import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.implement.notificiation.MoimRecipientFinder; import mouda.backend.moim.infrastructure.ChamyoRepository; import mouda.backend.moim.infrastructure.MoimRepository; import mouda.backend.notification.domain.Recipient; From b69bcf68dd15f39b34da68f1ca580da7a68e3205 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:46:36 +0900 Subject: [PATCH 14/17] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20=EA=B0=9D=EC=B2=B4=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 --- .../backend/chat/business/ChatService.java | 2 +- .../chat/domain/ChatNotificationEvent.java | 15 ++++ .../ChatNotificationEventHandler.java} | 76 ++++++++++++------- .../notification/ChatNotificationSender.java | 26 +++++++ .../ChatRecipientFinder.java | 23 +----- .../implement/ChatRecipientFinderTest.java | 8 +- .../ChatRoomSubscriptionFilterTest.java | 22 +++--- 7 files changed, 108 insertions(+), 64 deletions(-) create mode 100644 backend/src/main/java/mouda/backend/chat/domain/ChatNotificationEvent.java rename backend/src/main/java/mouda/backend/chat/implement/{sender/ChatNotificationSender.java => notification/ChatNotificationEventHandler.java} (52%) create mode 100644 backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationSender.java rename backend/src/main/java/mouda/backend/chat/implement/{sender => notification}/ChatRecipientFinder.java (67%) diff --git a/backend/src/main/java/mouda/backend/chat/business/ChatService.java b/backend/src/main/java/mouda/backend/chat/business/ChatService.java index 2acc8ac8..29ce9ada 100644 --- a/backend/src/main/java/mouda/backend/chat/business/ChatService.java +++ b/backend/src/main/java/mouda/backend/chat/business/ChatService.java @@ -14,7 +14,7 @@ import mouda.backend.chat.domain.Chats; import mouda.backend.chat.implement.ChatRoomFinder; import mouda.backend.chat.implement.ChatWriter; -import mouda.backend.chat.implement.sender.ChatNotificationSender; +import mouda.backend.chat.implement.notification.ChatNotificationSender; import mouda.backend.chat.presentation.request.ChatCreateRequest; import mouda.backend.chat.presentation.request.DateTimeConfirmRequest; import mouda.backend.chat.presentation.request.LastReadChatRequest; diff --git a/backend/src/main/java/mouda/backend/chat/domain/ChatNotificationEvent.java b/backend/src/main/java/mouda/backend/chat/domain/ChatNotificationEvent.java new file mode 100644 index 00000000..9b4854e7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/ChatNotificationEvent.java @@ -0,0 +1,15 @@ +package mouda.backend.chat.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Builder +public class ChatNotificationEvent { + + private final Long darakbangId; + private final ChatRoom chatRoom; + private final Chat appendedChat; +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/sender/ChatNotificationSender.java b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationEventHandler.java similarity index 52% rename from backend/src/main/java/mouda/backend/chat/implement/sender/ChatNotificationSender.java rename to backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationEventHandler.java index 290769b9..d4a07453 100644 --- a/backend/src/main/java/mouda/backend/chat/implement/sender/ChatNotificationSender.java +++ b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationEventHandler.java @@ -1,16 +1,20 @@ -package mouda.backend.chat.implement.sender; +package mouda.backend.chat.implement.notification; import java.util.List; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationEventPublisher; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; import lombok.Getter; import lombok.RequiredArgsConstructor; import mouda.backend.bet.domain.Bet; import mouda.backend.bet.implement.BetFinder; import mouda.backend.chat.domain.Chat; +import mouda.backend.chat.domain.ChatNotificationEvent; import mouda.backend.chat.domain.ChatRoom; import mouda.backend.chat.domain.ChatRoomType; import mouda.backend.chat.entity.ChatType; @@ -20,82 +24,93 @@ import mouda.backend.darakbang.implement.DarakbangFinder; import mouda.backend.moim.domain.Moim; import mouda.backend.moim.implement.finder.MoimFinder; -import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationPayload; import mouda.backend.notification.domain.NotificationType; import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.implement.NotificationProcessor; @Component -@EnableConfigurationProperties(UrlConfig.class) @RequiredArgsConstructor -public class ChatNotificationSender { +public class ChatNotificationEventHandler { private final UrlConfig urlConfig; - private final ChatRecipientFinder chatRecipientFinder; - private final ApplicationEventPublisher eventPublisher; private final MoimFinder moimFinder; private final BetFinder betFinder; private final DarakbangFinder darakbangFinder; + private final ChatRecipientFinder chatRecipientFinder; + private final NotificationProcessor notificationProcessor; + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = ChatNotificationEvent.class, phase = TransactionPhase.AFTER_COMMIT) public void sendChatNotification( - long darakbangId, ChatRoom chatRoom, Chat appendedChat + ChatNotificationEvent chatNotificationEvent ) { + long darakbangId = chatNotificationEvent.getDarakbangId(); + ChatRoom chatRoom = chatNotificationEvent.getChatRoom(); + Chat appendedChat = chatNotificationEvent.getAppendedChat(); + ChatRoomType chatRoomType = chatRoom.getType(); long chatRoomId = chatRoom.getId(); if (chatRoomType == ChatRoomType.BET) { Bet bet = betFinder.find(darakbangId, chatRoom.getTargetId()); - sendBetNotification(bet, appendedChat, chatRoomId); + handleBetNotification(bet, appendedChat, chatRoomId); return; } Moim moim = moimFinder.read(chatRoom.getTargetId(), darakbangId); - sendMoimNotification(moim, appendedChat, chatRoomId); + handleMoimNotification(moim, appendedChat, chatRoomId); } - private void sendMoimNotification( + private void handleMoimNotification( Moim moim, Chat chat, long chatRoomId ) { List recipients = chatRecipientFinder.getMoimChatNotificationRecipients(moim.getId(), chat.getAuthor()); long darakbangId = moim.getDarakbangId(); - publishEvent(darakbangId, chatRoomId, moim.getTitle(), chat, recipients); + processNotification(darakbangId, chatRoomId, moim.getTitle(), chat, recipients); } - private void sendBetNotification(Bet bet, Chat chat, long chatRoomId) { + private void handleBetNotification(Bet bet, Chat chat, long chatRoomId) { List recipients = chatRecipientFinder.getBetChatNotificationRecipients(bet.getId(), chat.getAuthor()); long darakbangId = bet.getDarakbangId(); - publishEvent(darakbangId, chatRoomId, bet.getTitle(), chat, recipients); + processNotification(darakbangId, chatRoomId, bet.getTitle(), chat, recipients); } - private void publishEvent(long darakbangId, long chatRoomId, String title, Chat chat, List recipients) { + private void processNotification( + long darakbangId, long chatRoomId, String title, Chat chat, List recipients + ) { Darakbang darakbang = darakbangFinder.findById(darakbangId); - ChatNotification chatNotification = ChatNotification.create(darakbang.getName(), title, chat); + ChatNotificationMessage chatNotificationMessage = ChatNotificationMessage.create(darakbang.getName(), title, + chat); - NotificationEvent notificationEvent = NotificationEvent.chatEvent( - chatNotification.getType(), + NotificationPayload payload = NotificationPayload.createChatPayload( + chatNotificationMessage.getType(), darakbang.getName(), - chatNotification.getMessage(), + chatNotificationMessage.getMessage(), urlConfig.getChatRoomUrl(darakbangId, chatRoomId), recipients, darakbangId, chatRoomId ); - eventPublisher.publishEvent(notificationEvent); + notificationProcessor.process(payload); } @Getter @RequiredArgsConstructor - static class ChatNotification { + static class ChatNotificationMessage { private final String title; private final NotificationType type; private final String message; - public static ChatNotification create(String darakbangName, String title, Chat chat) { + public static ChatNotificationMessage create(String darakbangName, String title, + Chat chat) { ChatType chatType = chat.getChatType(); String content = chat.getContent(); @@ -114,16 +129,19 @@ public static ChatNotification create(String darakbangName, String title, Chat c return basicChat(title, message); } - private static ChatNotification placeConfirmChat(String title, String message) { - return new ChatNotification(title, NotificationType.MOIM_PLACE_CONFIRMED, message); + private static ChatNotificationMessage placeConfirmChat(String title, String message) { + return new ChatNotificationMessage(title, NotificationType.MOIM_PLACE_CONFIRMED, + message); } - private static ChatNotification dateTimeConfirmChat(String title, String message) { - return new ChatNotification(title, NotificationType.MOIM_TIME_CONFIRMED, message); + private static ChatNotificationMessage dateTimeConfirmChat(String title, + String message) { + return new ChatNotificationMessage(title, NotificationType.MOIM_TIME_CONFIRMED, + message); } - private static ChatNotification basicChat(String title, String message) { - return new ChatNotification(title, NotificationType.NEW_CHAT, message); + private static ChatNotificationMessage basicChat(String title, String message) { + return new ChatNotificationMessage(title, NotificationType.NEW_CHAT, message); } } } diff --git a/backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationSender.java b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationSender.java new file mode 100644 index 00000000..1426e5d4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationSender.java @@ -0,0 +1,26 @@ +package mouda.backend.chat.implement.notification; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.Chat; +import mouda.backend.chat.domain.ChatNotificationEvent; +import mouda.backend.chat.domain.ChatRoom; + +@Component +@RequiredArgsConstructor +public class ChatNotificationSender { + + private final ApplicationEventPublisher eventPublisher; + + public void sendChatNotification(long darakbangId, ChatRoom chatRoom, Chat appendedChat) { + ChatNotificationEvent event = ChatNotificationEvent.builder() + .darakbangId(darakbangId) + .chatRoom(chatRoom) + .appendedChat(appendedChat) + .build(); + + eventPublisher.publishEvent(event); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/sender/ChatRecipientFinder.java b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatRecipientFinder.java similarity index 67% rename from backend/src/main/java/mouda/backend/chat/implement/sender/ChatRecipientFinder.java rename to backend/src/main/java/mouda/backend/chat/implement/notification/ChatRecipientFinder.java index 4d41c012..970cf3cb 100644 --- a/backend/src/main/java/mouda/backend/chat/implement/sender/ChatRecipientFinder.java +++ b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatRecipientFinder.java @@ -1,4 +1,4 @@ -package mouda.backend.chat.implement.sender; +package mouda.backend.chat.implement.notification; import java.util.List; import java.util.stream.Stream; @@ -48,25 +48,4 @@ public List getNotificationRecipients(Stream memberS .build()) .toList(); } - - // todo: 구버전 채팅 기능 삭제시 같이 지워주세요. - public List getMoimChatNotificationRecipients(long moimId, DarakbangMember sender) { - List chamyos = chamyoRepository.findAllByMoimId(moimId); - - Stream darakbangMemberStream = chamyos.stream() - .map(Chamyo::getDarakbangMember); - - return getNotificationRecipients(darakbangMemberStream, sender); - } - - // todo: 구버전 채팅 기능 삭제시 같이 지워주세요. - public List getNotificationRecipients(Stream memberStream, DarakbangMember sender) { - return memberStream - .filter(darakbangMember -> darakbangMember.isNotSameMemberWith(sender)) - .map(darakbangMember -> Recipient.builder() - .memberId(darakbangMember.getMemberId()) - .darakbangMemberId(darakbangMember.getId()) - .build()) - .toList(); - } } diff --git a/backend/src/test/java/mouda/backend/chat/implement/ChatRecipientFinderTest.java b/backend/src/test/java/mouda/backend/chat/implement/ChatRecipientFinderTest.java index 83bdc641..97918680 100644 --- a/backend/src/test/java/mouda/backend/chat/implement/ChatRecipientFinderTest.java +++ b/backend/src/test/java/mouda/backend/chat/implement/ChatRecipientFinderTest.java @@ -9,7 +9,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import mouda.backend.chat.implement.sender.ChatRecipientFinder; +import mouda.backend.chat.domain.Author; +import mouda.backend.chat.implement.notification.ChatRecipientFinder; import mouda.backend.common.fixture.DarakbangSetUp; import mouda.backend.common.fixture.MoimFixture; import mouda.backend.moim.domain.Moim; @@ -37,8 +38,9 @@ void getMoimChatNotificationRecipients() { chamyoWriter.saveAsMoimee(moim, darakbangHogee); // when - List result = chatRecipientFinder.getMoimChatNotificationRecipients(moim.getId(), - darakbangAnna); + Author author = Author.builder().memberId(darakbangAnna.getMemberId()).darakbangMemberId(darakbangAnna.getId()) + .profile("").nickname("안나").build(); + List result = chatRecipientFinder.getMoimChatNotificationRecipients(moim.getId(), author); // then assertThat(result).hasSize(1); diff --git a/backend/src/test/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilterTest.java b/backend/src/test/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilterTest.java index ac6bdea7..aafb3d68 100644 --- a/backend/src/test/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilterTest.java +++ b/backend/src/test/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilterTest.java @@ -10,7 +10,8 @@ import org.springframework.boot.test.context.SpringBootTest; import mouda.backend.common.fixture.DarakbangSetUp; -import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationSendEvent; import mouda.backend.notification.domain.NotificationType; import mouda.backend.notification.domain.Recipient; import mouda.backend.notification.implement.subscription.SubscriptionWriter; @@ -31,7 +32,7 @@ void filter_WhenTypeIsConfirmed() { subscriptionWriter.changeChatRoomSubscription(hogee, darakbang.getId(), 1L); // when - NotificationEvent notificationEvent = NotificationEvent.chatEvent( + NotificationPayload payload = NotificationPayload.createChatPayload( NotificationType.MOIM_PLACE_CONFIRMED, "모임 제목", "메시지", @@ -44,9 +45,10 @@ void filter_WhenTypeIsConfirmed() { darakbang.getId(), 1L ); + NotificationSendEvent notificationSendEvent = NotificationSendEvent.from(payload); // then - List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationEvent); + List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationSendEvent); assertThat(filteredRecipient).hasSize(1); assertThat(filteredRecipient).extracting(Recipient::getMemberId).containsExactly(darakbangHogee.getMemberId()); } @@ -58,22 +60,23 @@ void filter_WhenUnsubscribed() { subscriptionWriter.changeChatRoomSubscription(hogee, darakbang.getId(), 1L); // when - NotificationEvent notificationEvent = NotificationEvent.chatEvent( + NotificationPayload payload = NotificationPayload.createChatPayload( NotificationType.NEW_CHAT, "모임 제목", "메시지", "url", List.of(Recipient.builder() - .memberId(darakbangHogee.getMemberId()) + .memberId(hogee.getId()) .darakbangMemberId(darakbangHogee.getId()) .build() ), darakbang.getId(), 1L ); + NotificationSendEvent notificationSendEvent = NotificationSendEvent.from(payload); // then - List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationEvent); + List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationSendEvent); assertThat(filteredRecipient).isEmpty(); } @@ -81,22 +84,23 @@ void filter_WhenUnsubscribed() { @Test void filter_WhenSubscribed() { // when - NotificationEvent notificationEvent = NotificationEvent.chatEvent( + NotificationPayload payload = NotificationPayload.createChatPayload( NotificationType.NEW_CHAT, "모임 제목", "메시지", "url", List.of(Recipient.builder() - .memberId(darakbangHogee.getMemberId()) + .memberId(hogee.getId()) .darakbangMemberId(darakbangHogee.getId()) .build() ), darakbang.getId(), 1L ); + NotificationSendEvent notificationSendEvent = NotificationSendEvent.from(payload); // then - List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationEvent); + List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationSendEvent); assertThat(filteredRecipient).hasSize(1); assertThat(filteredRecipient).extracting(Recipient::getMemberId).containsExactly(darakbangHogee.getMemberId()); } From bff5b8c8e5423465e916e3c864e2b6c6cf254c60 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:47:07 +0900 Subject: [PATCH 15/17] =?UTF-8?q?refactor:=20FcmFailedResponse=EC=97=90=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=ED=95=9C=20=ED=86=A0=ED=81=B0=EC=9D=B4=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=EC=A7=80=20=ED=99=95=EC=9D=B8=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mouda/backend/notification/domain/FcmFailedResponse.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/main/java/mouda/backend/notification/domain/FcmFailedResponse.java b/backend/src/main/java/mouda/backend/notification/domain/FcmFailedResponse.java index a02d41c3..b96726aa 100644 --- a/backend/src/main/java/mouda/backend/notification/domain/FcmFailedResponse.java +++ b/backend/src/main/java/mouda/backend/notification/domain/FcmFailedResponse.java @@ -97,4 +97,8 @@ private boolean isTokenAbsent(MessagingErrorCode... errorCodes) { .map(failedTokens::get) .allMatch(tokens -> tokens == null || tokens.isEmpty()); } + + public boolean hasNoFailedTokens() { + return failedTokens.isEmpty(); + } } From 62eecb8d5af53bcc2286a6482cb1f3a0c2ad3ba0 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:47:32 +0900 Subject: [PATCH 16/17] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=A5=B4=EB=9F=AC=EC=97=90=20Transactional?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/implement/fcm/token/FcmTokenScheduler.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenScheduler.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenScheduler.java index 4a074e1c..39ae4a00 100644 --- a/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenScheduler.java +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenScheduler.java @@ -2,6 +2,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import mouda.backend.notification.infrastructure.entity.FcmTokenEntity; @@ -14,6 +15,7 @@ public class FcmTokenScheduler { private final FcmTokenRepository fcmTokenRepository; @Scheduled(cron = "0 0 0 1 * ?") + @Transactional public void checkAllTokensIfInactiveOrExpired() { fcmTokenRepository.findAll().forEach(this::deactiveOrDelete); } From c2c2d0e745078665f825086d5d428ce37d0a1cc7 Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 24 Oct 2024 05:56:48 +0900 Subject: [PATCH 17/17] =?UTF-8?q?refactor:=20=EB=B9=84=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mouda/backend/chat/ChatAsyncTest.java | 91 +++++++++++ .../moim/business/ChamyoAsyncTest.java | 84 ++++++++++ .../backend/moim/business/MoimAsyncTest.java | 146 ++++++++++++++++++ .../implement/NotificationAsyncTest.java | 75 +++++++++ 4 files changed, 396 insertions(+) create mode 100644 backend/src/test/java/mouda/backend/chat/ChatAsyncTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/business/ChamyoAsyncTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/business/MoimAsyncTest.java create mode 100644 backend/src/test/java/mouda/backend/notification/implement/NotificationAsyncTest.java diff --git a/backend/src/test/java/mouda/backend/chat/ChatAsyncTest.java b/backend/src/test/java/mouda/backend/chat/ChatAsyncTest.java new file mode 100644 index 00000000..9774223a --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/ChatAsyncTest.java @@ -0,0 +1,91 @@ +package mouda.backend.chat; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.auth.presentation.controller.AuthController; +import mouda.backend.chat.business.ChatService; +import mouda.backend.chat.domain.Author; +import mouda.backend.chat.domain.Chat; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.entity.ChatEntity; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.implement.notification.ChatRecipientFinder; +import mouda.backend.chat.infrastructure.ChatRepository; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.chat.presentation.request.ChatCreateRequest; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +public class ChatAsyncTest extends DarakbangSetUp { + + @Autowired + private ChatService chatService; + + @Autowired + private ChatRoomRepository chatRoomRepository; + + @MockBean + private ChatRecipientFinder chatRecipientFinder; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ChamyoRepository chamyoRepository; + + @Autowired + private ChatRepository chatRepository; + + private Moim moim; + + @BeforeEach + void init() { + moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moimRole(MoimRole.MOIMER) + .darakbangMember(darakbangAnna) + .moim(moim) + .build()); + } + + @DisplayName("채팅 알림 전송 과정에서 예외가 발생해도 채팅은 정상적으로 저장된다.") + @Test + void createChatAsync() { + // given + String content = "비동기 확인 채팅~"; + ChatCreateRequest chatCreateRequest = new ChatCreateRequest(content); + ChatRoomEntity chatRoom = chatRoomRepository.save( + new ChatRoomEntity(moim.getId(), darakbang.getId(), ChatRoomType.MOIM)); + + // when + when(chatRecipientFinder.getMoimChatNotificationRecipients(anyLong(), any(Author.class))) + .thenThrow(new RuntimeException("삐용12")); + + chatService.createChat(darakbang.getId(), chatRoom.getId(), chatCreateRequest, darakbangAnna); + + // then + List chats = chatRepository.findAll(); + assertThat(chats).hasSize(1); + + ChatEntity chat = chats.get(0); + assertThat(chat.getContent()).isEqualTo(content); + assertThat(chat.getChatRoomId()).isEqualTo(chatRoom.getId()); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/business/ChamyoAsyncTest.java b/backend/src/test/java/mouda/backend/moim/business/ChamyoAsyncTest.java new file mode 100644 index 00000000..b6b4767e --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/business/ChamyoAsyncTest.java @@ -0,0 +1,84 @@ +package mouda.backend.moim.business; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.implement.notificiation.ChamyoRecipientFinder; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +public class ChamyoAsyncTest extends DarakbangSetUp { + + @Autowired + private ChamyoService chamyoService; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ChamyoRepository chamyoRepository; + + @MockBean + private ChamyoRecipientFinder chamyoRecipientFinder; + + private Moim moim; + + @BeforeEach + void init() { + moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build()); + } + + @DisplayName("회원 참여시 알림 전송 과정에서 예외가 발생해도 참여는 정상적으로 처리된다.") + @Test + void asyncWhenChamyoCreate() { + // when + when(chamyoRecipientFinder.getChamyoNotificationRecipients(any(Moim.class), any(DarakbangMember.class))) + .thenThrow(new RuntimeException("삐용12")); + + chamyoService.chamyoMoim(darakbang.getId(), moim.getId(), darakbangHogee); + + // then + List chamyos = chamyoRepository.findAllByMoimId(moim.getId()); + assertThat(chamyos).hasSize(2); + assertThat(chamyos).extracting(chamyo -> chamyo.getDarakbangMember().getNickname()) + .contains(darakbangHogee.getNickname()); + } + + @DisplayName("회원 참여 취소시 알림 전송 과정에서 예외가 발생해도 취소는 정상적으로 처리된다.") + @Test + void asyncWhenChamyoCancel() { + // when + when(chamyoRecipientFinder.getChamyoNotificationRecipients(any(Moim.class), any(DarakbangMember.class))) + .thenThrow(new RuntimeException("삐용12")); + + chamyoService.chamyoMoim(darakbang.getId(), moim.getId(), darakbangHogee); + chamyoService.cancelChamyo(darakbang.getId(), moim.getId(), darakbangHogee); + + // then + List chamyos = chamyoRepository.findAllByMoimId(moim.getId()); + assertThat(chamyos).hasSize(1); + assertThat(chamyos).extracting(chamyo -> chamyo.getDarakbangMember().getNickname()) + .doesNotContain(darakbangHogee.getNickname()); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/business/MoimAsyncTest.java b/backend/src/test/java/mouda/backend/moim/business/MoimAsyncTest.java new file mode 100644 index 00000000..fc6263cd --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/business/MoimAsyncTest.java @@ -0,0 +1,146 @@ +package mouda.backend.moim.business; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.notificiation.MoimRecipientFinder; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.moim.presentation.request.moim.MoimCreateRequest; +import mouda.backend.moim.presentation.request.moim.MoimEditRequest; +import mouda.backend.moim.presentation.response.moim.MoimFindAllResponse; + +@SpringBootTest +public class MoimAsyncTest extends DarakbangSetUp { + + @Autowired + private MoimService moimService; + + @Autowired + private MoimRepository moimRepository; + + @MockBean + private MoimRecipientFinder moimRecipientFinder; + + @DisplayName("모임 생성시 알림 전송 과정에서 예외가 발생해도 모임은 생성된다.") + @Test + void asyncWhenMoimCreate() { + // given + String title = "비동기 확인 ~"; + String description = "비동기동비"; + + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + title, + null, + null, + null, + 10, + description + ); + + // when + when(moimRecipientFinder.getMoimCreatedNotificationRecipients(anyLong(), anyLong())) + .thenThrow(new RuntimeException("삐용12")); + + moimService.createMoim(darakbang.getId(), darakbangAnna, moimCreateRequest); + long moimId = getMoimId(title, description); + + // then + Optional moimOptional = moimRepository.findById(moimId); + assertThat(moimOptional).isNotEmpty(); + + Moim moim = moimOptional.get(); + assertThat(moim.getTitle()).isEqualTo(title); + assertThat(moim.getDescription()).isEqualTo(description); + } + + @DisplayName("모임 정보 수정시 알림 전송 과정에서 예외가 발생해도 수정은 반영된다.") + @Test + void asyncWhenMoimEdit() { + // given + String title = "비동기 확인 ~"; + String description = "비동기동비"; + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + title, + null, + null, + null, + 10, + description + ); + moimService.createMoim(darakbang.getId(), darakbangAnna, moimCreateRequest); + long moimId = getMoimId(title, description); + + // when + when(moimRecipientFinder.getMoimCreatedNotificationRecipients(anyLong(), anyLong())) + .thenThrow(new RuntimeException("삐용12")); + String editedTitle = "수정된 비동기 ~"; + String editedDescription = "수정된 비동기동비"; + + moimService.editMoim(darakbang.getId(), new MoimEditRequest( + moimId, + editedTitle, + null, + null, + null, + 10, + editedDescription + ), darakbangAnna); + + // then + Optional moimOptional = moimRepository.findById(moimId); + assertThat(moimOptional).isNotEmpty(); + Moim moim = moimOptional.get(); + assertThat(moim.getTitle()).isEqualTo(editedTitle); + assertThat(moim.getDescription()).isEqualTo(editedDescription); + } + + @DisplayName("모임 상태 변경시 알림 전송 과정에서 예외가 발생해도 상태 변경은 반영된다.") + @Test + void asyncWhenMoimStatusChange() { + // given + String title = "비동기 확인 ~"; + String description = "비동기동비"; + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + title, + null, + null, + null, + 10, + description + ); + moimService.createMoim(darakbang.getId(), darakbangAnna, moimCreateRequest); + long moimId = getMoimId(title, description); + + // when + when(moimRecipientFinder.getMoimCreatedNotificationRecipients(anyLong(), anyLong())) + .thenThrow(new RuntimeException("삐용12")); + + moimService.completeMoim(darakbang.getId(), moimId, darakbangAnna); + + // then + Optional moimOptional = moimRepository.findById(moimId); + assertThat(moimOptional).isNotEmpty(); + + Moim moim = moimOptional.get(); + assertThat(moim.isCompleted()).isTrue(); + } + + private Long getMoimId(String title, String description) { + return moimService.findAllMoim(darakbang.getId(), darakbangAnna).moims().stream() + .filter(moim -> moim.title().equals(title) && moim.description().equals(description)) + .map(MoimFindAllResponse::moimId) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/NotificationAsyncTest.java b/backend/src/test/java/mouda/backend/notification/implement/NotificationAsyncTest.java new file mode 100644 index 00000000..1f0f8386 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/NotificationAsyncTest.java @@ -0,0 +1,75 @@ +package mouda.backend.notification.implement; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.infrastructure.entity.MemberNotificationEntity; +import mouda.backend.notification.infrastructure.repository.MemberNotificationRepository; + +@SpringBootTest +public class NotificationAsyncTest extends DarakbangSetUp { + + @Autowired + private NotificationProcessor notificationProcessor; + + @Autowired + private MemberNotificationRepository notificationRepository; + + @MockBean + private NotificationSender notificationSender; + + @DisplayName("알림 전송 과정에서 예외가 발생해도 회원의 알림은 저장된다.") + @Test + void asyncWhenNotificationSend() { + // given + String title = "비동기 확인 ~"; + String body = "비동기동비"; + NotificationPayload payload = NotificationPayload.createNonChatPayload( + NotificationType.MOIM_CREATED, + title, + body, + "url", + List.of(Recipient.builder() + .memberId(darakbangAnna.getId()) + .darakbangMemberId(darakbangAnna.getId()) + .build()) + ); + + // when + doThrow(new RuntimeException("삐용12")) + .when(notificationSender).sendNotification(any(CommonNotification.class), anyList()); + + notificationProcessor.process(payload); + + // then + Optional notificationOptional = notificationRepository.findById( + getNotificationId(title, body)); + assertThat(notificationOptional).isNotEmpty(); + + MemberNotificationEntity notification = notificationOptional.get(); + assertThat(notification.getTitle()).isEqualTo(title); + assertThat(notification.getBody()).isEqualTo(body); + } + + private long getNotificationId(String title, String body) { + return notificationRepository.findAll().stream() + .filter(notification -> notification.getTitle().equals(title) && notification.getBody().equals(body)) + .findFirst() + .orElseThrow(IllegalArgumentException::new) + .getId(); + } +}