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 519aa7670..455f3f928 100644 --- a/backend/src/main/java/mouda/backend/chat/business/ChatService.java +++ b/backend/src/main/java/mouda/backend/chat/business/ChatService.java @@ -11,6 +11,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.presentation.request.ChatCreateRequest; import mouda.backend.chat.presentation.request.DateTimeConfirmRequest; import mouda.backend.chat.presentation.request.LastReadChatRequest; @@ -20,6 +21,7 @@ import mouda.backend.moim.domain.Moim; import mouda.backend.moim.implement.finder.MoimFinder; import mouda.backend.moim.implement.writer.MoimWriter; +import mouda.backend.notification.domain.NotificationType; @Service @Transactional @@ -30,6 +32,7 @@ public class ChatService { private final ChatWriter chatWriter; private final MoimWriter moimWriter; private final MoimFinder moimFinder; + private final ChatNotificationSender chatNotificationSender; public void createChat( long darakbangId, @@ -40,7 +43,9 @@ public void createChat( ChatRoom chatRoom = chatRoomFinder.read(darakbangId, chatRoomId, darakbangMember); chatWriter.append(chatRoom.getId(), request.content(), darakbangMember); - // 알림을 발생한다. + Moim moim = moimFinder.read(chatRoom.getTargetId(), darakbangId); + + chatNotificationSender.sendChatNotification(moim, darakbangMember, NotificationType.NEW_CHAT, chatRoomId); } @Transactional(readOnly = true) @@ -63,7 +68,9 @@ public void confirmPlace(long darakbangId, long chatRoomId, PlaceConfirmRequest moimWriter.confirmPlace(moim, darakbangMember, request.place()); chatWriter.appendPlaceTypeChat(chatRoom.getId(), request.place(), darakbangMember); - // notificationService.notifyToMembers(NotificationType.MOIM_PLACE_CONFIRMED, darakbangId, moim, darakbangMember); + + chatNotificationSender.sendChatNotification(moim, darakbangMember, NotificationType.MOIM_PLACE_CONFIRMED, + chatRoomId); } public void confirmDateTime(long darakbangId, long chatRoomId, DateTimeConfirmRequest request, @@ -75,7 +82,8 @@ public void confirmDateTime(long darakbangId, long chatRoomId, DateTimeConfirmRe chatWriter.appendDateTimeTypeChat(chatRoom.getId(), request.date(), request.time(), darakbangMember); - // notificationService.notifyToMembers(NotificationType.MOIM_TIME_CONFIRMED, darakbangId, moim, darakbangMember); + chatNotificationSender.sendChatNotification(moim, darakbangMember, NotificationType.MOIM_TIME_CONFIRMED, + chatRoomId); } public void updateLastReadChat( diff --git a/backend/src/main/java/mouda/backend/chat/implement/sender/ChatNotificationSender.java b/backend/src/main/java/mouda/backend/chat/implement/sender/ChatNotificationSender.java new file mode 100644 index 000000000..0ed5bfa53 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/sender/ChatNotificationSender.java @@ -0,0 +1,45 @@ +package mouda.backend.chat.implement.sender; + +import java.util.List; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.UrlConfig; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.finder.ChatRecipientFinder; +import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; + +@Component +@EnableConfigurationProperties(UrlConfig.class) +@RequiredArgsConstructor +public class ChatNotificationSender { + + private final UrlConfig urlConfig; + private final ChatRecipientFinder chatRecipientFinder; + private final ApplicationEventPublisher eventPublisher; + + public void sendChatNotification(Moim moim, DarakbangMember sender, NotificationType notificationType, long chatRoomId) { + List recipients = chatRecipientFinder.getChatNotificationRecipients(moim.getId(), sender); + NotificationEvent notificationEvent = createNotificationEvent(moim, sender, notificationType, recipients, chatRoomId); + + eventPublisher.publishEvent(notificationEvent); + } + + private NotificationEvent createNotificationEvent(Moim moim, DarakbangMember sender, NotificationType notificationType, List recipients, long chatRoomId) { + String message; + if (notificationType.isConfirmedType()) { + message = notificationType.createMessage(moim.getTitle()); + } else { + message = notificationType.createMessage(sender.getNickname()); + } + + return new NotificationEvent( + notificationType, moim.getTitle(), message, urlConfig.getChatRoomUrl(moim.getDarakbangId(), chatRoomId), recipients, moim.getDarakbangId(), chatRoomId); + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/UrlConfig.java b/backend/src/main/java/mouda/backend/common/config/UrlConfig.java new file mode 100644 index 000000000..a5426a333 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/UrlConfig.java @@ -0,0 +1,25 @@ +package mouda.backend.common.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@ConfigurationProperties(prefix = "url") +@Getter +@RequiredArgsConstructor +public class UrlConfig { + + private final String base; + private final String moim; + private final String chat; + private final String chatroom; + + public String getChatRoomUrl(long darakbangId, long chatRoomId) { + return base + String.format(chatroom, darakbangId, chatRoomId); + } + + public String getMoimUrl(long darakbangId, long moimId) { + return base + String.format(moim, darakbangId, moimId); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMember.java b/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMember.java index 0340e26bd..532b01b74 100644 --- a/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMember.java +++ b/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMember.java @@ -92,6 +92,14 @@ public DarakbangMember updateMyInfo(String nickname, String description, String return this; } + public boolean isSameMemberWith(DarakbangMember other) { + return id.equals(other.getId()); + } + + public boolean isNotSameMemberWith(DarakbangMember other) { + return !isSameMemberWith(other); + } + @Override public boolean equals(Object o) { if (this == o) 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 b3072c901..d625ea343 100644 --- a/backend/src/main/java/mouda/backend/moim/business/ChamyoService.java +++ b/backend/src/main/java/mouda/backend/moim/business/ChamyoService.java @@ -12,11 +12,11 @@ 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.writer.ChamyoWriter; import mouda.backend.moim.implement.writer.MoimWriter; import mouda.backend.moim.presentation.response.chamyo.ChamyoFindAllResponses; import mouda.backend.moim.presentation.response.chamyo.MoimRoleFindResponse; -import mouda.backend.notification.business.NotificationService; import mouda.backend.notification.domain.NotificationType; @Service @@ -28,7 +28,7 @@ public class ChamyoService { private final MoimWriter moimWriter; private final ChamyoFinder chamyoFinder; private final ChamyoWriter chamyoWriter; - private final NotificationService notificationService; + private final ChamyoNotificationSender chamyoNotificationSender; @Transactional(readOnly = true) public MoimRoleFindResponse findMoimRole(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { @@ -48,10 +48,10 @@ public ChamyoFindAllResponses findAllChamyo(Long darakbangId, Long moimId) { public void chamyoMoim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { Moim moim = moimFinder.read(moimId, darakbangId); - chamyoWriter.saveAsMoimee(moim, darakbangMember); + Chamyo chamyo = chamyoWriter.saveAsMoimee(moim, darakbangMember); moimWriter.updateMoimStatusIfFull(moim); - notificationService.notifyToMembers(NotificationType.NEW_MOIMEE_JOINED, darakbangId, moim, darakbangMember); + chamyoNotificationSender.sendChamyoNotification(moimId, darakbangMember, NotificationType.NEW_MOIMEE_JOINED); } public void cancelChamyo(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { @@ -59,12 +59,6 @@ public void cancelChamyo(Long darakbangId, Long moimId, DarakbangMember darakban Chamyo chamyo = chamyoFinder.read(moim, darakbangMember); chamyoWriter.delete(chamyo); - sendCancelNotification(darakbangId, darakbangMember, moim); - } - - private void sendCancelNotification(Long darakbangId, DarakbangMember darakbangMember, Moim moim) { - if (moim.isCompleted()) { - notificationService.notifyToMembers(NotificationType.MOIMEE_LEFT, darakbangId, moim, darakbangMember); - } + chamyoNotificationSender.sendChamyoNotification(moimId, darakbangMember, NotificationType.MOIMEE_LEFT); } } diff --git a/backend/src/main/java/mouda/backend/moim/business/ChatService.java b/backend/src/main/java/mouda/backend/moim/business/ChatService.java index 1f1414bdd..76eff6343 100644 --- a/backend/src/main/java/mouda/backend/moim/business/ChatService.java +++ b/backend/src/main/java/mouda/backend/moim/business/ChatService.java @@ -18,6 +18,7 @@ import mouda.backend.moim.implement.finder.ChatFinder; import mouda.backend.moim.implement.finder.ChatRoomFinder; import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.implement.sender.OldChatNotificationSender; import mouda.backend.moim.implement.validator.ChamyoValidator; import mouda.backend.moim.implement.writer.ChatWriter; import mouda.backend.moim.implement.writer.MoimWriter; @@ -27,7 +28,6 @@ import mouda.backend.moim.presentation.request.chat.OldPlaceConfirmRequest; import mouda.backend.moim.presentation.response.chat.OldChatFindUnloadedResponse; import mouda.backend.moim.presentation.response.chat.OldChatPreviewResponses; -import mouda.backend.notification.business.NotificationService; import mouda.backend.notification.domain.NotificationType; @Transactional @@ -35,7 +35,6 @@ @RequiredArgsConstructor public class ChatService { - private final NotificationService notificationService; private final MoimFinder moimFinder; private final MoimWriter moimWriter; private final ChatFinder chatFinder; @@ -43,6 +42,7 @@ public class ChatService { private final ChamyoValidator chamyoValidator; private final ChamyoFinder chamyoFinder; private final ChatRoomFinder chatRoomFinder; + private final OldChatNotificationSender oldChatNotificationSender; public void createChat(long darakbangId, OldChatCreateRequest oldChatCreateRequest, DarakbangMember darakbangMember) { Moim moim = moimFinder.read(oldChatCreateRequest.moimId(), darakbangId); @@ -51,7 +51,7 @@ public void createChat(long darakbangId, OldChatCreateRequest oldChatCreateReque Chat chat = oldChatCreateRequest.toEntity(moim, darakbangMember); chatWriter.save(chat); - notificationService.notifyToMembers(NotificationType.NEW_CHAT, darakbangId, moim, darakbangMember); + oldChatNotificationSender.sendChatNotification(moim, darakbangMember, NotificationType.NEW_CHAT); } @Transactional(readOnly = true) @@ -76,7 +76,7 @@ public void confirmPlace( Chat chat = request.toEntity(moim, darakbangMember); chatWriter.save(chat); - notificationService.notifyToMembers(NotificationType.MOIM_PLACE_CONFIRMED, darakbangId, moim, darakbangMember); + oldChatNotificationSender.sendChatNotification(moim, darakbangMember, NotificationType.MOIM_PLACE_CONFIRMED); } public void confirmDateTime( @@ -88,7 +88,7 @@ public void confirmDateTime( Chat chat = request.toEntity(moim, darakbangMember); chatWriter.save(chat); - notificationService.notifyToMembers(NotificationType.MOIM_TIME_CONFIRMED, darakbangId, moim, darakbangMember); + oldChatNotificationSender.sendChatNotification(moim, darakbangMember, NotificationType.MOIM_TIME_CONFIRMED); } public OldChatPreviewResponses findChatPreview(long darakbangId, DarakbangMember darakbangMember) { 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 500d0af74..f36ce6195 100644 --- a/backend/src/main/java/mouda/backend/moim/business/CommentService.java +++ b/backend/src/main/java/mouda/backend/moim/business/CommentService.java @@ -5,15 +5,12 @@ import lombok.RequiredArgsConstructor; import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Comment; import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.domain.MoimRole; -import mouda.backend.moim.implement.finder.ChamyoFinder; -import mouda.backend.moim.implement.finder.CommentFinder; import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.implement.sender.CommentNotificationSender; import mouda.backend.moim.implement.writer.CommentWriter; import mouda.backend.moim.presentation.request.comment.CommentCreateRequest; -import mouda.backend.notification.business.NotificationService; -import mouda.backend.notification.domain.NotificationType; @Transactional @Service @@ -21,30 +18,15 @@ public class CommentService { private final MoimFinder moimFinder; - private final ChamyoFinder chamyoFinder; - private final CommentFinder commentFinder; private final CommentWriter commentWriter; - private final NotificationService notificationService; + private final CommentNotificationSender commentNotificationSender; public void createComment( Long darakbangId, Long moimId, DarakbangMember darakbangMember, CommentCreateRequest request ) { Moim moim = moimFinder.read(moimId, darakbangId); - commentWriter.saveComment(moim, darakbangMember, request.parentId(), request.content()); + Comment comment = commentWriter.saveComment(moim, darakbangMember, request.parentId(), request.content()); - sendCommentNotification(moim, darakbangMember, request.parentId(), darakbangId); - } - - private void sendCommentNotification(Moim moim, DarakbangMember author, Long parentId, Long darakbangId) { - if (chamyoFinder.readMoimRole(moim, author) == MoimRole.MOIMER) { - return; - } - if (parentId != null) { - Long parentCommentAuthorId = commentFinder.readMemberIdByParentId(parentId); - notificationService.notifyToMember(NotificationType.NEW_REPLY, darakbangId, moim, author, - parentCommentAuthorId); - } - - notificationService.notifyToMembers(NotificationType.NEW_COMMENT, darakbangId, moim, author); + commentNotificationSender.sendCommentNotification(comment, darakbangMember); } } 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 739de86c0..3ec4129c3 100644 --- a/backend/src/main/java/mouda/backend/moim/business/MoimService.java +++ b/backend/src/main/java/mouda/backend/moim/business/MoimService.java @@ -13,13 +13,13 @@ 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.writer.MoimWriter; import mouda.backend.moim.presentation.request.moim.MoimCreateRequest; import mouda.backend.moim.presentation.request.moim.MoimEditRequest; import mouda.backend.moim.presentation.response.comment.CommentResponses; import mouda.backend.moim.presentation.response.moim.MoimDetailsFindResponse; import mouda.backend.moim.presentation.response.moim.MoimFindAllResponses; -import mouda.backend.notification.business.NotificationService; import mouda.backend.notification.domain.NotificationType; @Transactional @@ -30,7 +30,7 @@ public class MoimService { private final MoimWriter moimWriter; private final MoimFinder moimFinder; private final CommentFinder commentFinder; - private final NotificationService notificationService; + private final MoimNotificationSender moimNotificationSender; @Transactional(readOnly = true) public MoimDetailsFindResponse findMoimDetails(long darakbangId, long moimId) { @@ -63,33 +63,32 @@ public MoimFindAllResponses findZzimedMoim(DarakbangMember darakbangMember) { return MoimFindAllResponses.toResponse(moimOverviews); } - public void completeMoim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { - Moim moim = moimFinder.read(moimId, darakbangId); - moimWriter.completeMoim(moim, darakbangMember); - - notificationService.notifyToMembers(NotificationType.MOIMING_COMPLETED, darakbangId, moim, darakbangMember); - } - public Moim createMoim(Long darakbangId, DarakbangMember darakbangMember, MoimCreateRequest moimCreateRequest) { Moim moim = moimWriter.save(moimCreateRequest.toEntity(darakbangId), darakbangMember); - notificationService.notifyToMembers(NotificationType.MOIM_CREATED, darakbangId, moim, darakbangMember); - + moimNotificationSender.sendMoimCreatedNotification(moim, darakbangMember, NotificationType.MOIM_CREATED); return moim; } + public void completeMoim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { + Moim moim = moimFinder.read(moimId, darakbangId); + moimWriter.completeMoim(moim, darakbangMember); + + moimNotificationSender.sendMoimStatusChangedNotification(moim, NotificationType.MOIMING_COMPLETED); + } + public void cancelMoim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { Moim moim = moimFinder.read(moimId, darakbangId); moimWriter.cancelMoim(moim, darakbangMember); - notificationService.notifyToMembers(NotificationType.MOIM_CANCELLED, darakbangId, moim, darakbangMember); + moimNotificationSender.sendMoimStatusChangedNotification(moim, NotificationType.MOIM_CANCELLED); } public void reopenMoim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { Moim moim = moimFinder.read(moimId, darakbangId); moimWriter.reopenMoim(moim, darakbangMember); - notificationService.notifyToMembers(NotificationType.MOINING_REOPENED, darakbangId, moim, darakbangMember); + moimNotificationSender.sendMoimStatusChangedNotification(moim, NotificationType.MOINING_REOPENED); } public void editMoim(Long darakbangId, MoimEditRequest request, DarakbangMember darakbangMember) { @@ -97,6 +96,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()); - notificationService.notifyToMembers(NotificationType.MOIM_MODIFIED, darakbangId, moim, darakbangMember); + moimNotificationSender.sendMoimStatusChangedNotification(moim, NotificationType.MOIM_MODIFIED); } } diff --git a/backend/src/main/java/mouda/backend/moim/domain/Comment.java b/backend/src/main/java/mouda/backend/moim/domain/Comment.java index 0f0ab5b42..05718e04f 100644 --- a/backend/src/main/java/mouda/backend/moim/domain/Comment.java +++ b/backend/src/main/java/mouda/backend/moim/domain/Comment.java @@ -73,11 +73,11 @@ private void validateMember(DarakbangMember darakbangMember) { } } - public boolean isParent() { + public boolean isComment() { return parentId == null; } - public boolean isChild() { + public boolean isReply() { return parentId != null; } diff --git a/backend/src/main/java/mouda/backend/moim/domain/CommentRecipient.java b/backend/src/main/java/mouda/backend/moim/domain/CommentRecipient.java new file mode 100644 index 000000000..dc3efff6a --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/domain/CommentRecipient.java @@ -0,0 +1,16 @@ +package mouda.backend.moim.domain; + +import java.util.List; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; + +@RequiredArgsConstructor +@Getter +public class CommentRecipient { + + private final NotificationType notificationType; + private final List recipients; +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinder.java b/backend/src/main/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinder.java new file mode 100644 index 000000000..73072fe6e --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinder.java @@ -0,0 +1,26 @@ +package mouda.backend.moim.implement.finder; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.notification.domain.Recipient; + +@Component +@RequiredArgsConstructor +public class ChamyoRecipientFinder { + + private final ChamyoRepository chamyoRepository; + + public List getChamyoNotificationRecipients(long moimId, DarakbangMember darakbangMember) { + List chamyos = chamyoRepository.findAllByMoimId(moimId); + return chamyos.stream() + .filter(chamyo -> chamyo.getDarakbangMember().getId() != darakbangMember.getId()) + .map(chamyo -> new Recipient(chamyo.getDarakbangMember().getMemberId(), chamyo.getDarakbangMember().getId())) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/ChatRecipientFinder.java b/backend/src/main/java/mouda/backend/moim/implement/finder/ChatRecipientFinder.java new file mode 100644 index 000000000..8a923e7fe --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/finder/ChatRecipientFinder.java @@ -0,0 +1,28 @@ +package mouda.backend.moim.implement.finder; + +import java.util.List; +import java.util.Objects; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.notification.domain.Recipient; + +@Component +@RequiredArgsConstructor +public class ChatRecipientFinder { + + private final ChamyoRepository chamyoRepository; + + public List getChatNotificationRecipients(long moimId, DarakbangMember darakbangMember) { + List chamyos = chamyoRepository.findAllByMoimId(moimId); + + return chamyos.stream() + .filter(chamyo -> !Objects.equals(chamyo.getDarakbangMember().getId(), darakbangMember.getId())) + .map(chamyo -> new Recipient(chamyo.getDarakbangMember().getMemberId(), chamyo.getDarakbangMember().getId())) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/CommentFinder.java b/backend/src/main/java/mouda/backend/moim/implement/finder/CommentFinder.java index bfaff8511..49003c94e 100644 --- a/backend/src/main/java/mouda/backend/moim/implement/finder/CommentFinder.java +++ b/backend/src/main/java/mouda/backend/moim/implement/finder/CommentFinder.java @@ -22,7 +22,7 @@ public List readAllParentComments(Moim moim) { List comments = commentRepository.findAllByMoimOrderByCreatedAt(moim); return comments.stream() - .filter(Comment::isParent) + .filter(Comment::isComment) .map(parentComment -> new ParentComment(parentComment, getChildComments(parentComment, comments))) .collect(Collectors.toList()); } @@ -32,8 +32,4 @@ private List getChildComments(Comment parentComment, List comm .filter(comment -> Objects.equals(comment.getParentId(), parentComment.getId())) .toList(); } - - public Long readMemberIdByParentId(Long parentId) { - return commentRepository.findMemberIdByParentId(parentId); - } } diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/CommentRecipientFinder.java b/backend/src/main/java/mouda/backend/moim/implement/finder/CommentRecipientFinder.java new file mode 100644 index 000000000..c22dd383f --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/finder/CommentRecipientFinder.java @@ -0,0 +1,107 @@ +package mouda.backend.moim.implement.finder; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.springframework.http.HttpStatus; +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.CommentRecipient; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.exception.ChamyoErrorMessage; +import mouda.backend.moim.exception.ChamyoException; +import mouda.backend.moim.exception.CommentErrorMessage; +import mouda.backend.moim.exception.CommentException; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.CommentRepository; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; + +@Component +@RequiredArgsConstructor +public class CommentRecipientFinder { + + private final ChamyoRepository chamyoRepository; + private final CommentRepository commentRepository; + + public List getAllRecipient(Comment comment) { + if (comment.isComment()) { + return getCommentRecipientWhenComment(comment); + } + return getCommentRecipientWhenReply(comment); + } + + // 댓글 + // 작성자가 방장인 경우: 아무에게도 알림을 보내지 않음. + // 작성자가 방장이 아닌 경우: 방장에게 '댓글' 알림을 보냄. + private List getCommentRecipientWhenComment(Comment comment) { + List result = new ArrayList<>(); + Moim moim = comment.getMoim(); + DarakbangMember moimer = getMoimer(moim); + DarakbangMember author = comment.getDarakbangMember(); + + if (moimer.isNotSameMemberWith(author)) { + result.add(new CommentRecipient(NotificationType.NEW_COMMENT, + List.of(new Recipient(moimer.getMemberId(), moimer.getId())))); + } + + return result; + } + + // 답글 + // 작성자가 방장인 경우: + // -> 원 댓글 작성자가 방장이면 아무에게도 알림 X + // -> 원 댓글 작성자가 방장이 아니면 원 댓글 작성자에게만 답글 알림 + // 작성자가 방장이 아닌 경우: + // -> 원 댓글 작성자가 방장인 경우: 방장에게만 답글 알림 + // -> 원 댓글 작성자가 자신인 경우: 방장에게만 댓글 알림 + // -> 원 댓글 작성자가 방장이 아닌 경우: 원 댓글 작성자에게는 답글 알림, 방장에게는 댓글 알림. + private List getCommentRecipientWhenReply(Comment comment) { + List result = new ArrayList<>(); + Moim moim = comment.getMoim(); + DarakbangMember moimer = getMoimer(moim); + DarakbangMember author = comment.getDarakbangMember(); + DarakbangMember parentAuthor = commentRepository.findParentCommentByParentId(comment.getParentId()) + .map(Comment::getDarakbangMember) + .orElseThrow(() -> new CommentException(HttpStatus.NOT_FOUND, CommentErrorMessage.PARENT_NOT_FOUND)); + + // 작성자가 방장인 경우 + if (author.isSameMemberWith(moimer)) { + // 원 댓글 작성자가 방장이 아닌 경우 + if (parentAuthor.isNotSameMemberWith(moimer)) { + result.add(new CommentRecipient(NotificationType.NEW_REPLY, + List.of(new Recipient(parentAuthor.getMemberId(), parentAuthor.getId())))); + } + } else { + // 작성자가 방장이 아닌 경우 + if (parentAuthor.isSameMemberWith(moimer)) { + // 원 댓글 작성자가 방장인 경우 + result.add(new CommentRecipient(NotificationType.NEW_REPLY, + List.of(new Recipient(moimer.getMemberId(), moimer.getId())))); + } else { + // 원 댓글 작성자가 방장이 아닌 경우 + if (parentAuthor.isNotSameMemberWith(author)) { + result.add(new CommentRecipient(NotificationType.NEW_REPLY, + List.of(new Recipient(parentAuthor.getMemberId(), parentAuthor.getId())))); + } + result.add(new CommentRecipient(NotificationType.NEW_COMMENT, + List.of(new Recipient(moimer.getMemberId(), moimer.getId())))); + } + } + + return result; + } + + private DarakbangMember getMoimer(Moim moim) { + Optional chamyoOptional = chamyoRepository.findMoimerByMoimId(moim.getId()); + if (chamyoOptional.isEmpty()) { + throw new ChamyoException(HttpStatus.NOT_FOUND, ChamyoErrorMessage.NOT_FOUND); + } + return chamyoOptional.get().getDarakbangMember(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/MoimRecipientFinder.java b/backend/src/main/java/mouda/backend/moim/implement/finder/MoimRecipientFinder.java new file mode 100644 index 000000000..cf29becaa --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/finder/MoimRecipientFinder.java @@ -0,0 +1,39 @@ +package mouda.backend.moim.implement.finder; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.notification.domain.Recipient; + +@Component +@RequiredArgsConstructor +public class MoimRecipientFinder { + + private final ChamyoRepository chamyoRepository; + private final DarakbangMemberRepository darakbangMemberRepository; + + public List getMoimCreatedNotificationRecipients(long darakbangId, long authorId) { + List darakbangMembers = darakbangMemberRepository.findAllByDarakbangId(darakbangId); + + return darakbangMembers.stream() + .filter(darakbangMember -> darakbangMember.getId() != authorId) + .map(darakbangMember -> new Recipient(darakbangMember.getMemberId(), darakbangMember.getId())) + .toList(); + } + + public List getMoimStatusChangedNotificationRecipients(long moimId) { + List chamyos = chamyoRepository.findAllByMoimId(moimId); + + return chamyos.stream() + .filter(chamyo -> chamyo.getMoimRole() != MoimRole.MOIMER) + .map(chamyo -> new Recipient(chamyo.getDarakbangMember().getMemberId(), chamyo.getDarakbangMember().getId())) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/sender/AbstractMoimNotificationSender.java b/backend/src/main/java/mouda/backend/moim/implement/sender/AbstractMoimNotificationSender.java new file mode 100644 index 000000000..713e2dc8c --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/sender/AbstractMoimNotificationSender.java @@ -0,0 +1,23 @@ +package mouda.backend.moim.implement.sender; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.UrlConfig; + +@Component +@RequiredArgsConstructor +@EnableConfigurationProperties(UrlConfig.class) +public abstract class AbstractMoimNotificationSender { + + private final UrlConfig urlConfig; + + protected String getMoimUrl(long darakbangId, long moimId) { + return urlConfig.getMoimUrl(darakbangId, moimId); + } + + protected String getChatRoomUrl(long darakbangId, long moimId) { + return urlConfig.getChatRoomUrl(darakbangId, moimId); + } +} 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 new file mode 100644 index 000000000..133efd55e --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/sender/ChamyoNotificationSender.java @@ -0,0 +1,33 @@ +package mouda.backend.moim.implement.sender; + +import java.util.List; + +import org.springframework.context.ApplicationEventPublisher; +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; + +@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 = new NotificationEvent(notificationType, updatedMember.getDarakbang().getName(), notificationType.createMessage(updatedMember.getNickname()), getMoimUrl(updatedMember.getDarakbang().getId(), moimId), recipients); + + eventPublisher.publishEvent(notificationEvent); + } +} 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 new file mode 100644 index 000000000..100218fd0 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/sender/CommentNotificationSender.java @@ -0,0 +1,49 @@ +package mouda.backend.moim.implement.sender; + +import java.util.List; + +import org.springframework.context.ApplicationEventPublisher; +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.CommentRecipient; +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; + +@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) { + List commentRecipients = commentRecipientFinder.getAllRecipient(comment); + + commentRecipients.forEach(commentRecipient -> { + sendNotification(commentRecipient, comment, author); + }); + } + + private void sendNotification(CommentRecipient commentRecipient, Comment comment, DarakbangMember author) { + NotificationType notificationType = commentRecipient.getNotificationType(); + String message = notificationType.createMessage(author.getNickname()); + List recipients = commentRecipient.getRecipients(); + Moim moim = comment.getMoim(); + NotificationEvent notificationEvent = new NotificationEvent(notificationType, moim.getTitle(), message, + getMoimUrl(moim.getDarakbangId(), moim.getId()), recipients); + + eventPublisher.publishEvent(notificationEvent); + } +} 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 new file mode 100644 index 000000000..f4ea5a1b6 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/sender/MoimNotificationSender.java @@ -0,0 +1,52 @@ +package mouda.backend.moim.implement.sender; + +import java.util.List; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import mouda.backend.common.config.UrlConfig; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +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; + +@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()); + Darakbang darakbang = darakbangRepository.findById(moim.getDarakbangId()) + .orElseThrow(IllegalArgumentException::new); + NotificationEvent notificationEvent = new NotificationEvent(notificationType, darakbang.getName(), + notificationType.createMessage(moim.getTitle()), getMoimUrl(darakbang.getId(), moim.getId()), recipients); + eventPublisher.publishEvent(notificationEvent); + } + + public void sendMoimStatusChangedNotification(Moim moim, NotificationType notificationType) { + List recipients = moimRecipientFinder.getMoimStatusChangedNotificationRecipients(moim.getId()); + Darakbang darakbang = darakbangRepository.findById(moim.getDarakbangId()) + .orElseThrow(IllegalArgumentException::new); + NotificationEvent notificationEvent = new NotificationEvent(notificationType, darakbang.getName(), + notificationType.createMessage(moim.getTitle()), getMoimUrl(darakbang.getId(), moim.getId()), recipients); + + eventPublisher.publishEvent(notificationEvent); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/sender/OldChatNotificationSender.java b/backend/src/main/java/mouda/backend/moim/implement/sender/OldChatNotificationSender.java new file mode 100644 index 000000000..78f5fcd90 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/sender/OldChatNotificationSender.java @@ -0,0 +1,46 @@ +package mouda.backend.moim.implement.sender; + +import java.util.List; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import mouda.backend.common.config.UrlConfig; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.finder.ChatRecipientFinder; +import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; + +@Component +public class OldChatNotificationSender extends AbstractMoimNotificationSender { + + private final ChatRecipientFinder chatRecipientFinder; + private final ApplicationEventPublisher eventPublisher; + + public OldChatNotificationSender(UrlConfig urlConfig, ChatRecipientFinder chatRecipientFinder, ApplicationEventPublisher eventPublisher) { + super(urlConfig); + this.chatRecipientFinder = chatRecipientFinder; + this.eventPublisher = eventPublisher; + } + + public void sendChatNotification(Moim moim, DarakbangMember sender, NotificationType notificationType) { + List recipients = chatRecipientFinder.getChatNotificationRecipients(moim.getId(), sender); + NotificationEvent notificationEvent = createNotificationEvent(moim, sender, notificationType, recipients); + + eventPublisher.publishEvent(notificationEvent); + } + + private NotificationEvent createNotificationEvent(Moim moim, DarakbangMember sender, NotificationType notificationType, List recipients) { + String message; + if (notificationType.isConfirmedType()) { + message = notificationType.createMessage(moim.getTitle()); + } else { + message = notificationType.createMessage(sender.getNickname()); + } + + return new NotificationEvent( + notificationType, moim.getTitle(), message, getChatRoomUrl(moim.getDarakbangId(), moim.getId()), recipients, moim.getDarakbangId(), moim.getId()); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/writer/CommentWriter.java b/backend/src/main/java/mouda/backend/moim/implement/writer/CommentWriter.java index 69beec117..2cf971a49 100644 --- a/backend/src/main/java/mouda/backend/moim/implement/writer/CommentWriter.java +++ b/backend/src/main/java/mouda/backend/moim/implement/writer/CommentWriter.java @@ -18,7 +18,7 @@ public class CommentWriter { private final CommentRepository commentRepository; private final CommentValidator commentValidator; - public void saveComment(Moim moim, DarakbangMember darakbangMember, Long parentId, String content) { + public Comment saveComment(Moim moim, DarakbangMember darakbangMember, Long parentId, String content) { commentValidator.validateParentCommentExists(parentId); Comment comment = Comment.builder() @@ -29,6 +29,6 @@ public void saveComment(Moim moim, DarakbangMember darakbangMember, Long parentI .createdAt(LocalDateTime.now()) .build(); - commentRepository.save(comment); + return commentRepository.save(comment); } } diff --git a/backend/src/main/java/mouda/backend/moim/infrastructure/ChamyoRepository.java b/backend/src/main/java/mouda/backend/moim/infrastructure/ChamyoRepository.java index 0bdd16d5d..fcf345e8f 100644 --- a/backend/src/main/java/mouda/backend/moim/infrastructure/ChamyoRepository.java +++ b/backend/src/main/java/mouda/backend/moim/infrastructure/ChamyoRepository.java @@ -33,4 +33,7 @@ public interface ChamyoRepository extends JpaRepository { @Query("SELECT c.lastReadChatId FROM Chamyo c WHERE c.moim.id = :moimId") long findLastReadChatIdByMoimId(@Param("moimId") long moimId); + + @Query("SELECT c FROM Chamyo c WHERE c.moim.id = :moimId AND c.moimRole = 'MOIMER'") + Optional findMoimerByMoimId(@Param("moimId") Long moimId); } diff --git a/backend/src/main/java/mouda/backend/moim/infrastructure/CommentRepository.java b/backend/src/main/java/mouda/backend/moim/infrastructure/CommentRepository.java index a00cfbbbd..2ef460f38 100644 --- a/backend/src/main/java/mouda/backend/moim/infrastructure/CommentRepository.java +++ b/backend/src/main/java/mouda/backend/moim/infrastructure/CommentRepository.java @@ -1,6 +1,7 @@ package mouda.backend.moim.infrastructure; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -15,4 +16,7 @@ public interface CommentRepository extends JpaRepository { Long findMemberIdByParentId(@Param("parentId") long parentId); List findAllByMoimOrderByCreatedAt(Moim moim); + + @Query("SELECT c FROM Comment c WHERE c.id = :parentId") + Optional findParentCommentByParentId(@Param("parentId") Long parentId); } diff --git a/backend/src/main/java/mouda/backend/moim/infrastructure/MoimRepository.java b/backend/src/main/java/mouda/backend/moim/infrastructure/MoimRepository.java index cdbcdf5b8..90898906e 100644 --- a/backend/src/main/java/mouda/backend/moim/infrastructure/MoimRepository.java +++ b/backend/src/main/java/mouda/backend/moim/infrastructure/MoimRepository.java @@ -8,6 +8,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import mouda.backend.darakbang.domain.Darakbang; import mouda.backend.moim.domain.Moim; import mouda.backend.moim.domain.MoimStatus; @@ -31,4 +32,5 @@ public interface MoimRepository extends JpaRepository { Optional findByIdAndDarakbangId(Long moimId, Long darakbangId); boolean existsByIdAndDarakbangId(Long moimId, Long darakbangId); + } diff --git a/backend/src/main/java/mouda/backend/notification/business/FcmTokenService.java b/backend/src/main/java/mouda/backend/notification/business/FcmTokenService.java new file mode 100644 index 000000000..41a0e73b2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/business/FcmTokenService.java @@ -0,0 +1,23 @@ +package mouda.backend.notification.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.implement.fcm.token.FcmTokenWriter; +import mouda.backend.notification.presentation.request.FcmTokenRequest; + +@Service +@RequiredArgsConstructor +@Slf4j +public class FcmTokenService { + + private final FcmTokenWriter fcmTokenWriter; + + @Transactional + public void saveOrRefreshToken(Member member, FcmTokenRequest tokenRequest) { + fcmTokenWriter.saveOrRefresh(member, tokenRequest.token()); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/business/MemberNotificationService.java b/backend/src/main/java/mouda/backend/notification/business/MemberNotificationService.java new file mode 100644 index 000000000..23bf34baf --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/business/MemberNotificationService.java @@ -0,0 +1,26 @@ +package mouda.backend.notification.business; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.notification.domain.MemberNotification; +import mouda.backend.notification.implement.NotificationFinder; +import mouda.backend.notification.presentation.response.MemberNotificationFindAllResponse; + +@Service +@RequiredArgsConstructor +public class MemberNotificationService { + + private final NotificationFinder notificationFinder; + + @Transactional(readOnly = true) + public MemberNotificationFindAllResponse findAllMemberNotification(DarakbangMember darakbangMember) { + List notifications = notificationFinder.findAllMemberNotification(darakbangMember); + + return MemberNotificationFindAllResponse.from(notifications); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/business/NotificationService.java b/backend/src/main/java/mouda/backend/notification/business/NotificationService.java index 943146d64..3bd2a2b85 100644 --- a/backend/src/main/java/mouda/backend/notification/business/NotificationService.java +++ b/backend/src/main/java/mouda/backend/notification/business/NotificationService.java @@ -3,65 +3,37 @@ import java.util.List; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; - -import com.google.firebase.FirebaseException; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.member.domain.Member; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MemberNotification; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.implement.FcmClient; -import mouda.backend.notification.implement.FcmTokenFinder; -import mouda.backend.notification.implement.MemberNotificationFinder; -import mouda.backend.notification.implement.NotificationFactory; -import mouda.backend.notification.implement.RecipientFactory; -import mouda.backend.notification.presentation.request.FcmTokenSaveRequest; -import mouda.backend.notification.presentation.response.NotificationFindAllResponses; +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; -@Slf4j @Service @RequiredArgsConstructor -@Transactional(noRollbackFor = FirebaseException.class) public class NotificationService { - private final NotificationFactory notificationFactory; - private final RecipientFactory recipientFactory; - private final FcmClient fcmClient; - private final MemberNotificationFinder memberNotificationFinder; - private final FcmTokenFinder fcmTokenFinder; - - public void registerFcmToken(long memberId, FcmTokenSaveRequest fcmTokenSaveRequest) { - fcmClient.registerToken(memberId, fcmTokenSaveRequest.token()); - } - - public void notifyToMember(NotificationType type, Long darakbangId, Moim moim, - DarakbangMember sender, long recipientId) { - MoudaNotification notification = notificationFactory.getStrategy(type) - .buildNotification(darakbangId, moim, sender); + private final NotificationWriter notificationWriter; + private final SubscriptionFilterRegistry subscriptionFilterRegistry; + private final NotificationSender notificationSender; - List tokens = fcmTokenFinder.findAllByRecipientId(recipientId); - fcmClient.sendNotification(notification, tokens); - } - - public void notifyToMembers(NotificationType type, Long darakbangId, Moim moim, - DarakbangMember sender) { - MoudaNotification notification = notificationFactory.getStrategy(type) - .buildNotification(darakbangId, moim, sender); - List recipients = recipientFactory.getStrategy(type) - .resolveRecipients(darakbangId, notification, moim, sender); - - List tokens = fcmTokenFinder.findAllByRecipients(recipients); - fcmClient.sendNotification(notification, tokens); - } + @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()); - public NotificationFindAllResponses findAllMyNotifications(Member member, Long darakbangId) { - List memberNotifications = memberNotificationFinder.findAll(member, darakbangId); + SubscriptionFilter subscriptionFilter = subscriptionFilterRegistry.getFilter(notificationEvent.getNotificationType()); + List filteredRecipients = subscriptionFilter.filter(notificationEvent); - return NotificationFindAllResponses.toResponse(memberNotifications); + notificationSender.sendNotification(commonNotification, filteredRecipients); } } diff --git a/backend/src/main/java/mouda/backend/notification/business/SubscriptionService.java b/backend/src/main/java/mouda/backend/notification/business/SubscriptionService.java new file mode 100644 index 000000000..920100bc7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/business/SubscriptionService.java @@ -0,0 +1,49 @@ +package mouda.backend.notification.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.domain.Subscription; +import mouda.backend.notification.implement.subscription.SubscriptionFinder; +import mouda.backend.notification.implement.subscription.SubscriptionWriter; +import mouda.backend.notification.presentation.request.ChatSubscriptionRequest; +import mouda.backend.notification.presentation.response.SubscriptionResponse; + +@Service +@RequiredArgsConstructor +@Transactional +public class SubscriptionService { + + private final SubscriptionFinder subscriptionFinder; + private final SubscriptionWriter subscriptionWriter; + + public SubscriptionResponse readMoimCreateSubscription(Member member) { + Subscription subscription = subscriptionFinder.readSubscription(member); + boolean isSubscribed = subscription.isSubscribedMoimCreate(); + + return SubscriptionResponse.builder() + .isSubscribed(isSubscribed) + .build(); + } + + public SubscriptionResponse readChatRoomSubscription( + Member member, Long darakbangId, Long chatRoomId + ) { + Subscription subscription = subscriptionFinder.readSubscription(member); + boolean isSubscribed = subscription.isSubscribedChatRoom(darakbangId, chatRoomId); + + return SubscriptionResponse.builder() + .isSubscribed(isSubscribed) + .build(); + } + + public void changeMoimCreateSubscription(Member member) { + subscriptionWriter.changeMoimSubscription(member); + } + + public void changeChatRoomSubscription(Member member, ChatSubscriptionRequest request) { + subscriptionWriter.changeChatRoomSubscription(member, request.darakbangId(), request.chatRoomId()); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/CommonNotification.java b/backend/src/main/java/mouda/backend/notification/domain/CommonNotification.java new file mode 100644 index 000000000..8848243b5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/CommonNotification.java @@ -0,0 +1,35 @@ +package mouda.backend.notification.domain; + +import com.google.firebase.messaging.Notification; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class CommonNotification { + + private final NotificationType type; + private final String title; //-> 다락방 이름 + private final String body; + private final LocalDateTime createdAt; + private final String redirectUrl; + + @Builder + public CommonNotification(NotificationType type, String title, String body, String redirectUrl) { + this.type = type; + this.title = title; + this.body = body; + this.redirectUrl = redirectUrl; + this.createdAt = LocalDateTime.now(); + } + + public Notification toNotification() { + return Notification.builder() + .setTitle(title) + .setBody(body) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/FcmFailedResponse.java b/backend/src/main/java/mouda/backend/notification/domain/FcmFailedResponse.java new file mode 100644 index 000000000..0a0f09877 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/FcmFailedResponse.java @@ -0,0 +1,115 @@ +package mouda.backend.notification.domain; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; + +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.MessagingErrorCode; +import com.google.firebase.messaging.SendResponse; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@ToString +public class FcmFailedResponse { + + private static final int DEFAULT_RETRY_AFTER_SECONDS = 60; + + private final BatchResponse batchResponse; + private final List failedWith429Tokens; + private final List failedWith5xxTokens; + private final List nonRetryableFailedTokens; + + + public static FcmFailedResponse from(BatchResponse response, List triedTokens) { + List responses = response.getResponses(); + List failedWith429Tokens = new ArrayList<>(); + List failedWith5xxTokens = new ArrayList<>(); + List nonRetryableFailedTokens = new ArrayList<>(); + + IntStream.range(0, responses.size()) + .forEach(i -> { + SendResponse sendResponse = responses.get(i); + if (sendResponse.isSuccessful()) { + return; + } + String token = triedTokens.get(i); + if (isFailedWith429(sendResponse)) { + failedWith429Tokens.add(token); + return; + } + if (isFailedWith5xx(sendResponse)) { + failedWith5xxTokens.add(token); + return; + } + nonRetryableFailedTokens.add(token); + }); + + return new FcmFailedResponse(response, failedWith429Tokens, failedWith5xxTokens, nonRetryableFailedTokens); + } + + private static boolean isFailedWith429(SendResponse response) { + return hasSameErrorCode(response, MessagingErrorCode.QUOTA_EXCEEDED); + } + + private static boolean isFailedWith5xx(SendResponse response) { + return hasSameErrorCode(response, MessagingErrorCode.INTERNAL, MessagingErrorCode.UNAVAILABLE); + } + + private static boolean hasSameErrorCode(SendResponse response, MessagingErrorCode... errorCodes) { + if (response.isSuccessful()) { + return false; + } + FirebaseMessagingException exception = response.getException(); + return Arrays.stream(errorCodes) + .anyMatch(errorCode -> exception.getMessagingErrorCode() == errorCode); + } + + public boolean hasNoRetryableTokens() { + return failedWith429Tokens.isEmpty() && failedWith5xxTokens.isEmpty(); + } + + public boolean hasFailedWith429Tokens() { + return !failedWith429Tokens.isEmpty(); + } + + public boolean hasFailedWith5xxTokens() { + return !failedWith5xxTokens.isEmpty(); + } + + public List getFinallyFailedTokens() { + List failedTokens = new ArrayList<>(); + failedTokens.addAll(failedWith429Tokens); + failedTokens.addAll(failedWith5xxTokens); + failedTokens.addAll(nonRetryableFailedTokens); + return failedTokens; + } + + public int getRetryAfterSeconds() { + List responses = batchResponse.getResponses(); + return responses.stream() + .filter(FcmFailedResponse::isFailedWith429) + .map(this::parseRetryAfterSeconds) + .findAny() + .orElse(DEFAULT_RETRY_AFTER_SECONDS); + } + + private int parseRetryAfterSeconds(SendResponse response) { + Object retryAfterHeader = response.getException().getHttpResponse().getHeaders().get("Retry-After"); + if (retryAfterHeader == null) { + return DEFAULT_RETRY_AFTER_SECONDS; + } + try { + return Integer.parseInt(retryAfterHeader.toString()); + } catch (NumberFormatException e) { + return DEFAULT_RETRY_AFTER_SECONDS; + } + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/FcmToken.java b/backend/src/main/java/mouda/backend/notification/domain/FcmToken.java deleted file mode 100644 index e0756cf97..000000000 --- a/backend/src/main/java/mouda/backend/notification/domain/FcmToken.java +++ /dev/null @@ -1,42 +0,0 @@ -package mouda.backend.notification.domain; - -import java.time.LocalDateTime; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor -public class FcmToken { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private long memberId; - - @Column(nullable = false) - private String token; - - @Column(nullable = false) - private LocalDateTime timestamp; - - @Builder - public FcmToken(long memberId, String fcmToken) { - this.memberId = memberId; - this.token = fcmToken; - this.timestamp = LocalDateTime.now(); - } - - public void refreshTimestamp() { - this.timestamp = LocalDateTime.now(); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/domain/MemberNotification.java b/backend/src/main/java/mouda/backend/notification/domain/MemberNotification.java index 8fa8dfa02..391ac6ed9 100644 --- a/backend/src/main/java/mouda/backend/notification/domain/MemberNotification.java +++ b/backend/src/main/java/mouda/backend/notification/domain/MemberNotification.java @@ -1,39 +1,20 @@ package mouda.backend.notification.domain; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import java.time.LocalDateTime; + import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; -@Entity +@RequiredArgsConstructor @Getter -@NoArgsConstructor +@Builder public class MemberNotification { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private long memberId; - - @Column(nullable = false) - private long darakbangId; - - @ManyToOne - @JoinColumn(nullable = false) - private MoudaNotification moudaNotification; + private final String title; + private final String message; + private final LocalDateTime createdAt; + private final String type; + private final String redirectUrl; - @Builder - public MemberNotification(long memberId, long darakbangId, MoudaNotification moudaNotification) { - this.memberId = memberId; - this.darakbangId = darakbangId; - this.moudaNotification = moudaNotification; - } } diff --git a/backend/src/main/java/mouda/backend/notification/domain/MoudaNotification.java b/backend/src/main/java/mouda/backend/notification/domain/MoudaNotification.java deleted file mode 100644 index acae1fee4..000000000 --- a/backend/src/main/java/mouda/backend/notification/domain/MoudaNotification.java +++ /dev/null @@ -1,56 +0,0 @@ -package mouda.backend.notification.domain; - -import java.time.LocalDateTime; - -import com.google.firebase.messaging.Notification; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor -public class MoudaNotification { - - private static final String DEFAULT_NOTIFICATION_TITLE = "모우다"; - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String body; - - @Column(nullable = false) - private String targetUrl; - - @Enumerated(value = EnumType.STRING) - @Column(nullable = false) - private NotificationType type; - - @Column(nullable = false) - private LocalDateTime createdAt; - - @Builder - public MoudaNotification(String body, String targetUrl, NotificationType type) { - this.body = body; - this.targetUrl = targetUrl; - this.type = type; - this.createdAt = LocalDateTime.now(); - } - - public Notification toFcmNotification() { - return Notification.builder() - .setTitle(DEFAULT_NOTIFICATION_TITLE) - .setBody(body) - .build(); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/domain/Notifiable.java b/backend/src/main/java/mouda/backend/notification/domain/Notifiable.java deleted file mode 100644 index 17ccd3f08..000000000 --- a/backend/src/main/java/mouda/backend/notification/domain/Notifiable.java +++ /dev/null @@ -1,8 +0,0 @@ -package mouda.backend.notification.domain; - -public interface Notifiable { - - String getMessageData(); - - long getId(); -} diff --git a/backend/src/main/java/mouda/backend/notification/domain/NotificationEvent.java b/backend/src/main/java/mouda/backend/notification/domain/NotificationEvent.java new file mode 100644 index 000000000..f9692fe6a --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/NotificationEvent.java @@ -0,0 +1,43 @@ +package mouda.backend.notification.domain; + +import java.util.List; + +import lombok.Getter; + +@Getter +public class NotificationEvent { + + private final NotificationType notificationType; + private final String title; + private final String body; + private final String redirectUrl; + private final List recipients; + private final Long darakbangId; + private final Long chatRoomId; + + public NotificationEvent(NotificationType notificationType, String title, String body, String redirectUrl, List recipients) { + this.notificationType = notificationType; + this.title = title; + this.body = body; + this.redirectUrl = redirectUrl; + this.recipients = recipients; + this.darakbangId = null; + this.chatRoomId = null; + } + + public NotificationEvent(NotificationType notificationType, String title, String body, String redirectUrl, List recipients, Long darakbangId, Long chatRoomId) { + this.notificationType = notificationType; + this.title = title; + this.body = body; + this.redirectUrl = redirectUrl; + this.recipients = recipients; + this.darakbangId = darakbangId; + this.chatRoomId = chatRoomId; + } + + public CommonNotification toCommonNotification() { + return new CommonNotification( + notificationType, title, body, redirectUrl + ); + } +} 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 e4a367587..2bf845c65 100644 --- a/backend/src/main/java/mouda/backend/notification/domain/NotificationType.java +++ b/backend/src/main/java/mouda/backend/notification/domain/NotificationType.java @@ -4,21 +4,30 @@ public enum NotificationType { - // 모든 다락방 내 회원에게 전달 + // 전체 전송 + // MoimEvent -> 방장을 제외한 사람들한테 알림 MOIM_CREATED(moimName -> moimName + " 모임이 만들어졌어요!"), - // 해당되는 회원에게만 전달 + // 방장 제외 참여자 MOIMING_COMPLETED(moimName -> moimName + " 모집이 마감되었어요!"), MOINING_REOPENED(moimName -> moimName + " 모집이 재개되었어요!"), MOIM_CANCELLED(moimName -> moimName + " 모임이 취소되었어요!"), MOIM_MODIFIED(moimName -> moimName + " 모임 정보가 변경되었어요!"), + MOIM_PLACE_CONFIRMED(moimName -> moimName + " 모임 장소가 확정되었어요!"), + MOIM_TIME_CONFIRMED(moimName -> moimName + " 모임 시간이 확정되었어요!"), + + // ChamyoEvent -> 참여자(나간사람)를 제외한 나머지 모든 모임 참여자 + NEW_MOIMEE_JOINED(memberName -> memberName + " 님이 모임에 참여했어요!"), + MOIMEE_LEFT(memberName -> memberName + " 님이 참여를 취소했어요!"), + + // 댓글 쓴사람 제외 참여자 + // 댓글 NEW_COMMENT(memberName -> memberName + " 님이 댓글을 남겼어요!"), NEW_REPLY(memberName -> memberName + " 님이 답글을 남겼어요!"), + + // 채팅방 참여자 + // 채팅 NEW_CHAT(memberName -> memberName + " 님이 메시지를 보냈어요!"), - NEW_MOIMEE_JOINED(memberName -> memberName + " 님이 모임에 참여했어요!"), - MOIMEE_LEFT(memberName -> memberName + " 님이 참여를 취소했어요!"), - MOIM_PLACE_CONFIRMED(moimName -> moimName + " 모임 장소가 확정되었어요!"), - MOIM_TIME_CONFIRMED(moimName -> moimName + " 모임 시간이 확정되었어요!"), ; private final Function messageFunction; @@ -27,7 +36,11 @@ public enum NotificationType { this.messageFunction = messageFunction; } - public String createMessage(String moimName) { - return messageFunction.apply(moimName); + public String createMessage(String prefix) { + return messageFunction.apply(prefix); + } + + public boolean isConfirmedType() { + return this == MOIM_PLACE_CONFIRMED || this == MOIM_TIME_CONFIRMED; } } diff --git a/backend/src/main/java/mouda/backend/notification/domain/NotificationTypeProvider.java b/backend/src/main/java/mouda/backend/notification/domain/NotificationTypeProvider.java deleted file mode 100644 index e00bf38d5..000000000 --- a/backend/src/main/java/mouda/backend/notification/domain/NotificationTypeProvider.java +++ /dev/null @@ -1,12 +0,0 @@ -package mouda.backend.notification.domain; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface NotificationTypeProvider { - NotificationType value(); -} diff --git a/backend/src/main/java/mouda/backend/notification/domain/Recipient.java b/backend/src/main/java/mouda/backend/notification/domain/Recipient.java new file mode 100644 index 000000000..d19510997 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/Recipient.java @@ -0,0 +1,15 @@ +package mouda.backend.notification.domain; + +import lombok.Getter; + +@Getter +public class Recipient { + + private long memberId; + private long darakbangMemberId; + + public Recipient(long memberId, long darakbangMemberId) { + this.memberId = memberId; + this.darakbangMemberId = darakbangMemberId; + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/Subscription.java b/backend/src/main/java/mouda/backend/notification/domain/Subscription.java new file mode 100644 index 000000000..8cde927c2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/Subscription.java @@ -0,0 +1,28 @@ +package mouda.backend.notification.domain; + +import java.util.List; +import java.util.Map; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +@Builder +public class Subscription { + + private final boolean isSubscribedMoimCreate; + private final Map> unsubscribedChatRooms; + + public boolean isSubscribedChatRoom(Long darakbangId, Long chatRoomId) { + if (unsubscribedChatRooms.containsKey(darakbangId)) { + return !unsubscribedChatRooms.get(darakbangId).contains(chatRoomId); + } + return true; + } + + public boolean isSubscribedMoimCreate() { + return isSubscribedMoimCreate; + } +} diff --git a/backend/src/main/java/mouda/backend/notification/exception/NotificationErrorMessage.java b/backend/src/main/java/mouda/backend/notification/exception/NotificationErrorMessage.java deleted file mode 100644 index 87eb1e34c..000000000 --- a/backend/src/main/java/mouda/backend/notification/exception/NotificationErrorMessage.java +++ /dev/null @@ -1,17 +0,0 @@ -package mouda.backend.notification.exception; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum NotificationErrorMessage { - - // Firebase - FCM_TOKEN_NOT_FOUND_BY_MEMBER("회원의 FCM 토큰이 존재하지 않아요."), - FCM_TOKEN_NOT_FOUND_BY_TOKEN("요청한 FCM 토큰이 존재하지 않아요."); - - // Notification - - private final String message; -} diff --git a/backend/src/main/java/mouda/backend/notification/exception/NotificationException.java b/backend/src/main/java/mouda/backend/notification/exception/NotificationException.java deleted file mode 100644 index d9d937fe9..000000000 --- a/backend/src/main/java/mouda/backend/notification/exception/NotificationException.java +++ /dev/null @@ -1,12 +0,0 @@ -package mouda.backend.notification.exception; - -import org.springframework.http.HttpStatus; - -import mouda.backend.common.exception.MoudaException; - -public class NotificationException extends MoudaException { - - public NotificationException(HttpStatus httpStatus, NotificationErrorMessage notificationErrorMessage) { - super(httpStatus, notificationErrorMessage.getMessage()); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/FcmClient.java b/backend/src/main/java/mouda/backend/notification/implement/FcmClient.java deleted file mode 100644 index 16bd5604c..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/FcmClient.java +++ /dev/null @@ -1,113 +0,0 @@ -package mouda.backend.notification.implement; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.IntStream; - -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -import com.google.firebase.messaging.BatchResponse; -import com.google.firebase.messaging.FirebaseMessaging; -import com.google.firebase.messaging.FirebaseMessagingException; -import com.google.firebase.messaging.MessagingErrorCode; -import com.google.firebase.messaging.MulticastMessage; -import com.google.firebase.messaging.SendResponse; -import com.google.firebase.messaging.WebpushConfig; -import com.google.firebase.messaging.WebpushFcmOptions; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import mouda.backend.notification.domain.FcmToken; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.infrastructure.FcmTokenRepository; - -@Slf4j -@Service -@RequiredArgsConstructor -public class FcmClient { - - private final FcmTokenRepository fcmTokenRepository; - - public void sendNotification(MoudaNotification notification, List tokens) { - if (tokens.isEmpty()) { - return; - } - - List> chunkedTokens = chunkFcmTokensForMulticast(tokens); - - chunkedTokens.stream() - .map(chunk -> MulticastMessage.builder() - .setNotification(notification.toFcmNotification()) - .setWebpushConfig(getWebpushConfig(notification.getTargetUrl())) - .addAllTokens(chunk) - .build()) - .forEach(message -> { - try { - BatchResponse batchResponse = FirebaseMessaging.getInstance().sendEachForMulticast(message); - validateFcmTokenByErrorCode(tokens, batchResponse); - } catch (FirebaseMessagingException e) { - log.error("Failed to send message: {}", e.getMessage()); - } - }); - } - - private List> chunkFcmTokensForMulticast(List tokens) { - int defaultChunkSize = 500; - List> result = new ArrayList<>(); - for (int i = 0; i < tokens.size(); i += defaultChunkSize) { - result.add(tokens.subList(i, Math.min(i + defaultChunkSize, tokens.size()))); - } - return result; - } - - private WebpushConfig getWebpushConfig(String url) { - return WebpushConfig.builder() - .setFcmOptions(WebpushFcmOptions.withLink(url)) - .build(); - } - - private void validateFcmTokenByErrorCode(List tokens, BatchResponse batchResponse) { - if (batchResponse.getFailureCount() == 0) { - return; - } - - List responses = batchResponse.getResponses(); - IntStream.range(0, responses.size()) - .filter(index -> isInvalidTokenErrorCode(responses.get(index))) - .forEach(index -> fcmTokenRepository.deleteByToken(tokens.get(index))); - } - - private boolean isInvalidTokenErrorCode(SendResponse sendResponse) { - if (sendResponse.isSuccessful()) { - return false; - } - MessagingErrorCode errorCode = sendResponse.getException().getMessagingErrorCode(); - return errorCode == MessagingErrorCode.UNREGISTERED || errorCode == MessagingErrorCode.INVALID_ARGUMENT; - } - - public void registerToken(long memberId, String token) { - fcmTokenRepository.findByToken(token) - .ifPresentOrElse( - FcmToken::refreshTimestamp, - () -> { - FcmToken fcmToken = FcmToken.builder() - .memberId(memberId) - .fcmToken(token) - .build(); - fcmTokenRepository.save(fcmToken); - } - ); - } - - @Scheduled(cron = "0 0 0 1 * *") - public void cleanInactiveTokens() { - fcmTokenRepository.findAll() - .forEach(token -> { - if (token.getTimestamp().isBefore(LocalDateTime.now().minusMonths(1L))) { - fcmTokenRepository.delete(token); - } - }); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/FcmTokenFinder.java b/backend/src/main/java/mouda/backend/notification/implement/FcmTokenFinder.java deleted file mode 100644 index 20171086f..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/FcmTokenFinder.java +++ /dev/null @@ -1,23 +0,0 @@ -package mouda.backend.notification.implement; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import mouda.backend.notification.infrastructure.FcmTokenRepository; - -@Component -@RequiredArgsConstructor -public class FcmTokenFinder { - - private final FcmTokenRepository fcmTokenRepository; - - public List findAllByRecipientId(long recipientId) { - return fcmTokenRepository.findAllTokenByMemberId(recipientId); - } - - public List findAllByRecipients(List recipients) { - return fcmTokenRepository.findAllTokenByMemberIds(recipients); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/MemberNotificationFinder.java b/backend/src/main/java/mouda/backend/notification/implement/MemberNotificationFinder.java deleted file mode 100644 index 1ecfb533c..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/MemberNotificationFinder.java +++ /dev/null @@ -1,22 +0,0 @@ -package mouda.backend.notification.implement; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import mouda.backend.member.domain.Member; -import mouda.backend.notification.domain.MemberNotification; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@Component -@RequiredArgsConstructor -public class MemberNotificationFinder { - - private final MemberNotificationRepository memberNotificationRepository; - - public List findAll(Member member, long darakbangId) { - return memberNotificationRepository.findAllByMemberIdAndDarakbangIdOrderByIdDesc( - member.getId(), darakbangId); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/MessageFactory.java b/backend/src/main/java/mouda/backend/notification/implement/MessageFactory.java new file mode 100644 index 000000000..fc3ede523 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/MessageFactory.java @@ -0,0 +1,10 @@ +package mouda.backend.notification.implement; + +import java.util.List; + +import mouda.backend.notification.domain.CommonNotification; + +public interface MessageFactory { + + T createMessage(CommonNotification notification, List receivers); +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationFactory.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationFactory.java deleted file mode 100644 index db7439008..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/NotificationFactory.java +++ /dev/null @@ -1,30 +0,0 @@ -package mouda.backend.notification.implement; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.stereotype.Service; - -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.implement.notificationbuilder.NotificationBuilderStrategy; - -@Service -public class NotificationFactory { - - private final Map strategies; - - public NotificationFactory(List strategyList) { - this.strategies = strategyList.stream() - .collect( - Collectors.toMap(strategy -> { - return strategy.getClass().getAnnotation(NotificationTypeProvider.class).value(); - }, - strategy -> strategy)); - } - - public NotificationBuilderStrategy getStrategy(NotificationType type) { - return strategies.get(type); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationFinder.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationFinder.java new file mode 100644 index 000000000..91da690ac --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/NotificationFinder.java @@ -0,0 +1,35 @@ +package mouda.backend.notification.implement; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.notification.domain.MemberNotification; +import mouda.backend.notification.infrastructure.entity.MemberNotificationEntity; +import mouda.backend.notification.infrastructure.repository.MemberNotificationRepository; + +@Component +@RequiredArgsConstructor +public class NotificationFinder { + + private final MemberNotificationRepository memberNotificationRepository; + + public List findAllMemberNotification(DarakbangMember darakbangMember) { + return memberNotificationRepository.findAllByDarakbangMemberId(darakbangMember.getId()).stream() + .map(this::convertTo) + .sorted((n1, n2) -> n2.getCreatedAt().compareTo(n1.getCreatedAt())) + .toList(); + } + + private MemberNotification convertTo(MemberNotificationEntity entity) { + return MemberNotification.builder() + .title(entity.getTitle()) + .message(entity.getBody()) + .createdAt(entity.getCreatedAt()) + .type(entity.getType()) + .redirectUrl(entity.getTargetUrl()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationSender.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationSender.java new file mode 100644 index 000000000..846146afb --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/NotificationSender.java @@ -0,0 +1,11 @@ +package mouda.backend.notification.implement; + +import java.util.List; + +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.Recipient; + +public interface NotificationSender { + + void sendNotification(CommonNotification notification, List recipients); +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationWriter.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationWriter.java new file mode 100644 index 000000000..2a8c3b229 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/NotificationWriter.java @@ -0,0 +1,41 @@ +package mouda.backend.notification.implement; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.CommonNotification; +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; + +@Component +@RequiredArgsConstructor +public class NotificationWriter { + + private final MemberNotificationRepository memberNotificationRepository; + + public void saveAllMemberNotification(CommonNotification notification, List recipients) { + if (notification.getType() == NotificationType.NEW_CHAT) { + return; + } + List memberNotifications = recipients.stream() + .map(recipient -> createEntity(notification, recipient)) + .toList(); + + memberNotificationRepository.saveAll(memberNotifications); + } + + private MemberNotificationEntity createEntity(CommonNotification notification, Recipient recipient) { + return MemberNotificationEntity.builder() + .darakbangMemberId(recipient.getDarakbangMemberId()) + .type(notification.getType().name()) + .title(notification.getTitle()) + .body(notification.getBody()) + .targeturl(notification.getRedirectUrl()) + .createdAt(notification.getCreatedAt()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/RecipientFactory.java b/backend/src/main/java/mouda/backend/notification/implement/RecipientFactory.java deleted file mode 100644 index c109d0eab..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/RecipientFactory.java +++ /dev/null @@ -1,29 +0,0 @@ -package mouda.backend.notification.implement; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.stereotype.Service; - -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.implement.recipientresolver.RecipientResolverStrategy; - -@Service -public class RecipientFactory { - private final Map strategies; - - public RecipientFactory(List strategyList) { - this.strategies = strategyList.stream() - .collect( - Collectors.toMap(strategy -> { - return strategy.getClass().getAnnotation(NotificationTypeProvider.class).value(); - }, - strategy -> strategy)); - } - - public RecipientResolverStrategy getStrategy(NotificationType type) { - return strategies.get(type); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmConfigBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmConfigBuilder.java new file mode 100644 index 000000000..1295c981a --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmConfigBuilder.java @@ -0,0 +1,8 @@ +package mouda.backend.notification.implement.fcm; + +import com.google.firebase.messaging.MulticastMessage; + +public interface FcmConfigBuilder { + + MulticastMessage.Builder buildMulticastMessage(MulticastMessage.Builder builder); +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmMessageFactory.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmMessageFactory.java new file mode 100644 index 000000000..47cbdbc06 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmMessageFactory.java @@ -0,0 +1,50 @@ +package mouda.backend.notification.implement.fcm; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.google.firebase.messaging.MulticastMessage; +import com.google.firebase.messaging.MulticastMessage.Builder; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.implement.MessageFactory; + +@Component +@RequiredArgsConstructor +public class FcmMessageFactory implements MessageFactory> { + + private static final int TOKEN_BATCH_SIZE = 500; + private final List fcmConfigBuilders; + + @Override + public List createMessage(CommonNotification notification, List fcmTokens) { + List> partitionedTokens = partitionTokensByBatch(fcmTokens); + + return partitionedTokens.stream() + .map(tokens -> defaultMulticastMessageBuilder(notification).addAllTokens(tokens).build()) + .toList(); + } + + private Builder defaultMulticastMessageBuilder(CommonNotification notification) { + Builder builder = MulticastMessage.builder() + .setNotification(notification.toNotification()) + .putData("link", notification.getRedirectUrl()); + + for (FcmConfigBuilder configBuilder : fcmConfigBuilders) { + builder = configBuilder.buildMulticastMessage(builder); + } + + return builder; + } + + private List> partitionTokensByBatch(List tokens) { + List> result = new ArrayList<>(); + for (int i = 0; i < tokens.size(); i += TOKEN_BATCH_SIZE) { + result.add(tokens.subList(i, Math.min(i + TOKEN_BATCH_SIZE, tokens.size()))); + } + return result; + } +} 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 new file mode 100644 index 000000000..072b7d5da --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmNotificationSender.java @@ -0,0 +1,111 @@ +package mouda.backend.notification.implement.fcm; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.stream.IntStream; + +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.MessagingErrorCode; +import com.google.firebase.messaging.MulticastMessage; +import com.google.firebase.messaging.SendResponse; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.implement.NotificationSender; +import mouda.backend.notification.implement.fcm.token.FcmTokenFinder; +import mouda.backend.notification.implement.fcm.token.FcmTokenWriter; + +@Component +@Slf4j +@RequiredArgsConstructor +public class FcmNotificationSender implements NotificationSender { + + private static final int THREAD_POOL_SIZE_FOR_CALLBACK = 5; + + private final FcmMessageFactory fcmMessageFactory; + private final FcmTokenFinder fcmTokenFinder; + private final FcmResponseHandler fcmResponseHandler; + private final FcmTokenWriter fcmTokenWriter; + + @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; + } + + fcmMessageFactory.createMessage(notification, tokens) + .forEach(multicastMessage -> sendMulticastMessage(notification, multicastMessage, tokens)); + } + + private void sendMulticastMessage(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. error code: {}, messaging error code: {}, error message: {}", + exception.getErrorCode(), exception.getMessagingErrorCode(), exception.getMessage()); + } + } + + @Override + public void onSuccess(BatchResponse result) { + if (result.getFailureCount() == 0) { + log.info("All messages were sent successfully. message: {}", notification.getTitle()); + return; + } + List registeredTokens = checkUnregisteredTokensAndDelete(result, initialTokens); + if (registeredTokens.isEmpty()) { + return; + } + fcmResponseHandler.handleBatchResponse(result, notification, registeredTokens); + } + }, Executors.newFixedThreadPool(THREAD_POOL_SIZE_FOR_CALLBACK)); + } + + /** + * @param batchResponse 처음 알림을 전송했을 때의 응답 + * @param tokens 처음 알림을 전송했을 때 사용한 토큰 + * @return 등록되지 않은(FCM 에서 Unregistered 를 응답한) 토큰을 제거한 뒤, 나머지의 토큰을 반환 + */ + private List checkUnregisteredTokensAndDelete(BatchResponse batchResponse, List tokens) { + tokens = new ArrayList<>(tokens); + List responses = batchResponse.getResponses(); + List unregisteredTokens = IntStream.range(0, responses.size()) + .filter(i -> isUnregistered(responses.get(i))) + .mapToObj(tokens::get) + .toList(); + + if (!unregisteredTokens.isEmpty()) { + log.info("{} of {} tokens are unregistered. Deleting them..", unregisteredTokens.size(), tokens.size()); + fcmTokenWriter.deleteAll(unregisteredTokens); + tokens.removeAll(unregisteredTokens); + } + + return tokens; + } + + private boolean isUnregistered(SendResponse response) { + if (response.isSuccessful()) { + return false; + } + MessagingErrorCode messagingErrorCode = response.getException().getMessagingErrorCode(); + return messagingErrorCode == MessagingErrorCode.UNREGISTERED; + } +} 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 new file mode 100644 index 000000000..c402182f6 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmResponseHandler.java @@ -0,0 +1,96 @@ +package mouda.backend.notification.implement.fcm; + +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.springframework.stereotype.Component; + +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 jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.FcmFailedResponse; + +@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 FcmMessageFactory fcmMessageFactory; + + @PreDestroy + public void destroy() { + scheduler.shutdown(); + } + + public void handleBatchResponse(BatchResponse batchResponse, CommonNotification notification, + List initialTokens) { + FcmFailedResponse failedResponse = FcmFailedResponse.from(batchResponse, initialTokens); + + int attempt = 1; + retryAsync(notification, failedResponse, attempt, BACKOFF_DELAY_FOR_SECONDS); + } + + private void retryAsync(CommonNotification notification, FcmFailedResponse failedResponse, int attempt, + int backoffDelayForSeconds) { + if (attempt > MAX_ATTEMPT) { + log.info("Max attempt reached for notification: {}. failed: {}", notification.getBody(), + failedResponse.getFinallyFailedTokens()); + return; + } + + if (failedResponse.hasNoRetryableTokens()) { + log.info("No Retryable tokens for notification: {}. failed: {}.", notification.getBody(), + failedResponse.getNonRetryableFailedTokens()); + return; + } + + if (failedResponse.hasFailedWith429Tokens()) { + int retryAfterSeconds = failedResponse.getRetryAfterSeconds(); + scheduler.schedule(() -> { + log.info("Retrying 429 for notification: {}. Thread: {}", notification.getTitle(), + Thread.currentThread().getName()); + FcmFailedResponse retryResponse = retry(failedResponse, notification, + failedResponse.getFailedWith429Tokens()); + retryAsync(notification, retryResponse, attempt + 1, backoffDelayForSeconds * BACKOFF_MULTIPLIER); + }, retryAfterSeconds, TimeUnit.SECONDS); + } + + if (failedResponse.hasFailedWith5xxTokens()) { + scheduler.schedule(() -> { + log.info("Retrying 5xx for notification: {}. Thread: {}", notification.getTitle(), + Thread.currentThread().getName()); + FcmFailedResponse retryResponse = retry(failedResponse, notification, + failedResponse.getFailedWith5xxTokens()); + retryAsync(notification, retryResponse, attempt + 1, backoffDelayForSeconds * BACKOFF_MULTIPLIER); + }, backoffDelayForSeconds, TimeUnit.SECONDS); + } + } + + private FcmFailedResponse retry(FcmFailedResponse origin, CommonNotification notification, + List retryTokens) { + log.info("Retrying for notification: {}. failed: {}, Thread: {}", notification.getTitle(), retryTokens, + Thread.currentThread().getName()); + MulticastMessage message = fcmMessageFactory.createMessage(notification, retryTokens).get(0); + + try { + BatchResponse response = FirebaseMessaging.getInstance().sendEachForMulticast(message); + return FcmFailedResponse.from(response, retryTokens); + } catch (FirebaseMessagingException e) { + log.error("Error Sending Message. error message: {}", e.getMessage()); + return origin; + } + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/WebFcmConfigBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/WebFcmConfigBuilder.java new file mode 100644 index 000000000..533e62cbc --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/WebFcmConfigBuilder.java @@ -0,0 +1,32 @@ +package mouda.backend.notification.implement.fcm; + +import org.springframework.stereotype.Component; + +import com.google.firebase.messaging.MulticastMessage; +import com.google.firebase.messaging.WebpushConfig; +import com.google.firebase.messaging.WebpushFcmOptions; + +@Component +public class WebFcmConfigBuilder implements FcmConfigBuilder { + + private static final WebpushConfig webpushConfig; + + static { + webpushConfig = createWebpushConfig(); + } + + @Override + public MulticastMessage.Builder buildMulticastMessage(MulticastMessage.Builder builder) { + return builder.setWebpushConfig(webpushConfig); + } + + private static WebpushConfig createWebpushConfig() { + return WebpushConfig.builder() + .setFcmOptions(option()) + .build(); + } + + private static WebpushFcmOptions option() { + return WebpushFcmOptions.builder().build(); + } +} 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 new file mode 100644 index 000000000..fda9990f4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenFinder.java @@ -0,0 +1,29 @@ +package mouda.backend.notification.implement.fcm.token; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.infrastructure.entity.FcmTokenEntity; +import mouda.backend.notification.infrastructure.repository.FcmTokenRepository; + +@Component +@RequiredArgsConstructor +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; + } +} 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 new file mode 100644 index 000000000..4a074e1cd --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenScheduler.java @@ -0,0 +1,32 @@ +package mouda.backend.notification.implement.fcm.token; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.infrastructure.entity.FcmTokenEntity; +import mouda.backend.notification.infrastructure.repository.FcmTokenRepository; + +@Component +@RequiredArgsConstructor +public class FcmTokenScheduler { + + private final FcmTokenRepository fcmTokenRepository; + + @Scheduled(cron = "0 0 0 1 * ?") + public void checkAllTokensIfInactiveOrExpired() { + fcmTokenRepository.findAll().forEach(this::deactiveOrDelete); + } + + private void deactiveOrDelete(FcmTokenEntity tokenEntity) { + if (tokenEntity.isExpired()) { + fcmTokenRepository.delete(tokenEntity); + return; + } + if (tokenEntity.isInactive()) { + tokenEntity.refresh(); + tokenEntity.deactivate(); + fcmTokenRepository.save(tokenEntity); + } + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriter.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriter.java new file mode 100644 index 000000000..93016a9ef --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriter.java @@ -0,0 +1,46 @@ +package mouda.backend.notification.implement.fcm.token; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.infrastructure.entity.FcmTokenEntity; +import mouda.backend.notification.infrastructure.repository.FcmTokenRepository; + +@Component +@RequiredArgsConstructor +public class FcmTokenWriter { + + private final FcmTokenRepository fcmTokenRepository; + + public void saveOrRefresh(Member member, String token) { + Optional tokenEntity = fcmTokenRepository.findByToken(token); + tokenEntity.ifPresentOrElse(this::refresh, () -> save(member, token)); + } + + private void refresh(FcmTokenEntity tokenEntity) { + if (tokenEntity.isInactive()) { + tokenEntity.activate(); + } + + tokenEntity.refresh(); + fcmTokenRepository.save(tokenEntity); + } + + private void save(Member member, String token) { + FcmTokenEntity tokenEntity = FcmTokenEntity.builder() + .memberId(member.getId()) + .token(token) + .build(); + fcmTokenRepository.save(tokenEntity); + } + + @Transactional + public void deleteAll(List unregisteredTokens) { + fcmTokenRepository.deleteAllByTokenIn(unregisteredTokens); + } +} 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 new file mode 100644 index 000000000..a4e783148 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilter.java @@ -0,0 +1,39 @@ +package mouda.backend.notification.implement.filter; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.domain.Subscription; +import mouda.backend.notification.implement.subscription.SubscriptionFinder; + +@Component +@RequiredArgsConstructor +public class ChatRoomSubscriptionFilter implements SubscriptionFilter { + + private final SubscriptionFinder subscriptionFinder; + + @Override + public boolean support(NotificationType notificationType) { + return notificationType == NotificationType.NEW_CHAT || + notificationType.isConfirmedType(); + } + + @Override + public List filter(NotificationEvent notificationEvent) { + return notificationEvent.getRecipients().stream() + .filter(recipient -> { + // todo: 장소(시간) 확정 채팅은 알림이 가야함. + if (notificationEvent.getNotificationType().isConfirmedType()) { + return true; + } + Subscription subscription = subscriptionFinder.readSubscription(recipient.getMemberId()); + return subscription.isSubscribedChatRoom(notificationEvent.getDarakbangId(), notificationEvent.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 new file mode 100644 index 000000000..cf61d972d --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/MoimCreatedSubscriptionFilter.java @@ -0,0 +1,34 @@ +package mouda.backend.notification.implement.filter; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.domain.Subscription; +import mouda.backend.notification.implement.subscription.SubscriptionFinder; + +@Component +@RequiredArgsConstructor +public class MoimCreatedSubscriptionFilter implements SubscriptionFilter { + + private final SubscriptionFinder subscriptionFinder; + + @Override + public boolean support(NotificationType notificationType) { + return notificationType == NotificationType.MOIM_CREATED; + } + + @Override + public List filter(NotificationEvent notificationEvent) { + return notificationEvent.getRecipients().stream() + .filter(recipient -> { + Subscription subscription = subscriptionFinder.readSubscription(recipient.getMemberId()); + return subscription.isSubscribedMoimCreate(); + }) + .toList(); + } +} 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 new file mode 100644 index 000000000..f34d6d6fe --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/NonSubscriptionFilter.java @@ -0,0 +1,25 @@ +package mouda.backend.notification.implement.filter; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; + +@Component +public class NonSubscriptionFilter implements SubscriptionFilter { + + @Override + public boolean support(NotificationType notificationType) { + return notificationType != NotificationType.NEW_CHAT && + !notificationType.isConfirmedType() && + notificationType != NotificationType.MOIM_CREATED; + } + + @Override + public List filter(NotificationEvent notificationEvent) { + return notificationEvent.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 new file mode 100644 index 000000000..7e51a3ca7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilter.java @@ -0,0 +1,14 @@ +package mouda.backend.notification.implement.filter; + +import java.util.List; + +import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; + +public interface SubscriptionFilter { + + boolean support(NotificationType notificationType); + + List filter(NotificationEvent notificationEvent); +} 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 new file mode 100644 index 000000000..79fffe2bd --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilterRegistry.java @@ -0,0 +1,22 @@ +package mouda.backend.notification.implement.filter; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationType; + +@Component +@RequiredArgsConstructor +public class SubscriptionFilterRegistry { + + private final List subscriptionFilters; + + public SubscriptionFilter getFilter(NotificationType notificationType) { + return subscriptionFilters.stream() + .filter(subscriptionFilter -> subscriptionFilter.support(notificationType)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("no such filter")); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimCancelledNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimCancelledNotificationBuilder.java deleted file mode 100644 index b379d6eac..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimCancelledNotificationBuilder.java +++ /dev/null @@ -1,32 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIM_CANCELLED) -public class MoimCancelledNotificationBuilder extends MoimNotificationBuilder { - - public MoimCancelledNotificationBuilder( - MoudaNotificationRepository moudaNotificationRepository) { - super(moudaNotificationRepository); - } - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember sender) { - NotificationType notificationType = NotificationType.MOIM_CANCELLED; - MoudaNotification notification = MoudaNotification.builder() - .type(notificationType) - .body(notificationType.createMessage(moim.getTitle())) - .targetUrl(baseUrl + String.format(moimUrl, darakbangId, moim.getId())) - .build(); - - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimCreatedNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimCreatedNotificationBuilder.java deleted file mode 100644 index 0434a6220..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimCreatedNotificationBuilder.java +++ /dev/null @@ -1,30 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIM_CREATED) -public class MoimCreatedNotificationBuilder extends MoimNotificationBuilder { - - public MoimCreatedNotificationBuilder(MoudaNotificationRepository moudaNotificationRepository) { - super(moudaNotificationRepository); - } - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember sender) { - NotificationType notificationType = NotificationType.MOIM_CREATED; - MoudaNotification notification = MoudaNotification.builder() - .type(notificationType) - .body(notificationType.createMessage(moim.getTitle())) - .targetUrl(baseUrl + String.format(moimUrl, darakbangId, moim.getId())) - .build(); - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimModifiedNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimModifiedNotificationBuilder.java deleted file mode 100644 index 744a7aebc..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimModifiedNotificationBuilder.java +++ /dev/null @@ -1,32 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIM_MODIFIED) -public class MoimModifiedNotificationBuilder extends MoimNotificationBuilder { - - public MoimModifiedNotificationBuilder( - MoudaNotificationRepository moudaNotificationRepository) { - super(moudaNotificationRepository); - } - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember sender) { - NotificationType notificationType = NotificationType.MOIM_MODIFIED; - MoudaNotification notification = MoudaNotification.builder() - .type(notificationType) - .body(notificationType.createMessage(moim.getTitle())) - .targetUrl(baseUrl + String.format(moimUrl, darakbangId, moim.getId())) - .build(); - - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimNotificationBuilder.java deleted file mode 100644 index 786172d3e..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimNotificationBuilder.java +++ /dev/null @@ -1,18 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.beans.factory.annotation.Value; - -import lombok.RequiredArgsConstructor; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@RequiredArgsConstructor -public abstract class MoimNotificationBuilder implements NotificationBuilderStrategy { - - @Value("${url.base}") - protected String baseUrl; - - @Value("${url.moim}") - protected String moimUrl; - - protected final MoudaNotificationRepository moudaNotificationRepository; -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimPlaceConfirmedNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimPlaceConfirmedNotificationBuilder.java deleted file mode 100644 index cd8c52efd..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimPlaceConfirmedNotificationBuilder.java +++ /dev/null @@ -1,31 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIM_PLACE_CONFIRMED) -public class MoimPlaceConfirmedNotificationBuilder extends MoimNotificationBuilder { - - public MoimPlaceConfirmedNotificationBuilder(MoudaNotificationRepository moudaNotificationRepository) { - super(moudaNotificationRepository); - } - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember sender) { - NotificationType notificationType = NotificationType.MOIM_PLACE_CONFIRMED; - MoudaNotification notification = MoudaNotification.builder() - .type(notificationType) - .body(notificationType.createMessage(moim.getTitle())) - .targetUrl(baseUrl + String.format(moimUrl, darakbangId, moim.getId())) - .build(); - - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimTimeConfirmedNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimTimeConfirmedNotificationBuilder.java deleted file mode 100644 index 30638cbed..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimTimeConfirmedNotificationBuilder.java +++ /dev/null @@ -1,32 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIM_TIME_CONFIRMED) -public class MoimTimeConfirmedNotificationBuilder extends MoimNotificationBuilder { - - public MoimTimeConfirmedNotificationBuilder( - MoudaNotificationRepository moudaNotificationRepository) { - super(moudaNotificationRepository); - } - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember sender) { - NotificationType notificationType = NotificationType.MOIM_TIME_CONFIRMED; - MoudaNotification notification = MoudaNotification.builder() - .type(notificationType) - .body(notificationType.createMessage(moim.getTitle())) - .targetUrl(baseUrl + String.format(moimUrl, darakbangId, moim.getId())) - .build(); - - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimeeLeftNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimeeLeftNotificationBuilder.java deleted file mode 100644 index ff83786f1..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimeeLeftNotificationBuilder.java +++ /dev/null @@ -1,32 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIMEE_LEFT) -public class MoimeeLeftNotificationBuilder extends MoimNotificationBuilder { - - public MoimeeLeftNotificationBuilder( - MoudaNotificationRepository moudaNotificationRepository) { - super(moudaNotificationRepository); - } - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember sender) { - NotificationType notificationType = NotificationType.MOIMEE_LEFT; - MoudaNotification notification = MoudaNotification.builder() - .type(notificationType) - .body(notificationType.createMessage(sender.getNickname())) - .targetUrl(baseUrl + String.format(moimUrl, darakbangId, moim.getId())) - .build(); - - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimingCompletedNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimingCompletedNotificationBuilder.java deleted file mode 100644 index ab08d9e83..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimingCompletedNotificationBuilder.java +++ /dev/null @@ -1,31 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIMING_COMPLETED) -public class MoimingCompletedNotificationBuilder extends MoimNotificationBuilder { - public MoimingCompletedNotificationBuilder( - MoudaNotificationRepository moudaNotificationRepository) { - super(moudaNotificationRepository); - } - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember sender) { - NotificationType notificationType = NotificationType.MOIMING_COMPLETED; - MoudaNotification notification = MoudaNotification.builder() - .type(notificationType) - .body(notificationType.createMessage(moim.getTitle())) - .targetUrl(baseUrl + String.format(moimUrl, darakbangId, moim.getId())) - .build(); - - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimingReopenedNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimingReopenedNotificationBuilder.java deleted file mode 100644 index edce0f233..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/MoimingReopenedNotificationBuilder.java +++ /dev/null @@ -1,31 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOINING_REOPENED) -public class MoimingReopenedNotificationBuilder extends MoimNotificationBuilder { - - public MoimingReopenedNotificationBuilder(MoudaNotificationRepository moudaNotificationRepository) { - super(moudaNotificationRepository); - } - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember sender) { - NotificationType notificationType = NotificationType.MOINING_REOPENED; - MoudaNotification notification = MoudaNotification.builder() - .type(notificationType) - .body(notificationType.createMessage(moim.getTitle())) - .targetUrl(baseUrl + String.format(moimUrl, darakbangId, moim.getId())) - .build(); - - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewChatNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewChatNotificationBuilder.java deleted file mode 100644 index fb0faf82e..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewChatNotificationBuilder.java +++ /dev/null @@ -1,38 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.NEW_CHAT) -@RequiredArgsConstructor -public class NewChatNotificationBuilder implements NotificationBuilderStrategy { - - @Value("${url.base}") - private String baseUrl; - - @Value("${url.chatroom}") - private String chatroomUrl; - - private final MoudaNotificationRepository moudaNotificationRepository; - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember sender) { - NotificationType notificationType = NotificationType.NEW_CHAT; - MoudaNotification notification = MoudaNotification.builder() - .type(notificationType) - .body(notificationType.createMessage(sender.getNickname())) - .targetUrl(baseUrl + String.format(chatroomUrl, darakbangId, moim.getId())) - .build(); - - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewCommentNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewCommentNotificationBuilder.java deleted file mode 100644 index 1ef57d42e..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewCommentNotificationBuilder.java +++ /dev/null @@ -1,31 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.NEW_COMMENT) -public class NewCommentNotificationBuilder extends MoimNotificationBuilder { - - public NewCommentNotificationBuilder( - MoudaNotificationRepository moudaNotificationRepository) { - super(moudaNotificationRepository); - } - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember author) { - MoudaNotification notification = MoudaNotification.builder() - .type(NotificationType.NEW_COMMENT) - .body(NotificationType.NEW_COMMENT.createMessage(author.getNickname())) - .targetUrl(baseUrl + String.format(moimUrl, darakbangId, moim.getId())) - .build(); - - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewMoimeeJoinedNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewMoimeeJoinedNotificationBuilder.java deleted file mode 100644 index 809298503..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewMoimeeJoinedNotificationBuilder.java +++ /dev/null @@ -1,32 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.NEW_MOIMEE_JOINED) -public class NewMoimeeJoinedNotificationBuilder extends MoimNotificationBuilder { - - public NewMoimeeJoinedNotificationBuilder( - MoudaNotificationRepository moudaNotificationRepository) { - super(moudaNotificationRepository); - } - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember sender) { - NotificationType notificationType = NotificationType.NEW_MOIMEE_JOINED; - MoudaNotification notification = MoudaNotification.builder() - .type(notificationType) - .body(notificationType.createMessage(sender.getNickname())) - .targetUrl(baseUrl + String.format(moimUrl, darakbangId, moim.getId())) - .build(); - - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewReplyNotificationBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewReplyNotificationBuilder.java deleted file mode 100644 index c7b21dd54..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NewReplyNotificationBuilder.java +++ /dev/null @@ -1,30 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.NEW_REPLY) -public class NewReplyNotificationBuilder extends MoimNotificationBuilder { - - public NewReplyNotificationBuilder(MoudaNotificationRepository moudaNotificationRepository) { - super(moudaNotificationRepository); - } - - @Override - public MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember author) { - MoudaNotification notification = MoudaNotification.builder() - .type(NotificationType.NEW_REPLY) - .body(NotificationType.NEW_REPLY.createMessage(author.getNickname())) - .targetUrl(baseUrl + String.format(moimUrl, darakbangId, moim.getId())) - .build(); - - return moudaNotificationRepository.save(notification); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NotificationBuilderStrategy.java b/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NotificationBuilderStrategy.java deleted file mode 100644 index 68df74788..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/notificationbuilder/NotificationBuilderStrategy.java +++ /dev/null @@ -1,9 +0,0 @@ -package mouda.backend.notification.implement.notificationbuilder; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; - -public interface NotificationBuilderStrategy { - MoudaNotification buildNotification(Long darakbangId, Moim moim, DarakbangMember sender); -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimCanclledNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimCanclledNotificationRecipientResolver.java deleted file mode 100644 index de2127952..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimCanclledNotificationRecipientResolver.java +++ /dev/null @@ -1,21 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIM_CANCELLED) -public class MoimCanclledNotificationRecipientResolver extends MoimStatusChangedNotificationRecipientResolver { - - public MoimCanclledNotificationRecipientResolver( - DarakbangMemberRepository darakbangMemberRepository, - MemberNotificationRepository memberNotificationRepository, - ChamyoRepository chamyoRepository) { - super(darakbangMemberRepository, memberNotificationRepository, chamyoRepository); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimCompletedNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimCompletedNotificationRecipientResolver.java deleted file mode 100644 index ca2b8f1f6..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimCompletedNotificationRecipientResolver.java +++ /dev/null @@ -1,22 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIMING_COMPLETED) -public class MoimCompletedNotificationRecipientResolver extends MoimStatusChangedNotificationRecipientResolver { - - public MoimCompletedNotificationRecipientResolver( - DarakbangMemberRepository darakbangMemberRepository, - MemberNotificationRepository memberNotificationRepository, - ChamyoRepository chamyoRepository - ) { - super(darakbangMemberRepository, memberNotificationRepository, chamyoRepository); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimCreatedNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimCreatedNotificationRecipientResolver.java deleted file mode 100644 index 0f28cc7a3..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimCreatedNotificationRecipientResolver.java +++ /dev/null @@ -1,38 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import java.util.List; -import java.util.Objects; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIM_CREATED) -public class MoimCreatedNotificationRecipientResolver extends NoneChatRecipientResolverStrategy { - - public MoimCreatedNotificationRecipientResolver(DarakbangMemberRepository darakbangMemberRepository, - MemberNotificationRepository memberNotificationRepository, - ChamyoRepository chamyoRepository) { - super(darakbangMemberRepository, memberNotificationRepository, chamyoRepository); - } - - @Override - public List resolveRecipients(long darakbangId, MoudaNotification notification, Moim moim, - DarakbangMember sender) { - List recipientsIds = darakbangMemberRepository.findAllByDarakbangId(darakbangId).stream() - .filter(member -> !Objects.equals(member.getId(), sender.getId())) - .map(DarakbangMember::getMemberId) - .toList(); - - saveNotificationsForMembers(recipientsIds, darakbangId, notification); - return recipientsIds; - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimModifiedNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimModifiedNotificationRecipientResolver.java deleted file mode 100644 index 6947fdd5b..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimModifiedNotificationRecipientResolver.java +++ /dev/null @@ -1,21 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIM_MODIFIED) -public class MoimModifiedNotificationRecipientResolver extends MoimStatusChangedNotificationRecipientResolver { - - public MoimModifiedNotificationRecipientResolver( - DarakbangMemberRepository darakbangMemberRepository, - MemberNotificationRepository memberNotificationRepository, - ChamyoRepository chamyoRepository) { - super(darakbangMemberRepository, memberNotificationRepository, chamyoRepository); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimPlaceConfirmedNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimPlaceConfirmedNotificationRecipientResolver.java deleted file mode 100644 index 5d8c10106..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimPlaceConfirmedNotificationRecipientResolver.java +++ /dev/null @@ -1,38 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.domain.MoimRole; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIM_PLACE_CONFIRMED) -public class MoimPlaceConfirmedNotificationRecipientResolver extends NoneChatRecipientResolverStrategy { - public MoimPlaceConfirmedNotificationRecipientResolver( - DarakbangMemberRepository darakbangMemberRepository, - MemberNotificationRepository memberNotificationRepository, - ChamyoRepository chamyoRepository) { - super(darakbangMemberRepository, memberNotificationRepository, chamyoRepository); - } - - @Override - public List resolveRecipients(long darakbangId, MoudaNotification notification, Moim moim, - DarakbangMember sender) { - List recipientIds = chamyoRepository.findAllByMoimId(moim.getId()).stream() - .filter(chamyo -> chamyo.getMoimRole() != MoimRole.MOIMER) - .map(chamyo -> chamyo.getDarakbangMember().getMemberId()) - .toList(); - - saveNotificationsForMembers(recipientIds, darakbangId, notification); - return recipientIds; - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimStatusChangedNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimStatusChangedNotificationRecipientResolver.java deleted file mode 100644 index 2087b22b3..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimStatusChangedNotificationRecipientResolver.java +++ /dev/null @@ -1,34 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import java.util.List; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.domain.MoimRole; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -public abstract class MoimStatusChangedNotificationRecipientResolver extends NoneChatRecipientResolverStrategy { - - public MoimStatusChangedNotificationRecipientResolver( - DarakbangMemberRepository darakbangMemberRepository, - MemberNotificationRepository memberNotificationRepository, - ChamyoRepository chamyoRepository - ) { - super(darakbangMemberRepository, memberNotificationRepository, chamyoRepository); - } - - @Override - public List resolveRecipients(long darakbangId, MoudaNotification notification, Moim moim, - DarakbangMember sender) { - List recipientIds = chamyoRepository.findAllByMoimId(moim.getId()).stream() - .filter(chamyo -> chamyo.getMoimRole() != MoimRole.MOIMER) - .map(chamyo -> chamyo.getDarakbangMember().getMemberId()) - .toList(); - - saveNotificationsForMembers(recipientIds, darakbangId, notification); - return recipientIds; - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimTimeConfirmedNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimTimeConfirmedNotificationRecipientResolver.java deleted file mode 100644 index 78e04e895..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimTimeConfirmedNotificationRecipientResolver.java +++ /dev/null @@ -1,39 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.domain.MoimRole; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIM_TIME_CONFIRMED) -public class MoimTimeConfirmedNotificationRecipientResolver extends NoneChatRecipientResolverStrategy { - - public MoimTimeConfirmedNotificationRecipientResolver( - DarakbangMemberRepository darakbangMemberRepository, - MemberNotificationRepository memberNotificationRepository, - ChamyoRepository chamyoRepository) { - super(darakbangMemberRepository, memberNotificationRepository, chamyoRepository); - } - - @Override - public List resolveRecipients(long darakbangId, MoudaNotification notification, Moim moim, - DarakbangMember sender) { - List recipientIds = chamyoRepository.findAllByMoimId(moim.getId()).stream() - .filter(chamyo -> chamyo.getMoimRole() != MoimRole.MOIMER) - .map(chamyo -> chamyo.getDarakbangMember().getMemberId()) - .toList(); - - saveNotificationsForMembers(recipientIds, darakbangId, notification); - return recipientIds; - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimeeLeftNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimeeLeftNotificationRecipientResolver.java deleted file mode 100644 index 741a7a8e6..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimeeLeftNotificationRecipientResolver.java +++ /dev/null @@ -1,32 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOIMEE_LEFT) -public class MoimeeLeftNotificationRecipientResolver extends NoneChatRecipientResolverStrategy { - - public MoimeeLeftNotificationRecipientResolver( - DarakbangMemberRepository darakbangMemberRepository, - MemberNotificationRepository memberNotificationRepository, - ChamyoRepository chamyoRepository) { - super(darakbangMemberRepository, memberNotificationRepository, chamyoRepository); - } - - @Override - public List resolveRecipients(long darakbangId, MoudaNotification notification, Moim moim, - DarakbangMember sender) { - return List.of(chamyoRepository.findMoimerIdByMoimId(moim.getId())); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimingReopenedNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimingReopenedNotificationRecipientResolver.java deleted file mode 100644 index 209c0f6bf..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/MoimingReopenedNotificationRecipientResolver.java +++ /dev/null @@ -1,21 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.MOINING_REOPENED) -public class MoimingReopenedNotificationRecipientResolver extends MoimStatusChangedNotificationRecipientResolver { - - public MoimingReopenedNotificationRecipientResolver( - DarakbangMemberRepository darakbangMemberRepository, - MemberNotificationRepository memberNotificationRepository, - ChamyoRepository chamyoRepository) { - super(darakbangMemberRepository, memberNotificationRepository, chamyoRepository); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NewChatNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NewChatNotificationRecipientResolver.java deleted file mode 100644 index 16e4fffaa..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NewChatNotificationRecipientResolver.java +++ /dev/null @@ -1,34 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import java.util.List; -import java.util.Objects; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; - -@Component -@NotificationTypeProvider(NotificationType.NEW_CHAT) -public class NewChatNotificationRecipientResolver implements RecipientResolverStrategy { - - private final ChamyoRepository chamyoRepository; - - public NewChatNotificationRecipientResolver(ChamyoRepository chamyoRepository) { - this.chamyoRepository = chamyoRepository; - } - - @Override - public List resolveRecipients(long darakbangId, MoudaNotification notification, Moim moim, - DarakbangMember sender) { - - return chamyoRepository.findAllByMoimId(moim.getId()).stream() - .map(chamyo -> chamyo.getDarakbangMember().getMemberId()) - .filter(memberId -> !Objects.equals(memberId, sender.getMemberId())) - .toList(); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NewCommentNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NewCommentNotificationRecipientResolver.java deleted file mode 100644 index 47dcf4c4f..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NewCommentNotificationRecipientResolver.java +++ /dev/null @@ -1,31 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.NEW_COMMENT) -public class NewCommentNotificationRecipientResolver extends NoneChatRecipientResolverStrategy { - public NewCommentNotificationRecipientResolver( - DarakbangMemberRepository darakbangMemberRepository, - MemberNotificationRepository memberNotificationRepository, - ChamyoRepository chamyoRepository) { - super(darakbangMemberRepository, memberNotificationRepository, chamyoRepository); - } - - @Override - public List resolveRecipients(long darakbangId, MoudaNotification notification, Moim moim, - DarakbangMember sender) { - return List.of(chamyoRepository.findMoimerIdByMoimId(moim.getId())); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NewMoimeeJoinedNotificationRecipientResolver.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NewMoimeeJoinedNotificationRecipientResolver.java deleted file mode 100644 index 7dd8d4fc7..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NewMoimeeJoinedNotificationRecipientResolver.java +++ /dev/null @@ -1,40 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.domain.NotificationTypeProvider; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@Component -@NotificationTypeProvider(NotificationType.NEW_MOIMEE_JOINED) -public class NewMoimeeJoinedNotificationRecipientResolver extends NoneChatRecipientResolverStrategy { - - public NewMoimeeJoinedNotificationRecipientResolver( - DarakbangMemberRepository darakbangMemberRepository, - MemberNotificationRepository memberNotificationRepository, - ChamyoRepository chamyoRepository - ) { - super(darakbangMemberRepository, memberNotificationRepository, chamyoRepository); - } - - @Override - public List resolveRecipients(long darakbangId, MoudaNotification notification, Moim moim, - DarakbangMember sender) { - - List recipientIds = chamyoRepository.findAllByMoimId(moim.getId()).stream() - .map(c -> c.getDarakbangMember().getMemberId()) - .filter(memberId -> !memberId.equals(sender.getMemberId())) - .toList(); - - saveNotificationsForMembers(recipientIds, darakbangId, notification); - return recipientIds; - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NoneChatRecipientResolverStrategy.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NoneChatRecipientResolverStrategy.java deleted file mode 100644 index 512fcfcd9..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/NoneChatRecipientResolverStrategy.java +++ /dev/null @@ -1,29 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import java.util.List; - -import lombok.RequiredArgsConstructor; -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.moim.infrastructure.ChamyoRepository; -import mouda.backend.notification.domain.MemberNotification; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; - -@RequiredArgsConstructor -public abstract class NoneChatRecipientResolverStrategy implements RecipientResolverStrategy { - - protected final DarakbangMemberRepository darakbangMemberRepository; - protected final MemberNotificationRepository memberNotificationRepository; - protected final ChamyoRepository chamyoRepository; - - public void saveNotificationsForMembers(List recipientsIds, long darakbangId, - MoudaNotification notification) { - memberNotificationRepository.saveAll(recipientsIds.stream() - .map(memberId -> MemberNotification.builder() - .memberId(memberId) - .darakbangId(darakbangId) - .moudaNotification(notification) - .build()) - .toList()); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/RecipientResolverStrategy.java b/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/RecipientResolverStrategy.java deleted file mode 100644 index ff94a916d..000000000 --- a/backend/src/main/java/mouda/backend/notification/implement/recipientresolver/RecipientResolverStrategy.java +++ /dev/null @@ -1,13 +0,0 @@ -package mouda.backend.notification.implement.recipientresolver; - -import java.util.List; - -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.moim.domain.Moim; -import mouda.backend.notification.domain.MoudaNotification; - -public interface RecipientResolverStrategy { - - List resolveRecipients(long darakbangId, MoudaNotification notification, Moim moim, - DarakbangMember sender); -} diff --git a/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionFinder.java b/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionFinder.java new file mode 100644 index 000000000..ee6b8f941 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionFinder.java @@ -0,0 +1,54 @@ +package mouda.backend.notification.implement.subscription; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.domain.Subscription; +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; +import mouda.backend.notification.infrastructure.entity.UnsubscribedChatRooms; +import mouda.backend.notification.infrastructure.repository.SubscriptionRepository; + +@Component +@RequiredArgsConstructor +public class SubscriptionFinder { + + private final SubscriptionRepository subscriptionRepository; + + public Subscription readSubscription(Member member) { + return readSubscription(member.getId()); + } + + public Subscription readSubscription(long memberId) { + Optional subscriptionEntityOptional = subscriptionRepository.findByMemberId(memberId); + + if (subscriptionEntityOptional.isPresent()) { + return convertToSubscription(subscriptionEntityOptional.get()); + } + + SubscriptionEntity createdSubscriptionEntity = subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(memberId) + .unsubscribedChats(new ArrayList<>()) + .build()); + return convertToSubscription(createdSubscriptionEntity); + } + + private Subscription convertToSubscription(SubscriptionEntity subscriptionEntity) { + Map> unsubscribedChatRooms = subscriptionEntity.getUnsubscribedChats().stream() + .collect(Collectors.toConcurrentMap( + UnsubscribedChatRooms::getDarakbangId, + UnsubscribedChatRooms::getChatRoomIds + )); + + return Subscription.builder() + .isSubscribedMoimCreate(subscriptionEntity.isSubscribedMoimCreate()) + .unsubscribedChatRooms(unsubscribedChatRooms) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionWriter.java b/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionWriter.java new file mode 100644 index 000000000..d397e2126 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionWriter.java @@ -0,0 +1,50 @@ +package mouda.backend.notification.implement.subscription; + +import java.util.ArrayList; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; +import mouda.backend.notification.infrastructure.repository.SubscriptionRepository; + +@Component +@RequiredArgsConstructor +public class SubscriptionWriter { + + private final SubscriptionRepository subscriptionRepository; + + public void changeMoimSubscription(Member member) { + subscriptionRepository.findByMemberId(member.getId()) + .ifPresentOrElse( + this::changeMoimSubscription, + () -> changeMoimSubscription(create(member)) + ); + } + + public void changeChatRoomSubscription(Member member, long darakbangId, long chatRoomId) { + subscriptionRepository.findByMemberId(member.getId()) + .ifPresentOrElse( + subscription -> changeChatRoomSubscription(subscription, darakbangId, chatRoomId), + () -> changeChatRoomSubscription(create(member), darakbangId, chatRoomId) + ); + } + + private void changeMoimSubscription(SubscriptionEntity subscriptionEntity) { + subscriptionEntity.changeMoimCreateSubscription(); + subscriptionRepository.save(subscriptionEntity); + } + + private void changeChatRoomSubscription(SubscriptionEntity subscriptionEntity, long darakbangId, long chatRoomId) { + subscriptionEntity.changeChatRoomSubscription(darakbangId, chatRoomId); + subscriptionRepository.save(subscriptionEntity); + } + + private SubscriptionEntity create(Member member) { + return subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(member.getId()) + .unsubscribedChats(new ArrayList<>()) + .build()); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/FcmTokenRepository.java b/backend/src/main/java/mouda/backend/notification/infrastructure/FcmTokenRepository.java deleted file mode 100644 index 8c956cdd7..000000000 --- a/backend/src/main/java/mouda/backend/notification/infrastructure/FcmTokenRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package mouda.backend.notification.infrastructure; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import mouda.backend.notification.domain.FcmToken; - -public interface FcmTokenRepository extends JpaRepository { - - @Query("SELECT f.token FROM FcmToken f WHERE f.memberId = :memberId") - List findAllTokenByMemberId(@Param("memberId") Long memberId); - - @Query("SELECT f.token FROM FcmToken f WHERE f.memberId IN :memberIds") - List findAllTokenByMemberIds(@Param("memberIds") List memberIds); - - @Query("SELECT f.memberId FROM FcmToken f") - List findAllMemberId(); - - void deleteByToken(String token); - - Optional findByToken(String token); -} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/MemberNotificationRepository.java b/backend/src/main/java/mouda/backend/notification/infrastructure/MemberNotificationRepository.java deleted file mode 100644 index 02fc2cf73..000000000 --- a/backend/src/main/java/mouda/backend/notification/infrastructure/MemberNotificationRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package mouda.backend.notification.infrastructure; - -import java.util.List; - -import org.springframework.data.jpa.repository.JpaRepository; - -import mouda.backend.notification.domain.MemberNotification; - -public interface MemberNotificationRepository extends JpaRepository { - - List findAllByMemberIdAndDarakbangIdOrderByIdDesc(Long memberId, Long darakbangId); -} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/MoudaNotificationRepository.java b/backend/src/main/java/mouda/backend/notification/infrastructure/MoudaNotificationRepository.java deleted file mode 100644 index f3a84fdcf..000000000 --- a/backend/src/main/java/mouda/backend/notification/infrastructure/MoudaNotificationRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package mouda.backend.notification.infrastructure; - -import org.springframework.data.jpa.repository.JpaRepository; - -import mouda.backend.notification.domain.MoudaNotification; - -public interface MoudaNotificationRepository extends JpaRepository { -} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/entity/FcmTokenEntity.java b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/FcmTokenEntity.java new file mode 100644 index 000000000..6f75065e9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/FcmTokenEntity.java @@ -0,0 +1,62 @@ +package mouda.backend.notification.infrastructure.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "fcm_token") +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +@Getter +public class FcmTokenEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long memberId; + + private String token; + + private boolean isActive; + + private LocalDateTime lastUpdated; + + @Builder + public FcmTokenEntity(Long memberId, String token) { + this.memberId = memberId; + this.token = token; + this.isActive = true; + this.lastUpdated = LocalDateTime.now(); + } + + public void refresh() { + this.lastUpdated = LocalDateTime.now(); + } + + public void activate() { + this.isActive = true; + } + + public void deactivate() { + this.isActive = false; + } + + public boolean isActive(LocalDateTime threshold) { + return isActive && lastUpdated.isBefore(LocalDateTime.now().minusMonths(1L)); + } + + public boolean isInactive() { + return !isActive(); + } + + public boolean isExpired() { + return isInactive() && lastUpdated.isBefore(LocalDateTime.now().minusDays(270L)); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/entity/MemberNotificationEntity.java b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/MemberNotificationEntity.java new file mode 100644 index 000000000..88f0d5f39 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/MemberNotificationEntity.java @@ -0,0 +1,46 @@ +package mouda.backend.notification.infrastructure.entity; + +import java.time.LocalDateTime; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "member_notification") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemberNotificationEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private long darakbangMemberId; + + private String title; + + private String body; + + private String type; + + private String targetUrl; + + private LocalDateTime createdAt; + + @Builder + public MemberNotificationEntity(long darakbangMemberId, String title, String body, String type, String targeturl, LocalDateTime createdAt) { + this.darakbangMemberId = darakbangMemberId; + this.title = title; + this.body = body; + this.type = type; + this.targetUrl = targeturl; + this.createdAt = createdAt; + } +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/entity/SubscriptionEntity.java b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/SubscriptionEntity.java new file mode 100644 index 000000000..0cd9ef897 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/SubscriptionEntity.java @@ -0,0 +1,61 @@ +package mouda.backend.notification.infrastructure.entity; + +import java.util.List; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "subscription") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SubscriptionEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private long memberId; + + private boolean moimCreate; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "unsubscribed_chats", columnDefinition = "json") + private List unsubscribedChats; + + @Builder + public SubscriptionEntity(long memberId, List unsubscribedChats) { + this.memberId = memberId; + this.unsubscribedChats = unsubscribedChats; + this.moimCreate = true; + } + + public boolean isSubscribedMoimCreate() { + return moimCreate; + } + + public void changeMoimCreateSubscription() { + this.moimCreate = !this.moimCreate; + } + + public void changeChatRoomSubscription(long darakbangId, long chatRoomId) { + for (UnsubscribedChatRooms unsubscribedChatRoom : unsubscribedChats) { + if (unsubscribedChatRoom.getDarakbangId() == darakbangId) { + unsubscribedChatRoom.changeChatRoomSubscription(chatRoomId); + return; + } + } + unsubscribedChats.add(UnsubscribedChatRooms.create(darakbangId, chatRoomId)); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/entity/UnsubscribedChatRooms.java b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/UnsubscribedChatRooms.java new file mode 100644 index 000000000..bfe812ad9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/UnsubscribedChatRooms.java @@ -0,0 +1,31 @@ +package mouda.backend.notification.infrastructure.entity; + +import java.util.ArrayList; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class UnsubscribedChatRooms { + + private long darakbangId; + private List chatRoomIds; + + public static UnsubscribedChatRooms create(long darakbangId, long chatRoomId) { + List chatRoomIds = new ArrayList<>(); + chatRoomIds.add(chatRoomId); + return new UnsubscribedChatRooms(darakbangId, chatRoomIds); + } + + public void changeChatRoomSubscription(long chatRoomId) { + if (chatRoomIds.contains(chatRoomId)) { + chatRoomIds.remove(chatRoomId); + return; + } + chatRoomIds.add(chatRoomId); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/repository/FcmTokenRepository.java b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/FcmTokenRepository.java new file mode 100644 index 000000000..9847c6422 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/FcmTokenRepository.java @@ -0,0 +1,17 @@ +package mouda.backend.notification.infrastructure.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.notification.infrastructure.entity.FcmTokenEntity; + +public interface FcmTokenRepository extends JpaRepository { + + List findAllByMemberId(long memberId); + + Optional findByToken(String token); + + void deleteAllByTokenIn(List unregisteredTokens); +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/repository/MemberNotificationRepository.java b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/MemberNotificationRepository.java new file mode 100644 index 000000000..d3a771689 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/MemberNotificationRepository.java @@ -0,0 +1,12 @@ +package mouda.backend.notification.infrastructure.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.notification.infrastructure.entity.MemberNotificationEntity; + +public interface MemberNotificationRepository extends JpaRepository { + + List findAllByDarakbangMemberId(Long darakbangMemberId); +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/repository/SubscriptionRepository.java b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/SubscriptionRepository.java new file mode 100644 index 000000000..1ac089fcc --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/SubscriptionRepository.java @@ -0,0 +1,12 @@ +package mouda.backend.notification.infrastructure.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; + +public interface SubscriptionRepository extends JpaRepository { + + Optional findByMemberId(long memberId); +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationController.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationController.java new file mode 100644 index 000000000..81acaa582 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationController.java @@ -0,0 +1,32 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.notification.business.MemberNotificationService; +import mouda.backend.notification.presentation.response.MemberNotificationFindAllResponse; + +@RestController +@RequiredArgsConstructor +public class MemberNotificationController implements MemberNotificationSwagger { + + private final MemberNotificationService memberNotificationService; + + // todo: 이제 darakbangId로 조회할 필요가 없음. DarakbangMember의 id로 조회. -> API 명세 수정 필요 + @GetMapping("/v1/darakbang/{darakbangId}/notification/mine") + @Override + public ResponseEntity> findAllMemberNotification( + @LoginDarakbangMember DarakbangMember darakbangMember, + @PathVariable Long darakbangId + ) { + MemberNotificationFindAllResponse response = memberNotificationService.findAllMemberNotification( + darakbangMember); + return ResponseEntity.ok(new RestResponse<>(response)); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationSwagger.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationSwagger.java new file mode 100644 index 000000000..7d6b7d51f --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationSwagger.java @@ -0,0 +1,24 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.notification.presentation.response.MemberNotificationFindAllResponse; + +public interface MemberNotificationSwagger { + + @Operation(summary = "회원의 개별 알림 조회", description = "알림 센터에 표시되는 회원의 개별 알림을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공!"), + }) + ResponseEntity> findAllMemberNotification( + @LoginDarakbangMember DarakbangMember darakbangMember, + @PathVariable Long darakbangId + ); +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationController.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationController.java deleted file mode 100644 index 7fb5732cb..000000000 --- a/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationController.java +++ /dev/null @@ -1,49 +0,0 @@ -package mouda.backend.notification.presentation.controller; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import mouda.backend.common.config.argumentresolver.LoginMember; -import mouda.backend.common.response.RestResponse; -import mouda.backend.member.domain.Member; -import mouda.backend.notification.business.NotificationService; -import mouda.backend.notification.presentation.controller.swagger.NotificationSwagger; -import mouda.backend.notification.presentation.request.FcmTokenSaveRequest; -import mouda.backend.notification.presentation.response.NotificationFindAllResponses; - -@RestController -@RequestMapping -@RequiredArgsConstructor -public class NotificationController implements NotificationSwagger { - - private final NotificationService notificationService; - - @Override - @PostMapping("/v1/notification/register") - public ResponseEntity registerFcmToken( - @LoginMember Member member, - @Valid @RequestBody FcmTokenSaveRequest fcmTokenSaveRequest - ) { - notificationService.registerFcmToken(member.getId(), fcmTokenSaveRequest); - - return ResponseEntity.ok().build(); - } - - @Override - @GetMapping("/v1/darakbang/{darakbangId}/notification/mine") - public ResponseEntity> findAllMyNotification( - @LoginMember Member member, - @PathVariable Long darakbangId - ) { - NotificationFindAllResponses responses = notificationService.findAllMyNotifications(member, darakbangId); - - return ResponseEntity.ok().body(new RestResponse<>(responses)); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenController.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenController.java new file mode 100644 index 000000000..656fb5003 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenController.java @@ -0,0 +1,29 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.business.FcmTokenService; +import mouda.backend.notification.presentation.request.FcmTokenRequest; + +@RestController +@RequiredArgsConstructor +public class NotificationTokenController implements NotificationTokenSwagger { + + private final FcmTokenService fcmTokenService; + + @PostMapping("/v1/notification/register") + public ResponseEntity saveOrRefreshToken( + @LoginMember Member member, + @RequestBody FcmTokenRequest tokenRequest + ) { + fcmTokenService.saveOrRefreshToken(member, tokenRequest); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenSwagger.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenSwagger.java new file mode 100644 index 000000000..398670c0d --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenSwagger.java @@ -0,0 +1,23 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.presentation.request.FcmTokenRequest; + +public interface NotificationTokenSwagger { + + @Operation(summary = "FCM 토큰 등록", description = "알림 허용시 FCM 토큰을 등록하고, 이미 등록된 토큰이면 갱신한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "등록(갱신) 성공!"), + }) + ResponseEntity saveOrRefreshToken( + @LoginMember Member member, + @RequestBody FcmTokenRequest tokenRequest + ); +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionController.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionController.java new file mode 100644 index 000000000..d4772890e --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionController.java @@ -0,0 +1,68 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.business.SubscriptionService; +import mouda.backend.notification.presentation.request.ChatSubscriptionRequest; +import mouda.backend.notification.presentation.response.SubscriptionResponse; + +@RestController +@RequiredArgsConstructor +public class SubscriptionController implements SubscriptionSwagger { + + private final SubscriptionService subscriptionService; + + @GetMapping("/v1/subscription/moim") + @Override + public ResponseEntity> readMoimCreateSubscription( + @LoginMember Member member + ) { + SubscriptionResponse response = subscriptionService.readMoimCreateSubscription(member); + + return ResponseEntity.ok(new RestResponse<>(response)); + } + + @PostMapping("/v1/subscription/moim") + @Override + public ResponseEntity changeMoimCreateSubscription( + @LoginMember Member member + ) { + subscriptionService.changeMoimCreateSubscription(member); + + return ResponseEntity.ok().build(); + } + + @GetMapping("/v1/subscription/chat") + @Override + public ResponseEntity> readSpecificChatRoomSubscription( + @LoginMember Member member, + @RequestParam("darakbangId") Long darakbangId, + @RequestParam("chatRoomId") Long chatRoomId + ) { + SubscriptionResponse response = subscriptionService.readChatRoomSubscription(member, + darakbangId, chatRoomId); + + return ResponseEntity.ok(new RestResponse<>(response)); + } + + @PostMapping("/v1/subscription/chat") + @Override + public ResponseEntity changeChatRoomSubscription( + @LoginMember Member member, + @Valid @RequestBody ChatSubscriptionRequest chatSubscriptionRequest + ) { + subscriptionService.changeChatRoomSubscription(member, chatSubscriptionRequest); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionSwagger.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionSwagger.java new file mode 100644 index 000000000..947d6af9a --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionSwagger.java @@ -0,0 +1,53 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.presentation.request.ChatSubscriptionRequest; +import mouda.backend.notification.presentation.response.SubscriptionResponse; + +public interface SubscriptionSwagger { + + @Operation(summary = "모임 생성시 알림 여부 조회", description = "모임 생성에 대한 알림 허용 여부를 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공"), + }) + ResponseEntity> readMoimCreateSubscription( + @LoginMember Member member + ); + + @Operation(summary = "모임 생성 알림 여부 수정", description = "알림 허용상태면 비허용으로, 비허용 상태면 허용 상태로 변경한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "변경 성공"), + }) + ResponseEntity changeMoimCreateSubscription( + @LoginMember Member member + ); + + @Operation(summary = "특정 채팅방에 대한 알림 여부 조회", description = "특정 채팅방에 대한 알림 허용 여부를 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공"), + }) + ResponseEntity> readSpecificChatRoomSubscription( + @LoginMember Member member, + @RequestParam("darakbangId") Long darakbangId, + @RequestParam("chatRoomId") Long chatRoomId + ); + + @Operation(summary = "특정 채팅방에 대한 알림 여부 수정", description = "알림 허용상태면 비허용으로, 비허용 상태면 허용 상태로 변경한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "변경 성공"), + }) + ResponseEntity changeChatRoomSubscription( + @LoginMember Member member, + @Valid @RequestBody ChatSubscriptionRequest chatSubscriptionRequest + ); +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/swagger/NotificationSwagger.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/swagger/NotificationSwagger.java deleted file mode 100644 index c2e487b80..000000000 --- a/backend/src/main/java/mouda/backend/notification/presentation/controller/swagger/NotificationSwagger.java +++ /dev/null @@ -1,36 +0,0 @@ -package mouda.backend.notification.presentation.controller.swagger; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import jakarta.validation.Valid; -import mouda.backend.common.config.argumentresolver.LoginMember; -import mouda.backend.common.response.RestResponse; -import mouda.backend.member.domain.Member; -import mouda.backend.notification.presentation.request.FcmTokenSaveRequest; -import mouda.backend.notification.presentation.response.NotificationFindAllResponses; - -public interface NotificationSwagger { - - @Operation(summary = "FCM 토큰을 저장합니다.", description = "알림 허용시 FCM 토큰을 저장합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "FCM 토큰 저장 성공!") - }) - ResponseEntity registerFcmToken( - @LoginMember Member member, - @Valid @RequestBody FcmTokenSaveRequest fcmTokenSaveRequest - ); - - @Operation(summary = "모든 알림 조회", description = "회원의 모든 알림을 조회합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "알림 조회 성공!") - }) - ResponseEntity> findAllMyNotification( - @LoginMember Member member, - @PathVariable Long darakbangId - ); -} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/request/ChatSubscriptionRequest.java b/backend/src/main/java/mouda/backend/notification/presentation/request/ChatSubscriptionRequest.java new file mode 100644 index 000000000..934f1c06c --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/request/ChatSubscriptionRequest.java @@ -0,0 +1,12 @@ +package mouda.backend.notification.presentation.request; + +import jakarta.validation.constraints.NotNull; + +public record ChatSubscriptionRequest( + @NotNull + Long darakbangId, + + @NotNull + Long chatRoomId +) { +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/request/FcmTokenSaveRequest.java b/backend/src/main/java/mouda/backend/notification/presentation/request/FcmTokenRequest.java similarity index 69% rename from backend/src/main/java/mouda/backend/notification/presentation/request/FcmTokenSaveRequest.java rename to backend/src/main/java/mouda/backend/notification/presentation/request/FcmTokenRequest.java index e250cd27c..d7d135e98 100644 --- a/backend/src/main/java/mouda/backend/notification/presentation/request/FcmTokenSaveRequest.java +++ b/backend/src/main/java/mouda/backend/notification/presentation/request/FcmTokenRequest.java @@ -1,6 +1,6 @@ package mouda.backend.notification.presentation.request; -public record FcmTokenSaveRequest( +public record FcmTokenRequest( String token ) { } diff --git a/backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindAllResponse.java b/backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindAllResponse.java new file mode 100644 index 000000000..faa4f67b1 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindAllResponse.java @@ -0,0 +1,18 @@ +package mouda.backend.notification.presentation.response; + +import java.util.List; + +import mouda.backend.notification.domain.MemberNotification; + +public record MemberNotificationFindAllResponse( + List notifications +) { + + public static MemberNotificationFindAllResponse from(List notifications) { + List responses = notifications.stream() + .map(MemberNotificationFindResponse::from) + .toList(); + + return new MemberNotificationFindAllResponse(responses); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/response/NotificationFindAllResponse.java b/backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindResponse.java similarity index 60% rename from backend/src/main/java/mouda/backend/notification/presentation/response/NotificationFindAllResponse.java rename to backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindResponse.java index 302ec32f5..e0de0ab8f 100644 --- a/backend/src/main/java/mouda/backend/notification/presentation/response/NotificationFindAllResponse.java +++ b/backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindResponse.java @@ -3,26 +3,25 @@ import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; -import lombok.Builder; -import mouda.backend.notification.domain.MoudaNotification; +import mouda.backend.notification.domain.MemberNotification; -@Builder -public record NotificationFindAllResponse( +public record MemberNotificationFindResponse( String message, String createdAt, String type, String redirectUrl ) { - public static NotificationFindAllResponse from(MoudaNotification moudaNotification) { - return NotificationFindAllResponse.builder() - .type(moudaNotification.getType().name()) - .message(moudaNotification.getBody()) - .createdAt(parseTime(moudaNotification.getCreatedAt())) - .redirectUrl(moudaNotification.getTargetUrl()) - .build(); + public static MemberNotificationFindResponse from(MemberNotification notification) { + return new MemberNotificationFindResponse( + notification.getMessage(), + parseTime(notification.getCreatedAt()), + notification.getType(), + notification.getRedirectUrl() + ); } + private static String parseTime(LocalDateTime notificationCreatedAt) { LocalDateTime now = LocalDateTime.now(); long minutes = notificationCreatedAt.until(now, ChronoUnit.MINUTES); diff --git a/backend/src/main/java/mouda/backend/notification/presentation/response/NotificationFindAllResponses.java b/backend/src/main/java/mouda/backend/notification/presentation/response/NotificationFindAllResponses.java deleted file mode 100644 index 482d96b76..000000000 --- a/backend/src/main/java/mouda/backend/notification/presentation/response/NotificationFindAllResponses.java +++ /dev/null @@ -1,20 +0,0 @@ -package mouda.backend.notification.presentation.response; - -import java.util.List; - -import mouda.backend.notification.domain.MemberNotification; - -public record NotificationFindAllResponses( - List notifications -) { - - public static NotificationFindAllResponses toResponse(List memberNotifications) { - return new NotificationFindAllResponses( - memberNotifications - .stream() - .map(MemberNotification::getMoudaNotification) - .map(NotificationFindAllResponse::from) - .toList() - ); - } -} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/response/SubscriptionResponse.java b/backend/src/main/java/mouda/backend/notification/presentation/response/SubscriptionResponse.java new file mode 100644 index 000000000..80ab8b2f5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/response/SubscriptionResponse.java @@ -0,0 +1,9 @@ +package mouda.backend.notification.presentation.response; + +import lombok.Builder; + +@Builder +public record SubscriptionResponse( + boolean isSubscribed +) { +} diff --git a/backend/src/test/java/mouda/backend/common/global/IgnoreNotificationTest.java b/backend/src/test/java/mouda/backend/common/global/IgnoreNotificationTest.java index ed44ac415..bcff42696 100644 --- a/backend/src/test/java/mouda/backend/common/global/IgnoreNotificationTest.java +++ b/backend/src/test/java/mouda/backend/common/global/IgnoreNotificationTest.java @@ -1,23 +1,23 @@ -package mouda.backend.common.global; - -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -import org.junit.jupiter.api.BeforeEach; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; - -import mouda.backend.notification.business.NotificationService; - -@SpringBootTest -public class IgnoreNotificationTest { - - @MockBean - private NotificationService notificationService; - - @BeforeEach - void setUp() { - doNothing().when(notificationService).notifyToMember(any(), anyLong(), any(), any(), anyLong()); - doNothing().when(notificationService).notifyToMembers(any(), anyLong(), any(), any()); - } -} +// package mouda.backend.common.global; +// +// import static org.mockito.ArgumentMatchers.*; +// import static org.mockito.Mockito.*; +// +// import org.junit.jupiter.api.BeforeEach; +// import org.springframework.boot.test.context.SpringBootTest; +// import org.springframework.boot.test.mock.mockito.MockBean; +// +// import mouda.backend.notification.business.NotificationService; +// +// @SpringBootTest +// public class IgnoreNotificationTest { +// +// @MockBean +// private NotificationService notificationService; +// +// @BeforeEach +// void setUp() { +// doNothing().when(notificationService).notifyToMember(any(), anyLong(), any(), any(), anyLong()); +// doNothing().when(notificationService).notifyToMembers(any(), anyLong(), any(), any()); +// } +// } 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 new file mode 100644 index 000000000..ace3c2007 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinderTest.java @@ -0,0 +1,52 @@ +package mouda.backend.moim.implement.finder; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +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 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; +import mouda.backend.notification.domain.Recipient; + +@SpringBootTest +class ChamyoRecipientFinderTest extends DarakbangSetUp { + + @Autowired + ChamyoRecipientFinder chamyoRecipientFinder; + + @Autowired + MoimRepository moimRepository; + + @Autowired + ChamyoRepository chamyoRepository; + + @DisplayName("모임 참여/참여 취소는 참여자/참여취소자를 제외한 모든 인원에게 전송한다.") + @Test + void getChamyoNotificationRecipients() { + // given + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + Chamyo chamyoWithMoimerAnna = Chamyo.builder() + .moim(savedMoim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build(); + chamyoRepository.save(chamyoWithMoimerAnna); + + // when + List recipients = chamyoRecipientFinder.getChamyoNotificationRecipients(savedMoim.getId(), darakbangHogee); + + //then + assertThat(recipients).hasSize(1); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/ChatRecipientFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/ChatRecipientFinderTest.java new file mode 100644 index 000000000..947502003 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/ChatRecipientFinderTest.java @@ -0,0 +1,46 @@ +package mouda.backend.moim.implement.finder; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +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 mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.writer.ChamyoWriter; +import mouda.backend.moim.implement.writer.MoimWriter; +import mouda.backend.notification.domain.Recipient; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ChatRecipientFinderTest extends DarakbangSetUp { + + @Autowired + private ChatRecipientFinder chatRecipientFinder; + + @Autowired + private ChamyoWriter chamyoWriter; + + @Autowired + private MoimWriter moimWriter; + + @DisplayName("채팅 알림은 메시지를 보낸 사람을 제외한 모든 참여자를 대상으로 한다.") + @Test + void getChatNotificationRecipients() { + // given + Moim moim = moimWriter.save(MoimFixture.getCoffeeMoim(darakbang.getId()), darakbangAnna); + chamyoWriter.saveAsMoimee(moim, darakbangHogee); + + // when + List result = chatRecipientFinder.getChatNotificationRecipients(moim.getId(), + darakbangAnna); + + // then + assertThat(result).hasSize(1); + assertThat(result).extracting("memberId").containsExactly(darakbangHogee.getMemberId()); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/CommentFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/CommentFinderTest.java index 4e09a0ddd..d82af4ba9 100644 --- a/backend/src/test/java/mouda/backend/moim/implement/finder/CommentFinderTest.java +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/CommentFinderTest.java @@ -47,18 +47,4 @@ void readAllParentComments() { assertThat(actual.getChildren()).hasSize(1) .extracting(Comment::getId).containsOnly(childComment.getId()); } - - @DisplayName("부모 댓글의 ID로 회원 ID를 조회한다.") - @Test - void readMemberIdByParentId() { - Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); - Comment parentComment = commentRepository.save( - CommentFixture.getCommentWithAnnaAtSoccerMoim(darakbangHogee, moim)); - Comment childComment = commentRepository.save( - CommentFixture.getChildCommentWithAnnaAtSoccerMoim(darakbangAnna, moim)); - - Long memberId = commentFinder.readMemberIdByParentId(childComment.getParentId()); - - assertThat(memberId).isEqualTo(darakbangHogee.getId()); - } -} \ No newline at end of file +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/CommentRecipientFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/CommentRecipientFinderTest.java new file mode 100644 index 000000000..6b3e0e111 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/CommentRecipientFinderTest.java @@ -0,0 +1,177 @@ +package mouda.backend.moim.implement.finder; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.CommentRecipient; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.writer.MoimWriter; +import mouda.backend.moim.infrastructure.CommentRepository; +import mouda.backend.notification.domain.NotificationType; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class CommentRecipientFinderTest extends DarakbangSetUp { + + @Autowired + private CommentRecipientFinder commentRecipientFinder; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private MoimWriter moimWriter; + + private Moim moim; + + @BeforeEach + void init() { + moim = moimWriter.save(MoimFixture.getCoffeeMoim(darakbang.getId()), darakbangAnna); + } + + @DisplayName("댓글인 경우") + @Nested + class CommentTest { + + @DisplayName("댓글 작성자가 방장이면 아무에게도 알림을 보내지 않는다.") + @Test + void getNewCommentNotificationRecipients_WhenAuthorMoimer() { + Comment comment = createComment(darakbangAnna, null); + + assertThat(commentRecipientFinder.getAllRecipient(comment)).isEmpty(); + } + + @DisplayName("댓글 작성자가 방장이 아니면 방장에게 알림을 보낸다.") + @Test + void getNewCommentNotificationRecipients_WhenAuthorNotMoimer() { + Comment comment = createComment(darakbangHogee, null); + + List results = commentRecipientFinder.getAllRecipient(comment); + assertThat(results).hasSize(1); + + CommentRecipient recipient = results.get(0); + assertThat(recipient.getRecipients()).extracting("memberId") + .containsExactly(darakbangAnna.getMemberId()); + } + } + + @DisplayName("답글인 경우") + @Nested + class ReplyTest { + + @DisplayName("방장의 댓글에 답글을 남기는 경우") + @Nested + class ReplyToMoimer { + + private Comment parentComment; + + @BeforeEach + void setUp() { + parentComment = createComment(darakbangAnna, null); + } + + @DisplayName("방장 자신이 답글을 남기는 경우 아무에게도 알림을 보내지 않는다.") + @Test + void getReplyNotificationRecipients_WhenAuthorMoimer() { + Comment childComment = createComment(darakbangAnna, parentComment.getId()); + + assertThat(commentRecipientFinder.getAllRecipient(childComment)).isEmpty(); + } + + @DisplayName("방장 이외의 사람이 답글을 남기는 경우 방장에게 '답글' 알림을 보낸다.") + @Test + void getReplyNotificationRecipients_WhenAuthorNotMoimer() { + Comment childComment = createComment(darakbangHogee, parentComment.getId()); + + List results = commentRecipientFinder.getAllRecipient(childComment); + assertThat(results).hasSize(1); + + CommentRecipient recipient = results.get(0); + assertThat(recipient.getNotificationType()).isEqualTo(NotificationType.NEW_REPLY); + assertThat(recipient.getRecipients()).extracting("memberId") + .containsExactly(darakbangAnna.getMemberId()); + } + } + + @DisplayName("방장이 아닌 사람의 댓글에 답글을 남기는 경우") + @Nested + class ReplyToNotMoimer { + + private Comment parentComment; + + @BeforeEach + void setUp() { + parentComment = createComment(darakbangHogee, null); + } + + @DisplayName("방장이 답글을 남기면 댓글 작성자에게만 알림을 보낸다.") + @Test + void getReplyNotificationRecipients_WhenAuthorMoimer() { + Comment childComment = createComment(darakbangAnna, parentComment.getId()); + + List results = commentRecipientFinder.getAllRecipient(childComment); + assertThat(results).hasSize(1); + + CommentRecipient recipient = results.get(0); + assertThat(recipient.getNotificationType()).isEqualTo(NotificationType.NEW_REPLY); + assertThat(recipient.getRecipients()).extracting("memberId") + .containsExactly(darakbangHogee.getMemberId()); + } + + @DisplayName("방장 이외의 사람이 답글을 남기는 경우 방장에겐 '댓글' 알림을 보내고, 원 댓글 작성자에겐 '답글' 알림을 보낸다.") + @Test + void getReplyNotificationRecipients_WhenAuthorNotMoimer() { + Comment childComment = createComment(darakbangManager, parentComment.getId()); + + List results = commentRecipientFinder.getAllRecipient(childComment); + assertThat(results).hasSize(2); + + CommentRecipient replyRecipient = results.get(0); + assertThat(replyRecipient.getNotificationType()).isEqualTo(NotificationType.NEW_REPLY); + assertThat(replyRecipient.getRecipients()).extracting("memberId") + .containsExactly(darakbangHogee.getMemberId()); + + CommentRecipient commentRecipient = results.get(1); + assertThat(commentRecipient.getNotificationType()).isEqualTo(NotificationType.NEW_COMMENT); + assertThat(commentRecipient.getRecipients()).extracting("memberId") + .containsExactly(darakbangAnna.getMemberId()); + } + + @DisplayName("댓글 작성자가 자신에게 답글을 남기면 방장에게만 '댓글' 알림을 보낸다.") + @Test + void getReplyNotificationRecipients_WhenAuthorIsParentAuthor() { + Comment childComment = createComment(darakbangHogee, parentComment.getId()); + + List results = commentRecipientFinder.getAllRecipient(childComment); + assertThat(results).hasSize(1); + + CommentRecipient recipient = results.get(0); + assertThat(recipient.getNotificationType()).isEqualTo(NotificationType.NEW_COMMENT); + assertThat(recipient.getRecipients()).extracting("memberId") + .containsExactly(darakbangAnna.getMemberId()); + } + } + } + + private Comment createComment(DarakbangMember author, Long parentId) { + return commentRepository.save(Comment.builder() + .content("내용") + .moim(moim) + .darakbangMember(author) + .createdAt(LocalDateTime.now()) + .parentId(parentId) + .build()); + } +} 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 new file mode 100644 index 000000000..1b3afe4f7 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/MoimRecipientFinderTest.java @@ -0,0 +1,72 @@ +package mouda.backend.moim.implement.finder; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +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 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; +import mouda.backend.notification.domain.Recipient; + +@SpringBootTest +class MoimRecipientFinderTest extends DarakbangSetUp { + + @Autowired + MoimRecipientFinder moimRecipientFinder; + + @Autowired + MoimRepository moimRepository; + + @Autowired + ChamyoRepository chamyoRepository; + + @DisplayName("모임 생성 알림은 다락방 참여 전원에게 알린다.") + @Test + void getMoimCreatedNotificationRecipients() { + // given + Long darakbangId = darakbang.getId(); + + // when + List recipients = moimRecipientFinder.getMoimCreatedNotificationRecipients(darakbangId, darakbangHogee.getId()); + + //then + assertThat(recipients).hasSize(2); + } + + @DisplayName("모임 상태 변화(모집마감, 모집재개, 모임정보변경, 모임장소/시간 확정)는 방장 제외 모임참여자 전원에게 알린다.") + @Test + void getMoimStatusChangedNotificationRecipients() { + // given + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + Chamyo chamyoWithMoimerAnna = Chamyo.builder() + .moim(savedMoim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build(); + + Chamyo chamyoWithMoimeeHogee = Chamyo.builder() + .moim(savedMoim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build(); + chamyoRepository.save(chamyoWithMoimerAnna); + chamyoRepository.save(chamyoWithMoimeeHogee); + + // when + List recipients = moimRecipientFinder.getMoimStatusChangedNotificationRecipients(savedMoim.getId()); + + //then + assertThat(recipients).hasSize(1); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/moim/business/CommentServiceTest.java b/backend/src/test/java/mouda/backend/moim/moim/business/CommentServiceTest.java index 7fc610852..667617724 100644 --- a/backend/src/test/java/mouda/backend/moim/moim/business/CommentServiceTest.java +++ b/backend/src/test/java/mouda/backend/moim/moim/business/CommentServiceTest.java @@ -15,7 +15,6 @@ import mouda.backend.common.fixture.DarakbangMemberFixture; import mouda.backend.common.fixture.MemberFixture; import mouda.backend.common.fixture.MoimFixture; -import mouda.backend.common.global.IgnoreNotificationTest; import mouda.backend.darakbang.domain.Darakbang; import mouda.backend.darakbang.infrastructure.DarakbangRepository; import mouda.backend.darakbangmember.domain.DarakbangMember; @@ -23,15 +22,18 @@ import mouda.backend.member.domain.Member; import mouda.backend.member.infrastructure.MemberRepository; import mouda.backend.moim.business.CommentService; +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.exception.CommentException; +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 CommentServiceTest extends IgnoreNotificationTest { +public class CommentServiceTest { @Autowired private DarakbangRepository darakbangRepository; @@ -53,6 +55,8 @@ public class CommentServiceTest extends IgnoreNotificationTest { private Darakbang darakbang; private DarakbangMember darakbangHogee; + @Autowired + private ChamyoRepository chamyoRepository; @BeforeEach void setUp() { @@ -66,6 +70,11 @@ void setUp() { @Test void createComment() { Moim moim = moimRepository.save(MoimFixture.getBasketballMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMER) + .build()); CommentCreateRequest commentCreateRequest = new CommentCreateRequest(null, "댓글부대"); commentService.createComment(darakbang.getId(), moim.getId(), darakbangHogee, commentCreateRequest); diff --git a/backend/src/test/java/mouda/backend/moim/moim/business/MoimServiceTest.java b/backend/src/test/java/mouda/backend/moim/moim/business/MoimServiceTest.java index 9c9310bf4..76eb28d3f 100644 --- a/backend/src/test/java/mouda/backend/moim/moim/business/MoimServiceTest.java +++ b/backend/src/test/java/mouda/backend/moim/moim/business/MoimServiceTest.java @@ -1,11 +1,9 @@ package mouda.backend.moim.moim.business; import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; import java.time.LocalDate; import java.time.LocalTime; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -13,7 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import mouda.backend.common.global.IgnoreNotificationTest; import mouda.backend.common.fixture.DarakbangFixture; import mouda.backend.common.fixture.DarakbangMemberFixture; import mouda.backend.common.fixture.MemberFixture; @@ -25,18 +22,14 @@ import mouda.backend.member.domain.Member; import mouda.backend.member.infrastructure.MemberRepository; import mouda.backend.moim.business.MoimService; -import mouda.backend.moim.domain.Comment; import mouda.backend.moim.domain.Moim; -import mouda.backend.moim.exception.CommentException; -import mouda.backend.moim.infrastructure.CommentRepository; import mouda.backend.moim.infrastructure.MoimRepository; -import mouda.backend.moim.presentation.request.comment.CommentCreateRequest; import mouda.backend.moim.presentation.request.moim.MoimCreateRequest; import mouda.backend.moim.presentation.response.moim.MoimDetailsFindResponse; import mouda.backend.moim.presentation.response.moim.MoimFindAllResponses; @SpringBootTest -class MoimServiceTest extends IgnoreNotificationTest { +class MoimServiceTest { @Autowired private MoimService moimService; diff --git a/backend/src/test/java/mouda/backend/notification/business/NotificationServiceTest.java b/backend/src/test/java/mouda/backend/notification/business/NotificationServiceTest.java index f20c7c583..7f777f545 100644 --- a/backend/src/test/java/mouda/backend/notification/business/NotificationServiceTest.java +++ b/backend/src/test/java/mouda/backend/notification/business/NotificationServiceTest.java @@ -1,100 +1,100 @@ -package mouda.backend.notification.business; - -import static org.assertj.core.api.Assertions.*; - -import java.util.List; - -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 mouda.backend.common.fixture.DarakbangFixture; -import mouda.backend.common.fixture.DarakbangMemberFixture; -import mouda.backend.common.fixture.MemberFixture; -import mouda.backend.darakbang.domain.Darakbang; -import mouda.backend.darakbang.infrastructure.DarakbangRepository; -import mouda.backend.darakbangmember.domain.DarakbangMember; -import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; -import mouda.backend.member.domain.Member; -import mouda.backend.member.infrastructure.MemberRepository; -import mouda.backend.notification.domain.MemberNotification; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; -import mouda.backend.notification.presentation.response.NotificationFindAllResponse; - -@SpringBootTest -class NotificationServiceTest { - - @Autowired - private DarakbangRepository darakbangRepository; - - @Autowired - private MemberRepository memberRepository; - - @Autowired - private DarakbangMemberRepository darakbangMemberRepository; - - @Autowired - private MoudaNotificationRepository moudaNotificationRepository; - - @Autowired - private MemberNotificationRepository memberNotificationRepository; - - @Autowired - private NotificationService notificationService; - - @DisplayName("회원의 모든 알림을 조회한다.") - @Test - void findAllMyNotifications() { - Darakbang darakbang = DarakbangFixture.getDarakbangWithMouda(); - darakbangRepository.save(darakbang); - - NotificationType type1 = NotificationType.MOIM_CREATED; - MoudaNotification notification1 = moudaNotificationRepository.save(MoudaNotification.builder() - .type(type1) - .body(type1.createMessage("테스트모임")) - .targetUrl("test") - .build()); - - NotificationType type2 = NotificationType.NEW_CHAT; - MoudaNotification notification2 = moudaNotificationRepository.save(MoudaNotification.builder() - .type(type2) - .body(type2.createMessage("상돌")) - .targetUrl("test") - .build()); - - Member member = MemberFixture.getAnna(); - memberRepository.save(member); - - DarakbangMember darakbangMember = DarakbangMemberFixture.getDarakbangMemberWithWooteco(darakbang, - member); - darakbangMemberRepository.save(darakbangMember); - - memberNotificationRepository.save(MemberNotification.builder() - .memberId(darakbangMember.getMemberId()) - .moudaNotification(notification1) - .darakbangId(darakbang.getId()) - .build()); - - memberNotificationRepository.save(MemberNotification.builder() - .memberId(darakbangMember.getMemberId()) - .moudaNotification(notification2) - .darakbangId(darakbang.getId()) - .build()); - - List responses = notificationService.findAllMyNotifications(member, - darakbang.getId()) - .notifications(); - - assertThat(responses).satisfies(res -> { - assertThat(res).hasSize(2); - assertThat(res).extracting(NotificationFindAllResponse::message) - .containsExactly(type2.createMessage("상돌"), type1.createMessage("테스트모임")); - assertThat(res).extracting(NotificationFindAllResponse::type) - .containsExactly(type2.toString(), type1.toString()); - }); - } -} +// package mouda.backend.notification.business; +// +// import static org.assertj.core.api.Assertions.*; +// +// import java.util.List; +// +// 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 mouda.backend.common.fixture.DarakbangFixture; +// import mouda.backend.common.fixture.DarakbangMemberFixture; +// import mouda.backend.common.fixture.MemberFixture; +// import mouda.backend.darakbang.domain.Darakbang; +// import mouda.backend.darakbang.infrastructure.DarakbangRepository; +// import mouda.backend.darakbangmember.domain.DarakbangMember; +// import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +// import mouda.backend.member.domain.Member; +// import mouda.backend.member.infrastructure.MemberRepository; +// import mouda.backend.notification.domain.MemberNotification; +// import mouda.backend.notification.domain.MoudaNotification; +// import mouda.backend.notification.domain.NotificationType; +// import mouda.backend.notification.infrastructure.MemberNotificationRepository; +// import mouda.backend.notification.infrastructure.MoudaNotificationRepository; +// import mouda.backend.notification.presentation.response.NotificationFindAllResponse; +// +// @SpringBootTest +// class NotificationServiceTest { +// +// @Autowired +// private DarakbangRepository darakbangRepository; +// +// @Autowired +// private MemberRepository memberRepository; +// +// @Autowired +// private DarakbangMemberRepository darakbangMemberRepository; +// +// @Autowired +// private MoudaNotificationRepository moudaNotificationRepository; +// +// @Autowired +// private MemberNotificationRepository memberNotificationRepository; +// +// @Autowired +// private NotificationService notificationService; +// +// @DisplayName("회원의 모든 알림을 조회한다.") +// @Test +// void findAllMyNotifications() { +// Darakbang darakbang = DarakbangFixture.getDarakbangWithMouda(); +// darakbangRepository.save(darakbang); +// +// NotificationType type1 = NotificationType.MOIM_CREATED; +// MoudaNotification notification1 = moudaNotificationRepository.save(MoudaNotification.builder() +// .type(type1) +// .body(type1.createMessage("테스트모임")) +// .targetUrl("test") +// .build()); +// +// NotificationType type2 = NotificationType.NEW_CHAT; +// MoudaNotification notification2 = moudaNotificationRepository.save(MoudaNotification.builder() +// .type(type2) +// .body(type2.createMessage("상돌")) +// .targetUrl("test") +// .build()); +// +// Member member = MemberFixture.getAnna(); +// memberRepository.save(member); +// +// DarakbangMember darakbangMember = DarakbangMemberFixture.getDarakbangMemberWithWooteco(darakbang, +// member); +// darakbangMemberRepository.save(darakbangMember); +// +// memberNotificationRepository.save(MemberNotification.builder() +// .memberId(darakbangMember.getMemberId()) +// .moudaNotification(notification1) +// .darakbangId(darakbang.getId()) +// .build()); +// +// memberNotificationRepository.save(MemberNotification.builder() +// .memberId(darakbangMember.getMemberId()) +// .moudaNotification(notification2) +// .darakbangId(darakbang.getId()) +// .build()); +// +// List responses = notificationService.findAllMyNotifications(member, +// darakbang.getId()) +// .notifications(); +// +// assertThat(responses).satisfies(res -> { +// assertThat(res).hasSize(2); +// assertThat(res).extracting(NotificationFindAllResponse::message) +// .containsExactly(type2.createMessage("상돌"), type1.createMessage("테스트모임")); +// assertThat(res).extracting(NotificationFindAllResponse::type) +// .containsExactly(type2.toString(), type1.toString()); +// }); +// } +// } diff --git a/backend/src/test/java/mouda/backend/notification/implement/MemberNotificationFinderTest.java b/backend/src/test/java/mouda/backend/notification/implement/MemberNotificationFinderTest.java deleted file mode 100644 index 7fd8c3865..000000000 --- a/backend/src/test/java/mouda/backend/notification/implement/MemberNotificationFinderTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package mouda.backend.notification.implement; - -import static org.assertj.core.api.Assertions.*; - -import java.util.List; - -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 mouda.backend.common.fixture.MemberFixture; -import mouda.backend.member.domain.Member; -import mouda.backend.member.infrastructure.MemberRepository; -import mouda.backend.notification.domain.MemberNotification; -import mouda.backend.notification.domain.MoudaNotification; -import mouda.backend.notification.domain.NotificationType; -import mouda.backend.notification.infrastructure.MemberNotificationRepository; -import mouda.backend.notification.infrastructure.MoudaNotificationRepository; - -@SpringBootTest -public class MemberNotificationFinderTest { - - @Autowired - MemberNotificationRepository memberNotificationRepository; - - @Autowired - MoudaNotificationRepository moudaNotificationRepository; - - @Autowired - MemberRepository memberRepository; - - @Autowired - private MemberNotificationFinder memberNotificationFinder; - - @Test - @DisplayName("Member와 DarakbangId에 해당하는 알림 목록을 역순으로 반환한다.") - void findAllMemberNotifications_success() { - // given - Member member = MemberFixture.getTebah(); - Member savedMember = memberRepository.save(member); - - long darakbangId = 1L; - NotificationType type1 = NotificationType.MOIM_CREATED; - MoudaNotification notification1 = moudaNotificationRepository.save(MoudaNotification.builder() - .type(type1) - .body(type1.createMessage("테스트모임")) - .targetUrl("test") - .build()); - - NotificationType type2 = NotificationType.NEW_CHAT; - MoudaNotification notification2 = moudaNotificationRepository.save(MoudaNotification.builder() - .type(type2) - .body(type2.createMessage("상돌")) - .targetUrl("test") - .build()); - - MemberNotification memberNotification1 = MemberNotification.builder() - .memberId(savedMember.getId()) - .moudaNotification(notification1) - .darakbangId(darakbangId) - .build(); - - MemberNotification memberNotification2 = MemberNotification.builder() - .memberId(savedMember.getId()) - .moudaNotification(notification2) - .darakbangId(darakbangId) - .build(); - - memberNotificationRepository.save(memberNotification1); - memberNotificationRepository.save(memberNotification2); - - // when - List actualNotifications = memberNotificationFinder.findAll(member, darakbangId); - - // then - assertThat(actualNotifications).hasSize(2); - assertThat(actualNotifications) - .extracting("id") - .containsExactly(memberNotification2.getId(), memberNotification1.getId()); - } -} diff --git a/backend/src/test/java/mouda/backend/notification/implement/NotificationFinderTest.java b/backend/src/test/java/mouda/backend/notification/implement/NotificationFinderTest.java new file mode 100644 index 000000000..053628f63 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/NotificationFinderTest.java @@ -0,0 +1,54 @@ +package mouda.backend.notification.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.IntStream; + +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 mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.notification.domain.MemberNotification; +import mouda.backend.notification.infrastructure.entity.MemberNotificationEntity; +import mouda.backend.notification.infrastructure.repository.MemberNotificationRepository; + +@SpringBootTest +class NotificationFinderTest extends DarakbangSetUp { + + @Autowired + private NotificationFinder notificationFinder; + + @Autowired + private MemberNotificationRepository memberNotificationRepository; + + @DisplayName("회원의 모든 알림을 조회한다.") + @Test + void findAllMemberNotification() { + // given + memberNotificationRepository.saveAll(createTestEntity(darakbangHogee, 10)); + + // when + List result = notificationFinder.findAllMemberNotification(darakbangHogee); + + // then + assertThat(result).hasSize(10); + } + + private List createTestEntity(DarakbangMember darakbangMember, int count) { + return IntStream.rangeClosed(1, count) + .mapToObj(i -> MemberNotificationEntity.builder() + .darakbangMemberId(darakbangMember.getId()) + .title("title" + i) + .body("body" + i) + .createdAt(LocalDateTime.now()) + .type("MOIM_CREATED") + .targeturl("targeturl" + i) + .build()) + .toList(); + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/fcm/FcmMessageFactoryTest.java b/backend/src/test/java/mouda/backend/notification/implement/fcm/FcmMessageFactoryTest.java new file mode 100644 index 000000000..f2e207a4a --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/fcm/FcmMessageFactoryTest.java @@ -0,0 +1,116 @@ +package mouda.backend.notification.implement.fcm; + +import static org.assertj.core.api.Assertions.*; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.stream.IntStream; + +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 com.google.firebase.messaging.MulticastMessage; +import com.google.firebase.messaging.WebpushConfig; + +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.NotificationType; + +@SpringBootTest +class FcmMessageFactoryTest { + + @Autowired + private FcmMessageFactory fcmMessageFactory; + + @DisplayName("토큰 500개당 1개의 메시지가 생성된다.") + @Test + void createMessage() { + // given + CommonNotification notification = CommonNotification.builder() + .title("title") + .body("body") + .redirectUrl("redirectUrl") + .type(NotificationType.MOIM_CREATED) + .build(); + int tokenCount = 5196; + List tokens = createTokens(tokenCount); + + // when + List messages = fcmMessageFactory.createMessage(notification, tokens); + int expectedSize = calculateExpectedSize(tokenCount); + + // then + assertThat(messages).hasSize(expectedSize); + } + + @DisplayName("토큰이 500개 미만일 때는 1개의 메시지가 생성된다.") + @Test + void createMessage_WhenTokenCountIsLessThan500() { + // given + CommonNotification notification = CommonNotification.builder() + .title("title") + .body("body") + .redirectUrl("redirectUrl") + .type(NotificationType.MOIM_CREATED) + .build(); + int tokenCount = 499; + List tokens = createTokens(tokenCount); + + // when + List messages = fcmMessageFactory.createMessage(notification, tokens); + + // then + assertThat(messages).hasSize(1); + } + + @DisplayName("각 플랫폼별 설정이 반영된 메시지가 생성된다.") + @Test + void createMessage_WithConfig() { + CommonNotification notification = CommonNotification.builder() + .title("title") + .body("body") + .redirectUrl("redirectUrl") + .type(NotificationType.MOIM_CREATED) + .build(); + List tokens = createTokens(1001); + + List messages = fcmMessageFactory.createMessage(notification, tokens); + List allWebpushConfigs = messages.stream() + .map(message -> getFieldFromMulticastMessage("webpushConfig", WebpushConfig.class, message)) + .toList(); + + assertThat(allWebpushConfigs).doesNotContainNull(); + + // WebpushConfig 는 static 이므로 모든 객체가 동일한 참조를 가져야 한다. + assertThat(allWebpushConfigs).allMatch(webpushConfig -> webpushConfig == allWebpushConfigs.get(0)); + } + + + private List createTokens(int size) { + return IntStream.rangeClosed(1, size) + .mapToObj(i -> "token" + i) + .toList(); + } + + private int calculateExpectedSize(int tokenCount) { + int quotient = (int) tokenCount / 500; + int remainder = tokenCount % 500; + + if (remainder == 0) { + return quotient; + } + return quotient + 1; + } + + private T getFieldFromMulticastMessage(String name, Class type, MulticastMessage message) { + try { + Field field = message.getClass().getDeclaredField(name); + field.setAccessible(true); + + return type.cast(field.get(message)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriterTest.java b/backend/src/test/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriterTest.java new file mode 100644 index 000000000..eea42aa5d --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriterTest.java @@ -0,0 +1,91 @@ +package mouda.backend.notification.implement.fcm.token; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.notification.infrastructure.entity.FcmTokenEntity; +import mouda.backend.notification.infrastructure.repository.FcmTokenRepository; + +@SpringBootTest +class FcmTokenWriterTest extends DarakbangSetUp { + + @Autowired + private FcmTokenRepository fcmTokenRepository; + + @Autowired + private FcmTokenWriter fcmTokenWriter; + + @DisplayName("토큰 등록 / 갱신 테스트") + @Nested + class SaveOrRefreshTest { + + @DisplayName("토큰이 존재하지 않는 경우 새로 등록한다.") + @Test + void saveToken() { + // given + String token = "testToken"; + + // when + fcmTokenWriter.saveOrRefresh(hogee, token); + + // then + List results = fcmTokenRepository.findAllByMemberId(darakbangHogee.getMemberId()); + + assertThat(results).hasSize(1); + assertThat(results).extracting(FcmTokenEntity::getToken).containsExactly(token); + } + + @DisplayName("토큰이 존재하는 경우 갱신한다.") + @Test + void refreshToken() { + // given + FcmTokenEntity existToken = fcmTokenRepository.save(FcmTokenEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .token("testToken") + .build()); + + // when + fcmTokenWriter.saveOrRefresh(hogee, existToken.getToken()); + + // then + List results = fcmTokenRepository.findAllByMemberId(darakbangHogee.getMemberId()); + assertThat(results).hasSize(1); + + FcmTokenEntity result = results.get(0); + assertThat(result.getLastUpdated()).isAfter(existToken.getLastUpdated()); + } + } + + @DisplayName("입력된 모든 토큰을 삭제한다.") + @Test + void deleteAllTest() { + // given + String token1 = "token1"; + String token2 = "token2"; + + fcmTokenRepository.save(FcmTokenEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .token(token1) + .build()); + + fcmTokenRepository.save(FcmTokenEntity.builder() + .memberId(darakbangAnna.getMemberId()) + .token(token2) + .build()); + + // when + List tokens = List.of(token1, token2); + fcmTokenWriter.deleteAll(tokens); + + // then + assertThat(fcmTokenRepository.findAll()).isEmpty(); + } +} 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 new file mode 100644 index 000000000..076ede654 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilterTest.java @@ -0,0 +1,90 @@ +package mouda.backend.notification.implement.filter; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +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 mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.notification.domain.NotificationEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.implement.subscription.SubscriptionWriter; + +@SpringBootTest +class ChatRoomSubscriptionFilterTest extends DarakbangSetUp { + + @Autowired + private ChatRoomSubscriptionFilter chatRoomSubscriptionFilter; + + @Autowired + private SubscriptionWriter subscriptionWriter; + + @DisplayName("채팅 알림을 허용하지 않아도 확정 채팅인 경우에는 알림을 받는다.") + @Test + void filter_WhenTypeIsConfirmed() { + // given + subscriptionWriter.changeChatRoomSubscription(hogee, darakbang.getId(), 1L); + + // when + NotificationEvent notificationEvent = new NotificationEvent( + NotificationType.MOIM_PLACE_CONFIRMED, + "모임 제목", + "메시지", + "url", + List.of(new Recipient(hogee.getId(), darakbangHogee.getId())), + darakbang.getId(), + 1L); + + // then + List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationEvent); + assertThat(filteredRecipient).hasSize(1); + assertThat(filteredRecipient).extracting(Recipient::getMemberId).containsExactly(darakbangHogee.getMemberId()); + } + + @DisplayName("채팅 알림을 허용하지 않는 경우에는 알림을 받지 않는다.") + @Test + void filter_WhenUnsubscribed() { + // given + subscriptionWriter.changeChatRoomSubscription(hogee, darakbang.getId(), 1L); + + // when + NotificationEvent notificationEvent = new NotificationEvent( + NotificationType.NEW_CHAT, + "모임 제목", + "메시지", + "url", + List.of(new Recipient(darakbangHogee.getMemberId(), darakbangHogee.getId())), + darakbang.getId(), + 1L + ); + + // then + List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationEvent); + assertThat(filteredRecipient).isEmpty(); + } + + @DisplayName("채팅 알림을 허용하는 경우에는 알림을 받는다.") + @Test + void filter_WhenSubscribed() { + // when + NotificationEvent notificationEvent = new NotificationEvent( + NotificationType.NEW_CHAT, + "모임 제목", + "메시지", + "url", + List.of(new Recipient(darakbangHogee.getMemberId(), darakbangHogee.getId())), + darakbang.getId(), + 1L + ); + + // then + List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationEvent); + assertThat(filteredRecipient).hasSize(1); + assertThat(filteredRecipient).extracting(Recipient::getMemberId).containsExactly(darakbangHogee.getMemberId()); + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionFinderTest.java b/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionFinderTest.java new file mode 100644 index 000000000..e7638d8d6 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionFinderTest.java @@ -0,0 +1,57 @@ +package mouda.backend.notification.implement.subscription; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +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 mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.notification.domain.Subscription; +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; +import mouda.backend.notification.infrastructure.entity.UnsubscribedChatRooms; +import mouda.backend.notification.infrastructure.repository.SubscriptionRepository; + +@SpringBootTest +class SubscriptionFinderTest extends DarakbangSetUp { + + @Autowired + private SubscriptionWriter subscriptionWriter; + + @Autowired + private SubscriptionFinder subscriptionFinder; + + @Autowired + private SubscriptionRepository subscriptionRepository; + + @DisplayName("구독이 존재하지 않으면 새로 생성한 뒤 반환한다.") + @Test + void readSubscription_WhenSubscriptionNotExist() { + Subscription subscription = subscriptionFinder.readSubscription(hogee); + + assertThat(subscription.getUnsubscribedChatRooms()).isEmpty(); + assertThat(subscription.isSubscribedMoimCreate()).isTrue(); + } + + @DisplayName("기존 구독 정보가 조회하면 그대로 반환한다.") + @Test + void readSubscription_WhenSubscriptionExist() { + // given + subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .unsubscribedChats(List.of(UnsubscribedChatRooms.create(1L, 10L))) + .build()); + subscriptionWriter.changeMoimSubscription(hogee); + + // when + Subscription result = subscriptionFinder.readSubscription(hogee); + + // then + assertThat(result.isSubscribedMoimCreate()).isFalse(); + assertThat(result.getUnsubscribedChatRooms()).hasSize(1); + assertThat(result.isSubscribedChatRoom(1L, 10L)).isFalse(); + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionWriterTest.java b/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionWriterTest.java new file mode 100644 index 000000000..e6acc9f4e --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionWriterTest.java @@ -0,0 +1,160 @@ +package mouda.backend.notification.implement.subscription; + +import static org.assertj.core.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; +import mouda.backend.notification.infrastructure.entity.UnsubscribedChatRooms; +import mouda.backend.notification.infrastructure.repository.SubscriptionRepository; + +@SpringBootTest +class SubscriptionWriterTest extends DarakbangSetUp { + + @Autowired + private SubscriptionWriter subscriptionWriter; + + @Autowired + private SubscriptionRepository subscriptionRepository; + + + @DisplayName("모임 생성에 대한 알림 허용 여부를 변경한다.") + @Nested + class MoimCreateSubscriptionTest { + + @DisplayName("구독 정보가 존재하지 않으면 새로 만든 뒤 비허용 상태로 변경한다.") + @Test + void changeMoimCreateSubscription_WhenNotExist() { + // when + subscriptionWriter.changeMoimSubscription(hogee); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + assertThat(subscription.isSubscribedMoimCreate()).isFalse(); + } + + @DisplayName("알림 허용 상태에서 비허용 상태로 변경한다.") + @Test + void changeMoimCreateSubscription_WhenSubscribedMoimCreate() { + // given + subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .unsubscribedChats(new ArrayList<>()) + .build()); + + // when + subscriptionWriter.changeMoimSubscription(hogee); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + assertThat(subscription.isSubscribedMoimCreate()).isFalse(); + } + + @DisplayName("알림 비허용 상태에서 허용 상태로 변경한다.") + @Test + void changeMoimCreateSubscription_WhenUnsubscribedMoimCreate() { + // given + subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .unsubscribedChats(new ArrayList<>()) + .build()); + subscriptionWriter.changeMoimSubscription(hogee); + + // when + subscriptionWriter.changeMoimSubscription(hogee); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + assertThat(subscription.isSubscribedMoimCreate()).isTrue(); + } + } + + @DisplayName("특정 채팅방에 대한 알림 허용 여부를 변경한다.") + @Nested + class ChatRoomSubscriptionTest { + + @DisplayName("구독 정보가 존재하지 않으면 새로 만든 뒤 비허용 상태로 변경한다.") + @Test + void changeChatRoomSubscription_WhenNotExist() { + // when + subscriptionWriter.changeChatRoomSubscription(hogee, 1L, 10L); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + UnsubscribedChatRooms unsubscribedChatRooms = subscription.getUnsubscribedChats().get(0); + assertThat(unsubscribedChatRooms.getDarakbangId()).isEqualTo(1L); + assertThat(unsubscribedChatRooms.getChatRoomIds().contains(10L)).isTrue(); + } + + @DisplayName("알림 허용 상태에서 비허용 상태로 변경한다.") + @Test + void changeChatRoomSubscription_WhenSubscribedChatRoom() { + // given + subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .unsubscribedChats(new ArrayList<>()) + .build()); + + // when + subscriptionWriter.changeChatRoomSubscription(hogee, 1L, 10L); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + UnsubscribedChatRooms unsubscribedChatRooms = subscription.getUnsubscribedChats().get(0); + assertThat(unsubscribedChatRooms.getDarakbangId()).isEqualTo(1L); + assertThat(unsubscribedChatRooms.getChatRoomIds().contains(10L)).isTrue(); + } + + @DisplayName("알림 비허용 상태에서 허용 상태로 변경한다.") + @Test + void changeChatRoomSubscription_WhenUnSubscribedChatRoom() { + // given + subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .unsubscribedChats(new ArrayList<>()) + .build()); + subscriptionWriter.changeChatRoomSubscription(hogee, 1L, 10L); + + // when + subscriptionWriter.changeChatRoomSubscription(hogee, 1L, 10L); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + UnsubscribedChatRooms unsubscribedChatRooms = subscription.getUnsubscribedChats().get(0); + assertThat(unsubscribedChatRooms.getDarakbangId()).isEqualTo(1L); + assertThat(unsubscribedChatRooms.getChatRoomIds().contains(10L)).isFalse(); + } + } +} diff --git a/backend/src/test/java/mouda/backend/notification/infrastructure/SubscriptionEntityTest.java b/backend/src/test/java/mouda/backend/notification/infrastructure/SubscriptionEntityTest.java new file mode 100644 index 000000000..be67122b7 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/infrastructure/SubscriptionEntityTest.java @@ -0,0 +1,54 @@ +package mouda.backend.notification.infrastructure; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; +import mouda.backend.notification.infrastructure.entity.UnsubscribedChatRooms; +import mouda.backend.notification.infrastructure.repository.SubscriptionRepository; + +@SpringBootTest +public class SubscriptionEntityTest { + + @Autowired + private SubscriptionRepository subscriptionRepository; + + @Test + public void testSaveAndRetrieveSubscriptionEntity() { + // given: 구독 정보 JSON 생성 + UnsubscribedChatRooms subscription1 = new UnsubscribedChatRooms(1L, Arrays.asList(1L, 2L, 3L)); + UnsubscribedChatRooms subscription2 = new UnsubscribedChatRooms(2L, Arrays.asList(4L, 5L, 6L)); + List subscriptions = Arrays.asList(subscription1, subscription2); + + SubscriptionEntity subscriptionEntity = SubscriptionEntity.builder() + .memberId(100L) + .unsubscribedChats(subscriptions) + .build(); + + // when: 엔티티 저장 + SubscriptionEntity savedEntity = subscriptionRepository.save(subscriptionEntity); + + // then: 저장된 데이터 다시 조회 + Optional retrievedEntityOpt = subscriptionRepository.findById(savedEntity.getId()); + assertThat(retrievedEntityOpt).isPresent(); + + SubscriptionEntity retrievedEntity = retrievedEntityOpt.get(); + assertThat(retrievedEntity.getMemberId()).isEqualTo(100L); + assertThat(retrievedEntity.isMoimCreate()).isTrue(); + + System.out.println(retrievedEntityOpt); + + // JSON 데이터 비교 + List retrievedSubscriptions = retrievedEntity.getUnsubscribedChats(); + assertThat(retrievedSubscriptions).hasSize(2); + assertThat(retrievedSubscriptions.get(0).getDarakbangId()).isEqualTo(1L); + assertThat(retrievedSubscriptions.get(0).getChatRoomIds()).containsExactly(1L, 2L, 3L); + } +}