From d58bb31dadd1e87a00b11206ff1cbe106cd20f63 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 04:02:00 +0900 Subject: [PATCH 01/28] =?UTF-8?q?[SCRUM-164]=20feat:=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EA=B8=B0=EB=B3=B8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=B0=8F?= =?UTF-8?q?=20=EB=8C=93=EA=B8=80=20=EC=9E=91=EC=84=B1=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../service/CommentCommandService.java | 7 +- .../domain/common/message/ErrorCode.java | 3 + .../notification/domain/Notification.java | 86 +++++++++++++++++++ .../notification/domain/NotificationType.java | 9 ++ .../domain/event/CommentEvent.java | 22 +++++ .../UnknownNotificationException.java | 11 +++ .../NotificationCommandRepository.java | 9 ++ .../NotificationQueryRepository.java | 4 + .../service/NotificationCommandService.java | 25 ++++++ .../server/domain/user/entity/User.java | 10 ++- .../global/configuration/AsyncConfig.java | 26 ++++++ .../global/configuration/FirebaseConfig.java | 31 +++++++ 13 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/domain/event/CommentEvent.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/exception/UnknownNotificationException.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationCommandRepository.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java create mode 100644 src/main/java/com/projectlyrics/server/global/configuration/AsyncConfig.java create mode 100644 src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java diff --git a/build.gradle b/build.gradle index 43b68c3b..16e07cab 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,9 @@ dependencies { // redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // firebase cloud messaging + implementation 'com.google.firebase:firebase-admin:9.2.0' } swaggerSources { diff --git a/src/main/java/com/projectlyrics/server/domain/comment/service/CommentCommandService.java b/src/main/java/com/projectlyrics/server/domain/comment/service/CommentCommandService.java index e127827a..a79df3f7 100644 --- a/src/main/java/com/projectlyrics/server/domain/comment/service/CommentCommandService.java +++ b/src/main/java/com/projectlyrics/server/domain/comment/service/CommentCommandService.java @@ -12,10 +12,12 @@ import com.projectlyrics.server.domain.note.entity.Note; import com.projectlyrics.server.domain.note.exception.NoteNotFoundException; import com.projectlyrics.server.domain.note.repository.NoteQueryRepository; +import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; import com.projectlyrics.server.domain.user.entity.User; import com.projectlyrics.server.domain.user.exception.UserNotFoundException; import com.projectlyrics.server.domain.user.repository.UserQueryRepository; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,6 +32,7 @@ public class CommentCommandService { private final CommentQueryRepository commentQueryRepository; private final UserQueryRepository userQueryRepository; private final NoteQueryRepository noteQueryRepository; + private final ApplicationEventPublisher eventPublisher; public Comment create(CommentCreateRequest request, Long writerId) { User writer = userQueryRepository.findById(writerId) @@ -37,7 +40,9 @@ public Comment create(CommentCreateRequest request, Long writerId) { Note note = noteQueryRepository.findById(request.noteId()) .orElseThrow(NoteNotFoundException::new); - return commentCommandRepository.save(Comment.create(CommentCreate.of(request.content(), writer, note))); + Comment comment = Comment.create(CommentCreate.of(request.content(), writer, note)); + eventPublisher.publishEvent(CommentEvent.from(comment)); + return commentCommandRepository.save(comment); } public Comment update(CommentUpdateRequest request, Long commentId, Long writerId) { diff --git a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java index 2f53163e..e34946f1 100644 --- a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java +++ b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java @@ -71,6 +71,9 @@ public enum ErrorCode { // Bookmark BOOKMARK_NOT_FOUND(HttpStatus.NOT_FOUND, "10000", "해당 북마크를 조회할 수 없습니다."), BOOKMARK_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "10001", "이미 북마크를 추가한 상태입니다."), + + // Notification + UNKNOWN_NOTIFICATION_TYPE(HttpStatus.BAD_REQUEST, "11000", "알 수 없는 알림 타입입니다."), ; private final HttpStatus responseStatus; diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java new file mode 100644 index 00000000..697adc14 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java @@ -0,0 +1,86 @@ +package com.projectlyrics.server.domain.notification.domain; + +import com.google.firebase.messaging.Message; +import com.projectlyrics.server.domain.comment.domain.Comment; +import com.projectlyrics.server.domain.common.entity.BaseEntity; +import com.projectlyrics.server.domain.like.domain.Like; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "notifications") +@EqualsAndHashCode(of = "id", callSuper = false) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Notification extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private NotificationType type; + + @ManyToOne(fetch = FetchType.LAZY) + private User sender; + @ManyToOne(fetch = FetchType.LAZY) + private User receiver; + + @ManyToOne(fetch = FetchType.LAZY) + private Note note; + @ManyToOne(fetch = FetchType.LAZY) + private Comment comment; + @ManyToOne(fetch = FetchType.LAZY) + private Like like; + + private Notification( + Long id, + NotificationType type, + User sender, + User receiver, + Note note, + Comment comment, + Like like + ) { + this.id = id; + this.type = type; + this.sender = sender; + this.receiver = receiver; + this.note = note; + this.comment = comment; + this.like = like; + } + + private Notification( + NotificationType type, + User sender, + User receiver, + Note note, + Comment comment, + Like like + ) { + this(null, type, sender, receiver, note, comment, like); + } + + public static Notification create(CommentEvent event) { + return new Notification( + NotificationType.COMMENT_ON_NOTE, + event.sender(), + event.receiver(), + event.note(), + event.comment(), + null + ); + } + + public Message getMessage() { + return Message.builder() + .setToken(receiver.getFcmToken()) + .build(); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java new file mode 100644 index 00000000..bfcd1fad --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java @@ -0,0 +1,9 @@ +package com.projectlyrics.server.domain.notification.domain; + +public enum NotificationType { + COMMENT_ON_NOTE, + LIKE_ON_NOTE, + REPORT, + ALL, + ; +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/event/CommentEvent.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/event/CommentEvent.java new file mode 100644 index 00000000..497f516f --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/event/CommentEvent.java @@ -0,0 +1,22 @@ +package com.projectlyrics.server.domain.notification.domain.event; + +import com.projectlyrics.server.domain.comment.domain.Comment; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.user.entity.User; + +public record CommentEvent( + User sender, + User receiver, + Note note, + Comment comment +) { + + public static CommentEvent from(Comment comment) { + return new CommentEvent( + comment.getWriter(), + comment.getNote().getPublisher(), + comment.getNote(), + comment + ); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/exception/UnknownNotificationException.java b/src/main/java/com/projectlyrics/server/domain/notification/exception/UnknownNotificationException.java new file mode 100644 index 00000000..5c640372 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/exception/UnknownNotificationException.java @@ -0,0 +1,11 @@ +package com.projectlyrics.server.domain.notification.exception; + +import com.projectlyrics.server.domain.common.message.ErrorCode; +import com.projectlyrics.server.global.exception.FeelinException; + +public class UnknownNotificationException extends FeelinException { + + public UnknownNotificationException() { + super(ErrorCode.UNKNOWN_NOTIFICATION_TYPE); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationCommandRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationCommandRepository.java new file mode 100644 index 00000000..19cfe5bf --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationCommandRepository.java @@ -0,0 +1,9 @@ +package com.projectlyrics.server.domain.notification.repository; + +import com.projectlyrics.server.domain.notification.domain.Notification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NotificationCommandRepository extends JpaRepository { +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java new file mode 100644 index 00000000..dba63a85 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java @@ -0,0 +1,4 @@ +package com.projectlyrics.server.domain.notification.repository; + +public interface NotificationQueryRepository { +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java new file mode 100644 index 00000000..3e13bd90 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java @@ -0,0 +1,25 @@ +package com.projectlyrics.server.domain.notification.service; + +import com.projectlyrics.server.domain.notification.domain.Notification; +import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.notification.repository.NotificationCommandRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class NotificationCommandService { + + private final NotificationCommandRepository notificationCommandRepository; + + @Async + @EventListener + public void createCommentNotification(CommentEvent event) { + Notification notification = notificationCommandRepository.save(Notification.create(event)); + + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/user/entity/User.java b/src/main/java/com/projectlyrics/server/domain/user/entity/User.java index 6fa9a5d1..6f0be566 100644 --- a/src/main/java/com/projectlyrics/server/domain/user/entity/User.java +++ b/src/main/java/com/projectlyrics/server/domain/user/entity/User.java @@ -50,6 +50,8 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List termsAgreements; + private String fcmToken; + private User( Long id, SocialInfo socialInfo, @@ -58,7 +60,8 @@ private User( Role role, Gender gender, Integer birthYear, - List termsAgreements + List termsAgreements, + String fcmToken ) { checkNull(socialInfo); checkNull(termsAgreements); @@ -71,6 +74,7 @@ private User( this.info = new UserMetaInfo(gender, birthYear); this.termsAgreements = termsAgreements; termsAgreements.forEach(terms -> terms.setUser(this)); + this.fcmToken = fcmToken; } private User( @@ -82,7 +86,7 @@ private User( Integer birthYear, List termsAgreements ) { - this(null, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements); + this(null, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, null); } public static User withId( @@ -95,7 +99,7 @@ public static User withId( Integer birthYear, List termsAgreements ) { - return new User(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements); + return new User(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, null); } public static User create(UserCreate userCreate) { diff --git a/src/main/java/com/projectlyrics/server/global/configuration/AsyncConfig.java b/src/main/java/com/projectlyrics/server/global/configuration/AsyncConfig.java new file mode 100644 index 00000000..2d1e44fd --- /dev/null +++ b/src/main/java/com/projectlyrics/server/global/configuration/AsyncConfig.java @@ -0,0 +1,26 @@ +package com.projectlyrics.server.global.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@EnableAsync +@Configuration +public class AsyncConfig { + + @Bean + public Executor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + + executor.setCorePoolSize(3); + executor.setMaxPoolSize(30); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("async-executor-"); + + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java new file mode 100644 index 00000000..7aee7751 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java @@ -0,0 +1,31 @@ +package com.projectlyrics.server.global.configuration; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; + +import java.io.FileInputStream; +import java.io.IOException; + +@Slf4j +@Configuration +public class FirebaseConfig { + + @PostConstruct + public void init() { + try { + FileInputStream key = new FileInputStream("src/main/resources/firebase-key.json"); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(key)) + .build(); + + FirebaseApp.initializeApp(options); + } catch (IOException e) { + log.error("failed to initialize firebase", e); + } + } +} From 814fd5b4f3331a4ba15a7811e34fad19f133d83d Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 12:54:20 +0900 Subject: [PATCH 02/28] =?UTF-8?q?[SCRUM-164]=20feat:=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A0=84=EC=86=A1?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/message/ErrorCode.java | 1 + .../notification/domain/Notification.java | 18 +++++++++++++++--- .../FailedToSendNotificationException.java | 11 +++++++++++ .../service/NotificationCommandService.java | 14 ++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/exception/FailedToSendNotificationException.java diff --git a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java index e34946f1..1c2342de 100644 --- a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java +++ b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java @@ -74,6 +74,7 @@ public enum ErrorCode { // Notification UNKNOWN_NOTIFICATION_TYPE(HttpStatus.BAD_REQUEST, "11000", "알 수 없는 알림 타입입니다."), + NOTIFICATION_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "11001", "알림 전송에 실패했습니다."), ; private final HttpStatus responseStatus; diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java index 697adc14..ccde6d2d 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java @@ -79,8 +79,20 @@ public static Notification create(CommentEvent event) { } public Message getMessage() { - return Message.builder() - .setToken(receiver.getFcmToken()) - .build(); + Message.Builder builder = Message.builder() + .setToken(receiver.getFcmToken()); + + switch (type) { + case COMMENT_ON_NOTE: + return builder + .putData("type", type.name()) + .putData("senderId", sender.getId().toString()) + .putData("senderNickname", sender.getNickname().getValue()) + .putData("noteId", note.getId().toString()) + .putData("noteTitle", note.getContent()) + .build(); + default: + throw new IllegalArgumentException("Invalid notification type"); + } } } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/exception/FailedToSendNotificationException.java b/src/main/java/com/projectlyrics/server/domain/notification/exception/FailedToSendNotificationException.java new file mode 100644 index 00000000..7f9f6cf6 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/exception/FailedToSendNotificationException.java @@ -0,0 +1,11 @@ +package com.projectlyrics.server.domain.notification.exception; + +import com.projectlyrics.server.domain.common.message.ErrorCode; +import com.projectlyrics.server.global.exception.FeelinException; + +public class FailedToSendNotificationException extends FeelinException { + + public FailedToSendNotificationException() { + super(ErrorCode.NOTIFICATION_SEND_FAILED); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java index 3e13bd90..6d02c132 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java @@ -1,14 +1,19 @@ package com.projectlyrics.server.domain.notification.service; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; import com.projectlyrics.server.domain.notification.domain.Notification; import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.notification.exception.FailedToSendNotificationException; import com.projectlyrics.server.domain.notification.repository.NotificationCommandRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @Transactional @RequiredArgsConstructor @@ -20,6 +25,15 @@ public class NotificationCommandService { @EventListener public void createCommentNotification(CommentEvent event) { Notification notification = notificationCommandRepository.save(Notification.create(event)); + send(notification); + } + private void send(Notification notification) { + try { + FirebaseMessaging.getInstance().send(notification.getMessage()); + } catch (FirebaseMessagingException e) { + log.info(e.getMessage()); + throw new FailedToSendNotificationException(); + } } } From 0d48022f48a309d98115a3acf327a181219e484d Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 12:55:18 +0900 Subject: [PATCH 03/28] =?UTF-8?q?[SCRUM-164]=20test:=20=EB=8C=93=EA=B8=80?= =?UTF-8?q?=20=EC=B8=A1=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=ED=96=89?= =?UTF-8?q?=20=EC=97=AC=EB=B6=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationCommandServiceTest.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java diff --git a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java new file mode 100644 index 00000000..110c63f2 --- /dev/null +++ b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java @@ -0,0 +1,61 @@ +package com.projectlyrics.server.domain.notification.service; + +import com.projectlyrics.server.domain.comment.domain.Comment; +import com.projectlyrics.server.domain.comment.dto.request.CommentCreateRequest; +import com.projectlyrics.server.domain.comment.repository.CommentCommandRepository; +import com.projectlyrics.server.domain.comment.repository.CommentQueryRepository; +import com.projectlyrics.server.domain.comment.service.CommentCommandService; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.note.repository.NoteQueryRepository; +import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.user.entity.User; +import com.projectlyrics.server.domain.user.repository.UserQueryRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class NotificationCommandServiceTest { + + @Mock + private CommentCommandRepository commentCommandRepository; + + @Mock + private CommentQueryRepository commentQueryRepository; + + @Mock + private UserQueryRepository userQueryRepository; + + @Mock + private NoteQueryRepository noteQueryRepository; + + @Mock + private ApplicationEventPublisher eventPublisher; + + @InjectMocks + private CommentCommandService commentCommandService; + + @Test + void 댓글을_저장할_때_이벤트가_발행된다() { + // given + CommentCreateRequest request = new CommentCreateRequest("댓글 내용", 1L); + when(userQueryRepository.findById(anyLong())).thenReturn(Optional.of(mock(User.class))); + when(noteQueryRepository.findById(anyLong())).thenReturn(Optional.of(mock(Note.class))); + when(commentCommandRepository.save(any(Comment.class))).thenReturn(mock(Comment.class)); + + // when + commentCommandService.create(request, 1L); + + // then + verify(eventPublisher).publishEvent(any(CommentEvent.class)); + } +} \ No newline at end of file From f17c9fc49603074d75a32e42eca57859b54b951e Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 13:28:00 +0900 Subject: [PATCH 04/28] =?UTF-8?q?[SCRUM-164]=20test:=20CommentCommandServi?= =?UTF-8?q?ceMockTest=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CommentCommandServiceMockTest.java} | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename src/test/java/com/projectlyrics/server/domain/{notification/service/NotificationCommandServiceTest.java => comment/service/CommentCommandServiceMockTest.java} (92%) diff --git a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/comment/service/CommentCommandServiceMockTest.java similarity index 92% rename from src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java rename to src/test/java/com/projectlyrics/server/domain/comment/service/CommentCommandServiceMockTest.java index 110c63f2..bba049d6 100644 --- a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java +++ b/src/test/java/com/projectlyrics/server/domain/comment/service/CommentCommandServiceMockTest.java @@ -1,10 +1,9 @@ -package com.projectlyrics.server.domain.notification.service; +package com.projectlyrics.server.domain.comment.service; import com.projectlyrics.server.domain.comment.domain.Comment; import com.projectlyrics.server.domain.comment.dto.request.CommentCreateRequest; import com.projectlyrics.server.domain.comment.repository.CommentCommandRepository; import com.projectlyrics.server.domain.comment.repository.CommentQueryRepository; -import com.projectlyrics.server.domain.comment.service.CommentCommandService; import com.projectlyrics.server.domain.note.entity.Note; import com.projectlyrics.server.domain.note.repository.NoteQueryRepository; import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; @@ -24,7 +23,7 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -class NotificationCommandServiceTest { +class CommentCommandServiceMockTest { @Mock private CommentCommandRepository commentCommandRepository; From 0acb018bb021a00b4b9a7a8ab14ee194a48be391 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 13:28:15 +0900 Subject: [PATCH 05/28] =?UTF-8?q?[SCRUM-164]=20refactor:=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EC=95=8C=EB=A6=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/notification/domain/Notification.java | 15 ++++----------- .../notification/domain/NotificationType.java | 1 - 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java index ccde6d2d..0ebf1ba5 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java @@ -3,7 +3,6 @@ import com.google.firebase.messaging.Message; import com.projectlyrics.server.domain.comment.domain.Comment; import com.projectlyrics.server.domain.common.entity.BaseEntity; -import com.projectlyrics.server.domain.like.domain.Like; import com.projectlyrics.server.domain.note.entity.Note; import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; import com.projectlyrics.server.domain.user.entity.User; @@ -35,8 +34,6 @@ public class Notification extends BaseEntity { private Note note; @ManyToOne(fetch = FetchType.LAZY) private Comment comment; - @ManyToOne(fetch = FetchType.LAZY) - private Like like; private Notification( Long id, @@ -44,8 +41,7 @@ private Notification( User sender, User receiver, Note note, - Comment comment, - Like like + Comment comment ) { this.id = id; this.type = type; @@ -53,7 +49,6 @@ private Notification( this.receiver = receiver; this.note = note; this.comment = comment; - this.like = like; } private Notification( @@ -61,10 +56,9 @@ private Notification( User sender, User receiver, Note note, - Comment comment, - Like like + Comment comment ) { - this(null, type, sender, receiver, note, comment, like); + this(null, type, sender, receiver, note, comment); } public static Notification create(CommentEvent event) { @@ -73,8 +67,7 @@ public static Notification create(CommentEvent event) { event.sender(), event.receiver(), event.note(), - event.comment(), - null + event.comment() ); } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java index bfcd1fad..73c8b422 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java @@ -2,7 +2,6 @@ public enum NotificationType { COMMENT_ON_NOTE, - LIKE_ON_NOTE, REPORT, ALL, ; From 3df72202e3f2cb1b045b86e54f592c718f8684fa Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 14:02:27 +0900 Subject: [PATCH 06/28] =?UTF-8?q?[SCRUM-164]=20feat:=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=EB=B3=84=20=EC=95=8C=EB=A6=BC=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationQueryRepository.java | 6 +++ .../QueryDslNotificationQueryRepository.java | 42 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java index dba63a85..ae26f56d 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java @@ -1,4 +1,10 @@ package com.projectlyrics.server.domain.notification.repository; +import com.projectlyrics.server.domain.notification.domain.Notification; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + public interface NotificationQueryRepository { + + Slice findAllByReceiverId(Long receiverId, Long cursorId, Pageable pageable); } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java new file mode 100644 index 00000000..c775e7e8 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java @@ -0,0 +1,42 @@ +package com.projectlyrics.server.domain.notification.repository.impl; + +import com.projectlyrics.server.domain.common.util.QueryDslUtils; +import com.projectlyrics.server.domain.notification.domain.Notification; +import com.projectlyrics.server.domain.notification.repository.NotificationQueryRepository; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.projectlyrics.server.domain.notification.domain.QNotification.notification; + +@Repository +@RequiredArgsConstructor +public class QueryDslNotificationQueryRepository implements NotificationQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Slice findAllByReceiverId(Long receiverId, Long cursorId, Pageable pageable) { + List content = jpaQueryFactory + .selectFrom(notification) + .leftJoin(notification.sender).fetchJoin() + .leftJoin(notification.receiver).fetchJoin() + .leftJoin(notification.note).fetchJoin() + .leftJoin(notification.comment).fetchJoin() + .where( + notification.receiver.id.eq(receiverId), + QueryDslUtils.gtCursorId(cursorId, notification.id), + notification.deletedAt.isNull() + ) + .orderBy(notification.id.desc()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + return new SliceImpl<>(content, pageable, QueryDslUtils.checkIfHasNext(pageable, content)); + } +} From 301b67024b005922d6c9c24b530bf843c9dea686 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 14:02:43 +0900 Subject: [PATCH 07/28] =?UTF-8?q?[SCRUM-164]=20feat:=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=EB=B3=84=20=EC=95=8C=EB=A6=BC=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationCommandServiceTest.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java diff --git a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java new file mode 100644 index 00000000..b272f9ba --- /dev/null +++ b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java @@ -0,0 +1,104 @@ +package com.projectlyrics.server.domain.notification.service; + +import com.projectlyrics.server.domain.artist.entity.Artist; +import com.projectlyrics.server.domain.artist.repository.ArtistCommandRepository; +import com.projectlyrics.server.domain.comment.domain.Comment; +import com.projectlyrics.server.domain.comment.domain.CommentCreate; +import com.projectlyrics.server.domain.comment.dto.request.CommentCreateRequest; +import com.projectlyrics.server.domain.comment.repository.CommentCommandRepository; +import com.projectlyrics.server.domain.note.dto.request.NoteCreateRequest; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.note.entity.NoteBackground; +import com.projectlyrics.server.domain.note.entity.NoteCreate; +import com.projectlyrics.server.domain.note.entity.NoteStatus; +import com.projectlyrics.server.domain.note.repository.NoteCommandRepository; +import com.projectlyrics.server.domain.notification.domain.Notification; +import com.projectlyrics.server.domain.notification.domain.NotificationType; +import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.notification.repository.NotificationCommandRepository; +import com.projectlyrics.server.domain.notification.repository.NotificationQueryRepository; +import com.projectlyrics.server.domain.song.entity.Song; +import com.projectlyrics.server.domain.song.repository.SongCommandRepository; +import com.projectlyrics.server.domain.user.entity.User; +import com.projectlyrics.server.domain.user.repository.UserCommandRepository; +import com.projectlyrics.server.support.IntegrationTest; +import com.projectlyrics.server.support.fixture.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class NotificationCommandServiceTest extends IntegrationTest { + + @Autowired + UserCommandRepository userCommandRepository; + + @Autowired + ArtistCommandRepository artistCommandRepository; + + @Autowired + SongCommandRepository songCommandRepository; + + @Autowired + NoteCommandRepository noteCommandRepository; + + @Autowired + CommentCommandRepository commentCommandRepository; + + @Autowired + NotificationCommandRepository notificationCommandRepository; + + @Autowired + NotificationQueryRepository notificationQueryRepository; + + @Autowired + NotificationCommandService sut; + + private Comment comment; + private User user; + + @BeforeEach + void setUp() { + user = userCommandRepository.save(UserFixture.create()); + Artist artist = artistCommandRepository.save(ArtistFixture.create()); + Song song = songCommandRepository.save(SongFixture.create(artist)); + + NoteCreateRequest noteCreateRequest = new NoteCreateRequest( + "content", + "lyrics", + NoteBackground.DEFAULT, + NoteStatus.PUBLISHED, + song.getId() + ); + Note note = noteCommandRepository.save(Note.create(NoteCreate.from(noteCreateRequest, user, song))); + + comment = commentCommandRepository.save(Comment.create(CommentCreate.of("content", user, note))); + } + + @Test + void 댓글에_대한_알림을_생성한다() { + // given + CommentEvent commentEvent = CommentEvent.from(comment); + + // when + sut.createCommentNotification(commentEvent); + + // then + List result = notificationQueryRepository.findAllByReceiverId(user.getId(), null, PageRequest.ofSize(10)) + .getContent(); + + assertAll( + () -> assertThat(result.size()).isEqualTo(1), + () -> assertThat(result.get(0).getType()).isEqualTo(NotificationType.COMMENT_ON_NOTE), + () -> assertThat(result.get(0).getSender()).isEqualTo(comment.getWriter()), + () -> assertThat(result.get(0).getReceiver()).isEqualTo(comment.getNote().getPublisher()), + () -> assertThat(result.get(0).getNote()).isEqualTo(comment.getNote()), + () -> assertThat(result.get(0).getComment()).isEqualTo(comment) + ); + } +} \ No newline at end of file From 818c50c0a9b2b01c1ce573265bdaa87fa40062e6 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 14:04:24 +0900 Subject: [PATCH 08/28] =?UTF-8?q?[SCRUM-164]=20test:=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EC=BB=A4=EB=A7=A8=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationCommandServiceTest.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java diff --git a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java new file mode 100644 index 00000000..b272f9ba --- /dev/null +++ b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java @@ -0,0 +1,104 @@ +package com.projectlyrics.server.domain.notification.service; + +import com.projectlyrics.server.domain.artist.entity.Artist; +import com.projectlyrics.server.domain.artist.repository.ArtistCommandRepository; +import com.projectlyrics.server.domain.comment.domain.Comment; +import com.projectlyrics.server.domain.comment.domain.CommentCreate; +import com.projectlyrics.server.domain.comment.dto.request.CommentCreateRequest; +import com.projectlyrics.server.domain.comment.repository.CommentCommandRepository; +import com.projectlyrics.server.domain.note.dto.request.NoteCreateRequest; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.note.entity.NoteBackground; +import com.projectlyrics.server.domain.note.entity.NoteCreate; +import com.projectlyrics.server.domain.note.entity.NoteStatus; +import com.projectlyrics.server.domain.note.repository.NoteCommandRepository; +import com.projectlyrics.server.domain.notification.domain.Notification; +import com.projectlyrics.server.domain.notification.domain.NotificationType; +import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.notification.repository.NotificationCommandRepository; +import com.projectlyrics.server.domain.notification.repository.NotificationQueryRepository; +import com.projectlyrics.server.domain.song.entity.Song; +import com.projectlyrics.server.domain.song.repository.SongCommandRepository; +import com.projectlyrics.server.domain.user.entity.User; +import com.projectlyrics.server.domain.user.repository.UserCommandRepository; +import com.projectlyrics.server.support.IntegrationTest; +import com.projectlyrics.server.support.fixture.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class NotificationCommandServiceTest extends IntegrationTest { + + @Autowired + UserCommandRepository userCommandRepository; + + @Autowired + ArtistCommandRepository artistCommandRepository; + + @Autowired + SongCommandRepository songCommandRepository; + + @Autowired + NoteCommandRepository noteCommandRepository; + + @Autowired + CommentCommandRepository commentCommandRepository; + + @Autowired + NotificationCommandRepository notificationCommandRepository; + + @Autowired + NotificationQueryRepository notificationQueryRepository; + + @Autowired + NotificationCommandService sut; + + private Comment comment; + private User user; + + @BeforeEach + void setUp() { + user = userCommandRepository.save(UserFixture.create()); + Artist artist = artistCommandRepository.save(ArtistFixture.create()); + Song song = songCommandRepository.save(SongFixture.create(artist)); + + NoteCreateRequest noteCreateRequest = new NoteCreateRequest( + "content", + "lyrics", + NoteBackground.DEFAULT, + NoteStatus.PUBLISHED, + song.getId() + ); + Note note = noteCommandRepository.save(Note.create(NoteCreate.from(noteCreateRequest, user, song))); + + comment = commentCommandRepository.save(Comment.create(CommentCreate.of("content", user, note))); + } + + @Test + void 댓글에_대한_알림을_생성한다() { + // given + CommentEvent commentEvent = CommentEvent.from(comment); + + // when + sut.createCommentNotification(commentEvent); + + // then + List result = notificationQueryRepository.findAllByReceiverId(user.getId(), null, PageRequest.ofSize(10)) + .getContent(); + + assertAll( + () -> assertThat(result.size()).isEqualTo(1), + () -> assertThat(result.get(0).getType()).isEqualTo(NotificationType.COMMENT_ON_NOTE), + () -> assertThat(result.get(0).getSender()).isEqualTo(comment.getWriter()), + () -> assertThat(result.get(0).getReceiver()).isEqualTo(comment.getNote().getPublisher()), + () -> assertThat(result.get(0).getNote()).isEqualTo(comment.getNote()), + () -> assertThat(result.get(0).getComment()).isEqualTo(comment) + ); + } +} \ No newline at end of file From cf13b3750599fea632d049c946985f5a3b6e5c25 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 17:07:16 +0900 Subject: [PATCH 09/28] =?UTF-8?q?[SCRUM-164]=20refactor:=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=83=9D=EC=84=B1=EC=97=90=20fcmToken=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/dto/request/AuthSignUpRequest.java | 2 ++ .../server/domain/user/entity/User.java | 25 ++++++------------- .../server/domain/user/entity/UserCreate.java | 6 +++-- .../server/support/fixture/UserFixture.java | 11 +++++--- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/projectlyrics/server/domain/auth/dto/request/AuthSignUpRequest.java b/src/main/java/com/projectlyrics/server/domain/auth/dto/request/AuthSignUpRequest.java index e2303218..a02c77b8 100644 --- a/src/main/java/com/projectlyrics/server/domain/auth/dto/request/AuthSignUpRequest.java +++ b/src/main/java/com/projectlyrics/server/domain/auth/dto/request/AuthSignUpRequest.java @@ -33,6 +33,8 @@ public record AuthSignUpRequest( @Valid List terms + + // TODO: fcmToken 추가 ) { public record TermsInput( diff --git a/src/main/java/com/projectlyrics/server/domain/user/entity/User.java b/src/main/java/com/projectlyrics/server/domain/user/entity/User.java index 6f0be566..b565336b 100644 --- a/src/main/java/com/projectlyrics/server/domain/user/entity/User.java +++ b/src/main/java/com/projectlyrics/server/domain/user/entity/User.java @@ -84,9 +84,10 @@ private User( Role role, Gender gender, Integer birthYear, - List termsAgreements + List termsAgreements, + String fcmToken ) { - this(null, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, null); + this(null, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, fcmToken); } public static User withId( @@ -97,9 +98,10 @@ public static User withId( Role role, Gender gender, Integer birthYear, - List termsAgreements + List termsAgreements, + String fcmToken ) { - return new User(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, null); + return new User(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, fcmToken); } public static User create(UserCreate userCreate) { @@ -110,19 +112,8 @@ public static User create(UserCreate userCreate) { Role.USER, userCreate.gender(), userCreate.birthYear(), - userCreate.termsAgreements() + userCreate.termsAgreements(), + userCreate.fcmToken() ); } - - public static User of( - SocialInfo socialInfo, - String nickname, - ProfileCharacter profileCharacter, - Role role, - Gender gender, - Integer birthYear, - List termsAgreements - ) { - return new User(socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements); - } } diff --git a/src/main/java/com/projectlyrics/server/domain/user/entity/UserCreate.java b/src/main/java/com/projectlyrics/server/domain/user/entity/UserCreate.java index dba5120c..04fdab1b 100644 --- a/src/main/java/com/projectlyrics/server/domain/user/entity/UserCreate.java +++ b/src/main/java/com/projectlyrics/server/domain/user/entity/UserCreate.java @@ -11,7 +11,8 @@ public record UserCreate( ProfileCharacter profileCharacter, Gender gender, Integer birthYear, - List termsAgreements + List termsAgreements, + String fcmToken ) { public static UserCreate of(SocialInfo socialInfo, AuthSignUpRequest request) { @@ -25,7 +26,8 @@ public static UserCreate of(SocialInfo socialInfo, AuthSignUpRequest request) { request.profileCharacter(), request.gender(), Objects.nonNull(request.birthYear()) ? request.birthYear().getValue() : null, - termsList + termsList, + null ); } } diff --git a/src/test/java/com/projectlyrics/server/support/fixture/UserFixture.java b/src/test/java/com/projectlyrics/server/support/fixture/UserFixture.java index 5b26c58d..f9ad3a3a 100644 --- a/src/test/java/com/projectlyrics/server/support/fixture/UserFixture.java +++ b/src/test/java/com/projectlyrics/server/support/fixture/UserFixture.java @@ -21,8 +21,10 @@ public class UserFixture extends BaseFixture { private int birthYear = 1999; private Role role = Role.USER; private List termsAgreements = List.of(new TermsAgreements(true, "title", "agreement")); + private String fcmToken = "fcmToken"; - private UserFixture() {} + private UserFixture() { + } public static User create() { return User.withId( @@ -33,8 +35,9 @@ public static User create() { Role.USER, Gender.MALE, 1999, - List.of(new TermsAgreements(true, "title", "agreement") - )); + List.of(new TermsAgreements(true, "title", "agreement")), + "fcmToken" + ); } public static UserFixture builder() { @@ -42,7 +45,7 @@ public static UserFixture builder() { } public User build() { - return User.withId(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements); + return User.withId(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, fcmToken); } public UserFixture role(Role role) { From 2dd9bfb119fb2f58890b01f0a73ae05d10c7a98a Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 17:07:50 +0900 Subject: [PATCH 10/28] =?UTF-8?q?[SCRUM-164]=20refactor:=20FirebaseMessagi?= =?UTF-8?q?ng=EC=9D=98=20=EB=AA=A8=ED=82=B9=EC=9D=84=20=EC=9C=84=ED=95=B4?= =?UTF-8?q?=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=EB=A5=BC=20=EB=B9=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=93=B1=EB=A1=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationCommandService.java | 3 ++- .../server/global/configuration/FirebaseConfig.java | 8 ++++++++ .../service/NotificationCommandServiceTest.java | 10 ++++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java index 6d02c132..75990d36 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java @@ -20,6 +20,7 @@ public class NotificationCommandService { private final NotificationCommandRepository notificationCommandRepository; + private final FirebaseMessaging firebaseMessaging; @Async @EventListener @@ -30,7 +31,7 @@ public void createCommentNotification(CommentEvent event) { private void send(Notification notification) { try { - FirebaseMessaging.getInstance().send(notification.getMessage()); + firebaseMessaging.send(notification.getMessage()); } catch (FirebaseMessagingException e) { log.info(e.getMessage()); throw new FailedToSendNotificationException(); diff --git a/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java index 7aee7751..652a1c01 100644 --- a/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java +++ b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java @@ -3,8 +3,10 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.FileInputStream; @@ -28,4 +30,10 @@ public void init() { log.error("failed to initialize firebase", e); } } + + @Bean + public FirebaseMessaging firebaseMessaging() { + log.info("firebase messaging initialized"); + return FirebaseMessaging.getInstance(); + } } diff --git a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java index b272f9ba..d89f4ef1 100644 --- a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java +++ b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java @@ -1,10 +1,10 @@ package com.projectlyrics.server.domain.notification.service; +import com.google.firebase.messaging.FirebaseMessaging; import com.projectlyrics.server.domain.artist.entity.Artist; import com.projectlyrics.server.domain.artist.repository.ArtistCommandRepository; import com.projectlyrics.server.domain.comment.domain.Comment; import com.projectlyrics.server.domain.comment.domain.CommentCreate; -import com.projectlyrics.server.domain.comment.dto.request.CommentCreateRequest; import com.projectlyrics.server.domain.comment.repository.CommentCommandRepository; import com.projectlyrics.server.domain.note.dto.request.NoteCreateRequest; import com.projectlyrics.server.domain.note.entity.Note; @@ -26,12 +26,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.*; class NotificationCommandServiceTest extends IntegrationTest { @@ -56,6 +58,9 @@ class NotificationCommandServiceTest extends IntegrationTest { @Autowired NotificationQueryRepository notificationQueryRepository; + @MockBean + FirebaseMessaging firebaseMessaging; + @Autowired NotificationCommandService sut; @@ -81,9 +86,10 @@ void setUp() { } @Test - void 댓글에_대한_알림을_생성한다() { + void 댓글에_대한_알림을_저장한다() throws Exception { // given CommentEvent commentEvent = CommentEvent.from(comment); + when(firebaseMessaging.send(any())).thenReturn(null); // when sut.createCommentNotification(commentEvent); From ca2d23d8962001ae72998a464ae96044e1b85940 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 18:02:54 +0900 Subject: [PATCH 11/28] =?UTF-8?q?[SCRUM-164]=20feat:=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/NotificationController.java | 32 +++++++++++ .../PublicNotificationCreateRequest.java | 9 +++ .../PublicNotificationCreateResponse.java | 6 ++ .../notification/domain/Notification.java | 28 ++++++---- .../notification/domain/NotificationType.java | 2 +- .../domain/event/PublicEvent.java | 14 +++++ .../NotificationQueryRepository.java | 1 + .../QueryDslNotificationQueryRepository.java | 20 +++++++ .../service/NotificationCommandService.java | 46 ++++++++++++++++ .../user/repository/UserQueryRepository.java | 3 + .../impl/QueryDslUserQueryRepository.java | 31 ++++++++--- .../api/NotificationControllerTest.java | 55 +++++++++++++++++++ .../NotificationCommandServiceTest.java | 29 +++++++++- .../server/support/ControllerTest.java | 4 ++ 14 files changed, 259 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/api/NotificationController.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/api/dto/request/PublicNotificationCreateRequest.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/api/dto/response/PublicNotificationCreateResponse.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/domain/event/PublicEvent.java create mode 100644 src/test/java/com/projectlyrics/server/domain/notification/api/NotificationControllerTest.java diff --git a/src/main/java/com/projectlyrics/server/domain/notification/api/NotificationController.java b/src/main/java/com/projectlyrics/server/domain/notification/api/NotificationController.java new file mode 100644 index 00000000..b83e9245 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/api/NotificationController.java @@ -0,0 +1,32 @@ +package com.projectlyrics.server.domain.notification.api; + +import com.projectlyrics.server.domain.auth.authentication.AuthContext; +import com.projectlyrics.server.domain.auth.authentication.Authenticated; +import com.projectlyrics.server.domain.notification.api.dto.request.PublicNotificationCreateRequest; +import com.projectlyrics.server.domain.notification.api.dto.response.PublicNotificationCreateResponse; +import com.projectlyrics.server.domain.notification.service.NotificationCommandService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/notifications") +@RequiredArgsConstructor +public class NotificationController { + + public final NotificationCommandService notificationCommandService; + + @PostMapping("/public") + public ResponseEntity createPublicNotification( + @Authenticated AuthContext authContext, + @RequestBody @Valid PublicNotificationCreateRequest request + ) { + notificationCommandService.createPublicNotification(authContext.getId(), request.content()); + + return ResponseEntity.ok(new PublicNotificationCreateResponse(true)); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/api/dto/request/PublicNotificationCreateRequest.java b/src/main/java/com/projectlyrics/server/domain/notification/api/dto/request/PublicNotificationCreateRequest.java new file mode 100644 index 00000000..8acb94ed --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/api/dto/request/PublicNotificationCreateRequest.java @@ -0,0 +1,9 @@ +package com.projectlyrics.server.domain.notification.api.dto.request; + +import jakarta.validation.constraints.NotEmpty; + +public record PublicNotificationCreateRequest( + @NotEmpty(message = "알림 내용이 비어 있습니다.") + String content +) { +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/api/dto/response/PublicNotificationCreateResponse.java b/src/main/java/com/projectlyrics/server/domain/notification/api/dto/response/PublicNotificationCreateResponse.java new file mode 100644 index 00000000..20775854 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/api/dto/response/PublicNotificationCreateResponse.java @@ -0,0 +1,6 @@ +package com.projectlyrics.server.domain.notification.api.dto.response; + +public record PublicNotificationCreateResponse( + boolean status +) { +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java index 0ebf1ba5..be5a14b1 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java @@ -5,6 +5,7 @@ import com.projectlyrics.server.domain.common.entity.BaseEntity; import com.projectlyrics.server.domain.note.entity.Note; import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.notification.domain.event.PublicEvent; import com.projectlyrics.server.domain.user.entity.User; import jakarta.persistence.*; import lombok.AccessLevel; @@ -24,6 +25,7 @@ public class Notification extends BaseEntity { private Long id; private NotificationType type; + private String content; @ManyToOne(fetch = FetchType.LAZY) private User sender; @@ -38,6 +40,7 @@ public class Notification extends BaseEntity { private Notification( Long id, NotificationType type, + String content, User sender, User receiver, Note note, @@ -46,24 +49,17 @@ private Notification( this.id = id; this.type = type; this.sender = sender; + this.content = content; this.receiver = receiver; this.note = note; this.comment = comment; } - private Notification( - NotificationType type, - User sender, - User receiver, - Note note, - Comment comment - ) { - this(null, type, sender, receiver, note, comment); - } - public static Notification create(CommentEvent event) { return new Notification( + null, NotificationType.COMMENT_ON_NOTE, + null, event.sender(), event.receiver(), event.note(), @@ -71,6 +67,18 @@ public static Notification create(CommentEvent event) { ); } + public static Notification create(PublicEvent event) { + return new Notification( + null, + NotificationType.PUBLIC, + event.content(), + event.sender(), + event.receiver(), + null, + null + ); + } + public Message getMessage() { Message.Builder builder = Message.builder() .setToken(receiver.getFcmToken()); diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java index 73c8b422..5ca819eb 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java @@ -3,6 +3,6 @@ public enum NotificationType { COMMENT_ON_NOTE, REPORT, - ALL, + PUBLIC, ; } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/event/PublicEvent.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/event/PublicEvent.java new file mode 100644 index 00000000..cff3f667 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/event/PublicEvent.java @@ -0,0 +1,14 @@ +package com.projectlyrics.server.domain.notification.domain.event; + +import com.projectlyrics.server.domain.user.entity.User; + +public record PublicEvent( + User sender, + User receiver, + String content +) { + + public static PublicEvent of(String content, User sender, User receiver) { + return new PublicEvent(sender, receiver, content); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java index ae26f56d..10a2e298 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java @@ -7,4 +7,5 @@ public interface NotificationQueryRepository { Slice findAllByReceiverId(Long receiverId, Long cursorId, Pageable pageable); + Slice findAllBySenderId(Long senderId, Long cursorId, Pageable pageable); } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java index c775e7e8..cb72cd20 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java @@ -39,4 +39,24 @@ public Slice findAllByReceiverId(Long receiverId, Long cursorId, P return new SliceImpl<>(content, pageable, QueryDslUtils.checkIfHasNext(pageable, content)); } + + @Override + public Slice findAllBySenderId(Long senderId, Long cursorId, Pageable pageable) { + List content = jpaQueryFactory + .selectFrom(notification) + .leftJoin(notification.sender).fetchJoin() + .leftJoin(notification.receiver).fetchJoin() + .leftJoin(notification.note).fetchJoin() + .leftJoin(notification.comment).fetchJoin() + .where( + notification.sender.id.eq(senderId), + QueryDslUtils.gtCursorId(cursorId, notification.id), + notification.deletedAt.isNull() + ) + .orderBy(notification.id.desc()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + return new SliceImpl<>(content, pageable, QueryDslUtils.checkIfHasNext(pageable, content)); + } } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java index 75990d36..ed13c6bd 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java @@ -2,10 +2,15 @@ import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.MulticastMessage; import com.projectlyrics.server.domain.notification.domain.Notification; import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.notification.domain.event.PublicEvent; import com.projectlyrics.server.domain.notification.exception.FailedToSendNotificationException; import com.projectlyrics.server.domain.notification.repository.NotificationCommandRepository; +import com.projectlyrics.server.domain.user.entity.User; +import com.projectlyrics.server.domain.user.exception.UserNotFoundException; +import com.projectlyrics.server.domain.user.repository.UserQueryRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; @@ -13,6 +18,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Slf4j @Service @Transactional @@ -20,6 +27,7 @@ public class NotificationCommandService { private final NotificationCommandRepository notificationCommandRepository; + private final UserQueryRepository userQueryRepository; private final FirebaseMessaging firebaseMessaging; @Async @@ -37,4 +45,42 @@ private void send(Notification notification) { throw new FailedToSendNotificationException(); } } + + public void createPublicNotification(Long adminId, String content) { + // TODO: adminId 확인하는 로직 추가 + User admin = userQueryRepository.findById(adminId) + .orElseThrow(UserNotFoundException::new); + List receivers = userQueryRepository.findAll(); + + List notifications = receivers.stream() + .map(receiver -> Notification.create(PublicEvent.of(content, admin, receiver))) + .toList(); + notificationCommandRepository.saveAll(notifications); + + sendAll(notifications, content); + } + + private void sendAll(List notifications, String content) { + int batchSize = notifications.size() / 500 + 1; + + for (int i = 0; i < batchSize; i++) { + List fcmTokens = notifications.subList(i * 500, Math.min((i + 1) * 500, notifications.size())) + .stream() + .map(Notification::getReceiver) + .map(User::getFcmToken) + .toList(); + + MulticastMessage message = MulticastMessage.builder() + .addAllTokens(fcmTokens) + .putData("content", content) + .build(); + + try { + firebaseMessaging.sendEachForMulticast(message); + } catch (FirebaseMessagingException e) { + log.info(e.getMessage()); + throw new FailedToSendNotificationException(); + } + } + } } diff --git a/src/main/java/com/projectlyrics/server/domain/user/repository/UserQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/user/repository/UserQueryRepository.java index b5494b24..29ac7c07 100644 --- a/src/main/java/com/projectlyrics/server/domain/user/repository/UserQueryRepository.java +++ b/src/main/java/com/projectlyrics/server/domain/user/repository/UserQueryRepository.java @@ -4,6 +4,7 @@ import com.projectlyrics.server.domain.user.entity.SocialInfo; import com.projectlyrics.server.domain.user.entity.User; +import java.util.List; import java.util.Optional; public interface UserQueryRepository { @@ -12,5 +13,7 @@ public interface UserQueryRepository { Optional findById(Long id); + List findAll(); + boolean existsBySocialInfo(SocialInfo socialInfo); } diff --git a/src/main/java/com/projectlyrics/server/domain/user/repository/impl/QueryDslUserQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/user/repository/impl/QueryDslUserQueryRepository.java index 9292460b..156d4c02 100644 --- a/src/main/java/com/projectlyrics/server/domain/user/repository/impl/QueryDslUserQueryRepository.java +++ b/src/main/java/com/projectlyrics/server/domain/user/repository/impl/QueryDslUserQueryRepository.java @@ -7,11 +7,14 @@ import com.projectlyrics.server.domain.user.repository.UserQueryRepository; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import static com.projectlyrics.server.domain.user.entity.QUser.user; + @Repository @RequiredArgsConstructor public class QueryDslUserQueryRepository implements UserQueryRepository { @@ -22,11 +25,11 @@ public class QueryDslUserQueryRepository implements UserQueryRepository { public Optional findBySocialIdAndAuthProvider(String socialId, AuthProvider authProvider) { return Optional.ofNullable( jpaQueryFactory - .selectFrom(QUser.user) + .selectFrom(user) .where( - QUser.user.socialInfo.socialId.eq(socialId), - QUser.user.socialInfo.authProvider.eq(authProvider), - QUser.user.deletedAt.isNull() + user.socialInfo.socialId.eq(socialId), + user.socialInfo.authProvider.eq(authProvider), + user.deletedAt.isNull() ) .fetchOne() ); @@ -36,21 +39,31 @@ public Optional findBySocialIdAndAuthProvider(String socialId, AuthProvide public Optional findById(Long id) { return Optional.ofNullable( jpaQueryFactory - .selectFrom(QUser.user) + .selectFrom(user) .where( - QUser.user.id.eq(id), - QUser.user.deletedAt.isNull() + user.id.eq(id), + user.deletedAt.isNull() ) .fetchOne() ); } + @Override + public List findAll() { + return jpaQueryFactory + .selectFrom(user) + .where( + user.deletedAt.isNull() + ) + .fetch(); + } + @Override public boolean existsBySocialInfo(SocialInfo socialInfo) { return Optional.ofNullable( jpaQueryFactory - .selectFrom(QUser.user) - .where(QUser.user.socialInfo.eq(socialInfo)) + .selectFrom(user) + .where(user.socialInfo.eq(socialInfo)) .fetchOne() ).isPresent(); } diff --git a/src/test/java/com/projectlyrics/server/domain/notification/api/NotificationControllerTest.java b/src/test/java/com/projectlyrics/server/domain/notification/api/NotificationControllerTest.java new file mode 100644 index 00000000..81af4510 --- /dev/null +++ b/src/test/java/com/projectlyrics/server/domain/notification/api/NotificationControllerTest.java @@ -0,0 +1,55 @@ +package com.projectlyrics.server.domain.notification.api; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.projectlyrics.server.domain.notification.api.dto.request.PublicNotificationCreateRequest; +import com.projectlyrics.server.support.RestDocsTest; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.JsonFieldType; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class NotificationControllerTest extends RestDocsTest { + + @Test + void 전체_알림을_등록하면_결과_상태값과_200응답을_해야_한다() throws Exception { + // given + PublicNotificationCreateRequest request = new PublicNotificationCreateRequest("content"); + + // when, then + mockMvc.perform(post("/api/v1/notifications/public") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(getPublicNotificationCreateDocument()); + } + + private RestDocumentationResultHandler getPublicNotificationCreateDocument() { + return restDocs.document( + resource(ResourceSnippetParameters.builder() + .tag("Notification API") + .summary("전체 알림 등록 API") + .requestHeaders(getAuthorizationHeader()) + .requestFields( + fieldWithPath("content").type(JsonFieldType.STRING) + .description("알림 내용") + ) + .requestSchema(Schema.schema("Create Public Notification Request")) + .responseFields( + fieldWithPath("status").type(JsonFieldType.BOOLEAN) + .description("요청 처리 상태") + ) + .responseSchema(Schema.schema("Create Public Notification Response")) + .build()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java index d89f4ef1..b681512a 100644 --- a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java +++ b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java @@ -29,6 +29,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -66,10 +67,15 @@ class NotificationCommandServiceTest extends IntegrationTest { private Comment comment; private User user; + private List users = new ArrayList<>(); @BeforeEach void setUp() { - user = userCommandRepository.save(UserFixture.create()); + for (int i = 0; i < 100; i++) { + users.add(userCommandRepository.save(UserFixture.create())); + } + user = users.getFirst(); + Artist artist = artistCommandRepository.save(ArtistFixture.create()); Song song = songCommandRepository.save(SongFixture.create(artist)); @@ -107,4 +113,25 @@ void setUp() { () -> assertThat(result.get(0).getComment()).isEqualTo(comment) ); } + + @Test + void 공개_알림을_저장한다() throws Exception { + // given + when(firebaseMessaging.sendEachForMulticast(any())).thenReturn(null); + String content = "content"; + + // when + sut.createPublicNotification(user.getId(), content); + + // then + List result = notificationQueryRepository.findAllBySenderId(user.getId(), null, PageRequest.ofSize(10)) + .getContent(); + + assertAll( + () -> assertThat(result.size()).isEqualTo(10), + () -> assertThat(result.stream().allMatch(notification -> notification.getType().equals(NotificationType.PUBLIC))).isTrue(), + () -> assertThat(result.stream().allMatch(notification -> notification.getSender().getId().equals(user.getId()))).isTrue(), + () -> assertThat(result.stream().allMatch(notification -> notification.getContent().equalsIgnoreCase(content))).isTrue() + ); + } } \ No newline at end of file diff --git a/src/test/java/com/projectlyrics/server/support/ControllerTest.java b/src/test/java/com/projectlyrics/server/support/ControllerTest.java index e1325d94..c7bec69b 100644 --- a/src/test/java/com/projectlyrics/server/support/ControllerTest.java +++ b/src/test/java/com/projectlyrics/server/support/ControllerTest.java @@ -18,6 +18,7 @@ import com.projectlyrics.server.domain.like.service.LikeQueryService; import com.projectlyrics.server.domain.note.service.NoteCommandService; import com.projectlyrics.server.domain.note.service.NoteQueryService; +import com.projectlyrics.server.domain.notification.service.NotificationCommandService; import com.projectlyrics.server.domain.song.service.SongQueryService; import com.projectlyrics.server.global.configuration.ClockConfig; import org.junit.jupiter.api.BeforeEach; @@ -85,6 +86,9 @@ public abstract class ControllerTest { @MockBean protected BookmarkCommandService bookmarkCommandService; + @MockBean + protected NotificationCommandService notificationCommandService; + public String accessToken; public String refreshToken; From 3796521cfd3d4fd867e166af3a2528ff29ff7ec0 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 18:34:54 +0900 Subject: [PATCH 12/28] =?UTF-8?q?[SCRUM-164]=20fix:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=8A=88=ED=8A=B8=20=EC=8B=A4=ED=96=89=20=EC=8B=9C?= =?UTF-8?q?=20FirebaseApp=20=EC=A4=91=EB=B3=B5=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/global/configuration/FirebaseConfig.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java index 652a1c01..30cb5932 100644 --- a/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java +++ b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java @@ -19,13 +19,15 @@ public class FirebaseConfig { @PostConstruct public void init() { try { - FileInputStream key = new FileInputStream("src/main/resources/firebase-key.json"); + if (FirebaseApp.getApps().isEmpty()) { + FileInputStream key = new FileInputStream("src/main/resources/firebase-key.json"); - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(GoogleCredentials.fromStream(key)) - .build(); + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(key)) + .build(); - FirebaseApp.initializeApp(options); + FirebaseApp.initializeApp(options); + } } catch (IOException e) { log.error("failed to initialize firebase", e); } From 16d0cef0f20c4ce9429fe107e912080258d1f348 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Tue, 27 Aug 2024 00:52:51 +0900 Subject: [PATCH 13/28] =?UTF-8?q?[SCRUM-164]=20settings:=20Test=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=82=B4=20firebase=20key=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/Test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 3a090aba..007ab524 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -25,6 +25,14 @@ jobs: with: redis-version: 6 + - name: overwrite firebase-key.json + if: | + contains(github.ref, 'dev') + run: | + mkdir -p ./src/main/resources + echo "${{ secrets.FIREBASE_KEY }}" > src/main/resources/firebase-key.json + shell: bash + - name: Test with Gradle run: | ./gradlew test From 949770bc65257f15c4f866cf61c21e8cf216847a Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 04:02:00 +0900 Subject: [PATCH 14/28] =?UTF-8?q?[SCRUM-164]=20feat:=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EA=B8=B0=EB=B3=B8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=B0=8F?= =?UTF-8?q?=20=EB=8C=93=EA=B8=80=20=EC=9E=91=EC=84=B1=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../service/CommentCommandService.java | 7 +- .../domain/common/message/ErrorCode.java | 3 + .../notification/domain/Notification.java | 86 +++++++++++++++++++ .../notification/domain/NotificationType.java | 9 ++ .../domain/event/CommentEvent.java | 22 +++++ .../UnknownNotificationException.java | 11 +++ .../NotificationCommandRepository.java | 9 ++ .../NotificationQueryRepository.java | 4 + .../service/NotificationCommandService.java | 25 ++++++ .../server/domain/user/entity/User.java | 10 ++- .../global/configuration/AsyncConfig.java | 26 ++++++ .../global/configuration/FirebaseConfig.java | 31 +++++++ 13 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/domain/event/CommentEvent.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/exception/UnknownNotificationException.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationCommandRepository.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java create mode 100644 src/main/java/com/projectlyrics/server/global/configuration/AsyncConfig.java create mode 100644 src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java diff --git a/build.gradle b/build.gradle index 43b68c3b..16e07cab 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,9 @@ dependencies { // redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // firebase cloud messaging + implementation 'com.google.firebase:firebase-admin:9.2.0' } swaggerSources { diff --git a/src/main/java/com/projectlyrics/server/domain/comment/service/CommentCommandService.java b/src/main/java/com/projectlyrics/server/domain/comment/service/CommentCommandService.java index e127827a..a79df3f7 100644 --- a/src/main/java/com/projectlyrics/server/domain/comment/service/CommentCommandService.java +++ b/src/main/java/com/projectlyrics/server/domain/comment/service/CommentCommandService.java @@ -12,10 +12,12 @@ import com.projectlyrics.server.domain.note.entity.Note; import com.projectlyrics.server.domain.note.exception.NoteNotFoundException; import com.projectlyrics.server.domain.note.repository.NoteQueryRepository; +import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; import com.projectlyrics.server.domain.user.entity.User; import com.projectlyrics.server.domain.user.exception.UserNotFoundException; import com.projectlyrics.server.domain.user.repository.UserQueryRepository; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,6 +32,7 @@ public class CommentCommandService { private final CommentQueryRepository commentQueryRepository; private final UserQueryRepository userQueryRepository; private final NoteQueryRepository noteQueryRepository; + private final ApplicationEventPublisher eventPublisher; public Comment create(CommentCreateRequest request, Long writerId) { User writer = userQueryRepository.findById(writerId) @@ -37,7 +40,9 @@ public Comment create(CommentCreateRequest request, Long writerId) { Note note = noteQueryRepository.findById(request.noteId()) .orElseThrow(NoteNotFoundException::new); - return commentCommandRepository.save(Comment.create(CommentCreate.of(request.content(), writer, note))); + Comment comment = Comment.create(CommentCreate.of(request.content(), writer, note)); + eventPublisher.publishEvent(CommentEvent.from(comment)); + return commentCommandRepository.save(comment); } public Comment update(CommentUpdateRequest request, Long commentId, Long writerId) { diff --git a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java index 2f53163e..e34946f1 100644 --- a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java +++ b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java @@ -71,6 +71,9 @@ public enum ErrorCode { // Bookmark BOOKMARK_NOT_FOUND(HttpStatus.NOT_FOUND, "10000", "해당 북마크를 조회할 수 없습니다."), BOOKMARK_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "10001", "이미 북마크를 추가한 상태입니다."), + + // Notification + UNKNOWN_NOTIFICATION_TYPE(HttpStatus.BAD_REQUEST, "11000", "알 수 없는 알림 타입입니다."), ; private final HttpStatus responseStatus; diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java new file mode 100644 index 00000000..697adc14 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java @@ -0,0 +1,86 @@ +package com.projectlyrics.server.domain.notification.domain; + +import com.google.firebase.messaging.Message; +import com.projectlyrics.server.domain.comment.domain.Comment; +import com.projectlyrics.server.domain.common.entity.BaseEntity; +import com.projectlyrics.server.domain.like.domain.Like; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "notifications") +@EqualsAndHashCode(of = "id", callSuper = false) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Notification extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private NotificationType type; + + @ManyToOne(fetch = FetchType.LAZY) + private User sender; + @ManyToOne(fetch = FetchType.LAZY) + private User receiver; + + @ManyToOne(fetch = FetchType.LAZY) + private Note note; + @ManyToOne(fetch = FetchType.LAZY) + private Comment comment; + @ManyToOne(fetch = FetchType.LAZY) + private Like like; + + private Notification( + Long id, + NotificationType type, + User sender, + User receiver, + Note note, + Comment comment, + Like like + ) { + this.id = id; + this.type = type; + this.sender = sender; + this.receiver = receiver; + this.note = note; + this.comment = comment; + this.like = like; + } + + private Notification( + NotificationType type, + User sender, + User receiver, + Note note, + Comment comment, + Like like + ) { + this(null, type, sender, receiver, note, comment, like); + } + + public static Notification create(CommentEvent event) { + return new Notification( + NotificationType.COMMENT_ON_NOTE, + event.sender(), + event.receiver(), + event.note(), + event.comment(), + null + ); + } + + public Message getMessage() { + return Message.builder() + .setToken(receiver.getFcmToken()) + .build(); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java new file mode 100644 index 00000000..bfcd1fad --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java @@ -0,0 +1,9 @@ +package com.projectlyrics.server.domain.notification.domain; + +public enum NotificationType { + COMMENT_ON_NOTE, + LIKE_ON_NOTE, + REPORT, + ALL, + ; +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/event/CommentEvent.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/event/CommentEvent.java new file mode 100644 index 00000000..497f516f --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/event/CommentEvent.java @@ -0,0 +1,22 @@ +package com.projectlyrics.server.domain.notification.domain.event; + +import com.projectlyrics.server.domain.comment.domain.Comment; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.user.entity.User; + +public record CommentEvent( + User sender, + User receiver, + Note note, + Comment comment +) { + + public static CommentEvent from(Comment comment) { + return new CommentEvent( + comment.getWriter(), + comment.getNote().getPublisher(), + comment.getNote(), + comment + ); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/exception/UnknownNotificationException.java b/src/main/java/com/projectlyrics/server/domain/notification/exception/UnknownNotificationException.java new file mode 100644 index 00000000..5c640372 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/exception/UnknownNotificationException.java @@ -0,0 +1,11 @@ +package com.projectlyrics.server.domain.notification.exception; + +import com.projectlyrics.server.domain.common.message.ErrorCode; +import com.projectlyrics.server.global.exception.FeelinException; + +public class UnknownNotificationException extends FeelinException { + + public UnknownNotificationException() { + super(ErrorCode.UNKNOWN_NOTIFICATION_TYPE); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationCommandRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationCommandRepository.java new file mode 100644 index 00000000..19cfe5bf --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationCommandRepository.java @@ -0,0 +1,9 @@ +package com.projectlyrics.server.domain.notification.repository; + +import com.projectlyrics.server.domain.notification.domain.Notification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NotificationCommandRepository extends JpaRepository { +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java new file mode 100644 index 00000000..dba63a85 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java @@ -0,0 +1,4 @@ +package com.projectlyrics.server.domain.notification.repository; + +public interface NotificationQueryRepository { +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java new file mode 100644 index 00000000..3e13bd90 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java @@ -0,0 +1,25 @@ +package com.projectlyrics.server.domain.notification.service; + +import com.projectlyrics.server.domain.notification.domain.Notification; +import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.notification.repository.NotificationCommandRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class NotificationCommandService { + + private final NotificationCommandRepository notificationCommandRepository; + + @Async + @EventListener + public void createCommentNotification(CommentEvent event) { + Notification notification = notificationCommandRepository.save(Notification.create(event)); + + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/user/entity/User.java b/src/main/java/com/projectlyrics/server/domain/user/entity/User.java index 6fa9a5d1..6f0be566 100644 --- a/src/main/java/com/projectlyrics/server/domain/user/entity/User.java +++ b/src/main/java/com/projectlyrics/server/domain/user/entity/User.java @@ -50,6 +50,8 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List termsAgreements; + private String fcmToken; + private User( Long id, SocialInfo socialInfo, @@ -58,7 +60,8 @@ private User( Role role, Gender gender, Integer birthYear, - List termsAgreements + List termsAgreements, + String fcmToken ) { checkNull(socialInfo); checkNull(termsAgreements); @@ -71,6 +74,7 @@ private User( this.info = new UserMetaInfo(gender, birthYear); this.termsAgreements = termsAgreements; termsAgreements.forEach(terms -> terms.setUser(this)); + this.fcmToken = fcmToken; } private User( @@ -82,7 +86,7 @@ private User( Integer birthYear, List termsAgreements ) { - this(null, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements); + this(null, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, null); } public static User withId( @@ -95,7 +99,7 @@ public static User withId( Integer birthYear, List termsAgreements ) { - return new User(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements); + return new User(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, null); } public static User create(UserCreate userCreate) { diff --git a/src/main/java/com/projectlyrics/server/global/configuration/AsyncConfig.java b/src/main/java/com/projectlyrics/server/global/configuration/AsyncConfig.java new file mode 100644 index 00000000..2d1e44fd --- /dev/null +++ b/src/main/java/com/projectlyrics/server/global/configuration/AsyncConfig.java @@ -0,0 +1,26 @@ +package com.projectlyrics.server.global.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@EnableAsync +@Configuration +public class AsyncConfig { + + @Bean + public Executor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + + executor.setCorePoolSize(3); + executor.setMaxPoolSize(30); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("async-executor-"); + + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java new file mode 100644 index 00000000..7aee7751 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java @@ -0,0 +1,31 @@ +package com.projectlyrics.server.global.configuration; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; + +import java.io.FileInputStream; +import java.io.IOException; + +@Slf4j +@Configuration +public class FirebaseConfig { + + @PostConstruct + public void init() { + try { + FileInputStream key = new FileInputStream("src/main/resources/firebase-key.json"); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(key)) + .build(); + + FirebaseApp.initializeApp(options); + } catch (IOException e) { + log.error("failed to initialize firebase", e); + } + } +} From 9800d6d01f888ab7ff873a8bae58ab1d3b622618 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 12:54:20 +0900 Subject: [PATCH 15/28] =?UTF-8?q?[SCRUM-164]=20feat:=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A0=84=EC=86=A1?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/message/ErrorCode.java | 1 + .../notification/domain/Notification.java | 18 +++++++++++++++--- .../FailedToSendNotificationException.java | 11 +++++++++++ .../service/NotificationCommandService.java | 14 ++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/exception/FailedToSendNotificationException.java diff --git a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java index e34946f1..1c2342de 100644 --- a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java +++ b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java @@ -74,6 +74,7 @@ public enum ErrorCode { // Notification UNKNOWN_NOTIFICATION_TYPE(HttpStatus.BAD_REQUEST, "11000", "알 수 없는 알림 타입입니다."), + NOTIFICATION_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "11001", "알림 전송에 실패했습니다."), ; private final HttpStatus responseStatus; diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java index 697adc14..ccde6d2d 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java @@ -79,8 +79,20 @@ public static Notification create(CommentEvent event) { } public Message getMessage() { - return Message.builder() - .setToken(receiver.getFcmToken()) - .build(); + Message.Builder builder = Message.builder() + .setToken(receiver.getFcmToken()); + + switch (type) { + case COMMENT_ON_NOTE: + return builder + .putData("type", type.name()) + .putData("senderId", sender.getId().toString()) + .putData("senderNickname", sender.getNickname().getValue()) + .putData("noteId", note.getId().toString()) + .putData("noteTitle", note.getContent()) + .build(); + default: + throw new IllegalArgumentException("Invalid notification type"); + } } } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/exception/FailedToSendNotificationException.java b/src/main/java/com/projectlyrics/server/domain/notification/exception/FailedToSendNotificationException.java new file mode 100644 index 00000000..7f9f6cf6 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/exception/FailedToSendNotificationException.java @@ -0,0 +1,11 @@ +package com.projectlyrics.server.domain.notification.exception; + +import com.projectlyrics.server.domain.common.message.ErrorCode; +import com.projectlyrics.server.global.exception.FeelinException; + +public class FailedToSendNotificationException extends FeelinException { + + public FailedToSendNotificationException() { + super(ErrorCode.NOTIFICATION_SEND_FAILED); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java index 3e13bd90..6d02c132 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java @@ -1,14 +1,19 @@ package com.projectlyrics.server.domain.notification.service; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; import com.projectlyrics.server.domain.notification.domain.Notification; import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.notification.exception.FailedToSendNotificationException; import com.projectlyrics.server.domain.notification.repository.NotificationCommandRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @Transactional @RequiredArgsConstructor @@ -20,6 +25,15 @@ public class NotificationCommandService { @EventListener public void createCommentNotification(CommentEvent event) { Notification notification = notificationCommandRepository.save(Notification.create(event)); + send(notification); + } + private void send(Notification notification) { + try { + FirebaseMessaging.getInstance().send(notification.getMessage()); + } catch (FirebaseMessagingException e) { + log.info(e.getMessage()); + throw new FailedToSendNotificationException(); + } } } From d63c7bd493283bec99504ecade3812bd519e53a3 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 12:55:18 +0900 Subject: [PATCH 16/28] =?UTF-8?q?[SCRUM-164]=20test:=20=EB=8C=93=EA=B8=80?= =?UTF-8?q?=20=EC=B8=A1=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=ED=96=89?= =?UTF-8?q?=20=EC=97=AC=EB=B6=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationCommandServiceTest.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java diff --git a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java new file mode 100644 index 00000000..110c63f2 --- /dev/null +++ b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java @@ -0,0 +1,61 @@ +package com.projectlyrics.server.domain.notification.service; + +import com.projectlyrics.server.domain.comment.domain.Comment; +import com.projectlyrics.server.domain.comment.dto.request.CommentCreateRequest; +import com.projectlyrics.server.domain.comment.repository.CommentCommandRepository; +import com.projectlyrics.server.domain.comment.repository.CommentQueryRepository; +import com.projectlyrics.server.domain.comment.service.CommentCommandService; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.note.repository.NoteQueryRepository; +import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.user.entity.User; +import com.projectlyrics.server.domain.user.repository.UserQueryRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class NotificationCommandServiceTest { + + @Mock + private CommentCommandRepository commentCommandRepository; + + @Mock + private CommentQueryRepository commentQueryRepository; + + @Mock + private UserQueryRepository userQueryRepository; + + @Mock + private NoteQueryRepository noteQueryRepository; + + @Mock + private ApplicationEventPublisher eventPublisher; + + @InjectMocks + private CommentCommandService commentCommandService; + + @Test + void 댓글을_저장할_때_이벤트가_발행된다() { + // given + CommentCreateRequest request = new CommentCreateRequest("댓글 내용", 1L); + when(userQueryRepository.findById(anyLong())).thenReturn(Optional.of(mock(User.class))); + when(noteQueryRepository.findById(anyLong())).thenReturn(Optional.of(mock(Note.class))); + when(commentCommandRepository.save(any(Comment.class))).thenReturn(mock(Comment.class)); + + // when + commentCommandService.create(request, 1L); + + // then + verify(eventPublisher).publishEvent(any(CommentEvent.class)); + } +} \ No newline at end of file From 3a2bcc5d5a0de9a92a26a98e6b7109665d7e8f52 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 13:28:00 +0900 Subject: [PATCH 17/28] =?UTF-8?q?[SCRUM-164]=20test:=20CommentCommandServi?= =?UTF-8?q?ceMockTest=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CommentCommandServiceMockTest.java} | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename src/test/java/com/projectlyrics/server/domain/{notification/service/NotificationCommandServiceTest.java => comment/service/CommentCommandServiceMockTest.java} (92%) diff --git a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/comment/service/CommentCommandServiceMockTest.java similarity index 92% rename from src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java rename to src/test/java/com/projectlyrics/server/domain/comment/service/CommentCommandServiceMockTest.java index 110c63f2..bba049d6 100644 --- a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java +++ b/src/test/java/com/projectlyrics/server/domain/comment/service/CommentCommandServiceMockTest.java @@ -1,10 +1,9 @@ -package com.projectlyrics.server.domain.notification.service; +package com.projectlyrics.server.domain.comment.service; import com.projectlyrics.server.domain.comment.domain.Comment; import com.projectlyrics.server.domain.comment.dto.request.CommentCreateRequest; import com.projectlyrics.server.domain.comment.repository.CommentCommandRepository; import com.projectlyrics.server.domain.comment.repository.CommentQueryRepository; -import com.projectlyrics.server.domain.comment.service.CommentCommandService; import com.projectlyrics.server.domain.note.entity.Note; import com.projectlyrics.server.domain.note.repository.NoteQueryRepository; import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; @@ -24,7 +23,7 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -class NotificationCommandServiceTest { +class CommentCommandServiceMockTest { @Mock private CommentCommandRepository commentCommandRepository; From 1865c3c17ed09b60e8d7503f6b7a33b258985fa5 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 13:28:15 +0900 Subject: [PATCH 18/28] =?UTF-8?q?[SCRUM-164]=20refactor:=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EC=95=8C=EB=A6=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/notification/domain/Notification.java | 15 ++++----------- .../notification/domain/NotificationType.java | 1 - 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java index ccde6d2d..0ebf1ba5 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java @@ -3,7 +3,6 @@ import com.google.firebase.messaging.Message; import com.projectlyrics.server.domain.comment.domain.Comment; import com.projectlyrics.server.domain.common.entity.BaseEntity; -import com.projectlyrics.server.domain.like.domain.Like; import com.projectlyrics.server.domain.note.entity.Note; import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; import com.projectlyrics.server.domain.user.entity.User; @@ -35,8 +34,6 @@ public class Notification extends BaseEntity { private Note note; @ManyToOne(fetch = FetchType.LAZY) private Comment comment; - @ManyToOne(fetch = FetchType.LAZY) - private Like like; private Notification( Long id, @@ -44,8 +41,7 @@ private Notification( User sender, User receiver, Note note, - Comment comment, - Like like + Comment comment ) { this.id = id; this.type = type; @@ -53,7 +49,6 @@ private Notification( this.receiver = receiver; this.note = note; this.comment = comment; - this.like = like; } private Notification( @@ -61,10 +56,9 @@ private Notification( User sender, User receiver, Note note, - Comment comment, - Like like + Comment comment ) { - this(null, type, sender, receiver, note, comment, like); + this(null, type, sender, receiver, note, comment); } public static Notification create(CommentEvent event) { @@ -73,8 +67,7 @@ public static Notification create(CommentEvent event) { event.sender(), event.receiver(), event.note(), - event.comment(), - null + event.comment() ); } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java index bfcd1fad..73c8b422 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java @@ -2,7 +2,6 @@ public enum NotificationType { COMMENT_ON_NOTE, - LIKE_ON_NOTE, REPORT, ALL, ; From 9dbd9c578c4557373560eff77728ecac9409dd20 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 14:02:27 +0900 Subject: [PATCH 19/28] =?UTF-8?q?[SCRUM-164]=20feat:=20=EC=88=98=EC=8B=A0?= =?UTF-8?q?=EC=9E=90=EB=B3=84=20=EC=95=8C=EB=A6=BC=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationQueryRepository.java | 6 +++ .../QueryDslNotificationQueryRepository.java | 42 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java index dba63a85..ae26f56d 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java @@ -1,4 +1,10 @@ package com.projectlyrics.server.domain.notification.repository; +import com.projectlyrics.server.domain.notification.domain.Notification; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + public interface NotificationQueryRepository { + + Slice findAllByReceiverId(Long receiverId, Long cursorId, Pageable pageable); } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java new file mode 100644 index 00000000..c775e7e8 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java @@ -0,0 +1,42 @@ +package com.projectlyrics.server.domain.notification.repository.impl; + +import com.projectlyrics.server.domain.common.util.QueryDslUtils; +import com.projectlyrics.server.domain.notification.domain.Notification; +import com.projectlyrics.server.domain.notification.repository.NotificationQueryRepository; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.projectlyrics.server.domain.notification.domain.QNotification.notification; + +@Repository +@RequiredArgsConstructor +public class QueryDslNotificationQueryRepository implements NotificationQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Slice findAllByReceiverId(Long receiverId, Long cursorId, Pageable pageable) { + List content = jpaQueryFactory + .selectFrom(notification) + .leftJoin(notification.sender).fetchJoin() + .leftJoin(notification.receiver).fetchJoin() + .leftJoin(notification.note).fetchJoin() + .leftJoin(notification.comment).fetchJoin() + .where( + notification.receiver.id.eq(receiverId), + QueryDslUtils.gtCursorId(cursorId, notification.id), + notification.deletedAt.isNull() + ) + .orderBy(notification.id.desc()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + return new SliceImpl<>(content, pageable, QueryDslUtils.checkIfHasNext(pageable, content)); + } +} From 6192c309350b3d64d3a6640dbd01e58af8696adb Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 14:04:24 +0900 Subject: [PATCH 20/28] =?UTF-8?q?[SCRUM-164]=20test:=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EC=BB=A4=EB=A7=A8=EB=93=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationCommandServiceTest.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java diff --git a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java new file mode 100644 index 00000000..b272f9ba --- /dev/null +++ b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java @@ -0,0 +1,104 @@ +package com.projectlyrics.server.domain.notification.service; + +import com.projectlyrics.server.domain.artist.entity.Artist; +import com.projectlyrics.server.domain.artist.repository.ArtistCommandRepository; +import com.projectlyrics.server.domain.comment.domain.Comment; +import com.projectlyrics.server.domain.comment.domain.CommentCreate; +import com.projectlyrics.server.domain.comment.dto.request.CommentCreateRequest; +import com.projectlyrics.server.domain.comment.repository.CommentCommandRepository; +import com.projectlyrics.server.domain.note.dto.request.NoteCreateRequest; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.note.entity.NoteBackground; +import com.projectlyrics.server.domain.note.entity.NoteCreate; +import com.projectlyrics.server.domain.note.entity.NoteStatus; +import com.projectlyrics.server.domain.note.repository.NoteCommandRepository; +import com.projectlyrics.server.domain.notification.domain.Notification; +import com.projectlyrics.server.domain.notification.domain.NotificationType; +import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.notification.repository.NotificationCommandRepository; +import com.projectlyrics.server.domain.notification.repository.NotificationQueryRepository; +import com.projectlyrics.server.domain.song.entity.Song; +import com.projectlyrics.server.domain.song.repository.SongCommandRepository; +import com.projectlyrics.server.domain.user.entity.User; +import com.projectlyrics.server.domain.user.repository.UserCommandRepository; +import com.projectlyrics.server.support.IntegrationTest; +import com.projectlyrics.server.support.fixture.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class NotificationCommandServiceTest extends IntegrationTest { + + @Autowired + UserCommandRepository userCommandRepository; + + @Autowired + ArtistCommandRepository artistCommandRepository; + + @Autowired + SongCommandRepository songCommandRepository; + + @Autowired + NoteCommandRepository noteCommandRepository; + + @Autowired + CommentCommandRepository commentCommandRepository; + + @Autowired + NotificationCommandRepository notificationCommandRepository; + + @Autowired + NotificationQueryRepository notificationQueryRepository; + + @Autowired + NotificationCommandService sut; + + private Comment comment; + private User user; + + @BeforeEach + void setUp() { + user = userCommandRepository.save(UserFixture.create()); + Artist artist = artistCommandRepository.save(ArtistFixture.create()); + Song song = songCommandRepository.save(SongFixture.create(artist)); + + NoteCreateRequest noteCreateRequest = new NoteCreateRequest( + "content", + "lyrics", + NoteBackground.DEFAULT, + NoteStatus.PUBLISHED, + song.getId() + ); + Note note = noteCommandRepository.save(Note.create(NoteCreate.from(noteCreateRequest, user, song))); + + comment = commentCommandRepository.save(Comment.create(CommentCreate.of("content", user, note))); + } + + @Test + void 댓글에_대한_알림을_생성한다() { + // given + CommentEvent commentEvent = CommentEvent.from(comment); + + // when + sut.createCommentNotification(commentEvent); + + // then + List result = notificationQueryRepository.findAllByReceiverId(user.getId(), null, PageRequest.ofSize(10)) + .getContent(); + + assertAll( + () -> assertThat(result.size()).isEqualTo(1), + () -> assertThat(result.get(0).getType()).isEqualTo(NotificationType.COMMENT_ON_NOTE), + () -> assertThat(result.get(0).getSender()).isEqualTo(comment.getWriter()), + () -> assertThat(result.get(0).getReceiver()).isEqualTo(comment.getNote().getPublisher()), + () -> assertThat(result.get(0).getNote()).isEqualTo(comment.getNote()), + () -> assertThat(result.get(0).getComment()).isEqualTo(comment) + ); + } +} \ No newline at end of file From 052f72a7be2c77cea62d5a2c30f0cd5f246dd2b6 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 17:07:16 +0900 Subject: [PATCH 21/28] =?UTF-8?q?[SCRUM-164]=20refactor:=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=83=9D=EC=84=B1=EC=97=90=20fcmToken=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/dto/request/AuthSignUpRequest.java | 2 ++ .../server/domain/user/entity/User.java | 25 ++++++------------- .../server/domain/user/entity/UserCreate.java | 6 +++-- .../server/support/fixture/UserFixture.java | 11 +++++--- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/projectlyrics/server/domain/auth/dto/request/AuthSignUpRequest.java b/src/main/java/com/projectlyrics/server/domain/auth/dto/request/AuthSignUpRequest.java index e2303218..a02c77b8 100644 --- a/src/main/java/com/projectlyrics/server/domain/auth/dto/request/AuthSignUpRequest.java +++ b/src/main/java/com/projectlyrics/server/domain/auth/dto/request/AuthSignUpRequest.java @@ -33,6 +33,8 @@ public record AuthSignUpRequest( @Valid List terms + + // TODO: fcmToken 추가 ) { public record TermsInput( diff --git a/src/main/java/com/projectlyrics/server/domain/user/entity/User.java b/src/main/java/com/projectlyrics/server/domain/user/entity/User.java index 6f0be566..b565336b 100644 --- a/src/main/java/com/projectlyrics/server/domain/user/entity/User.java +++ b/src/main/java/com/projectlyrics/server/domain/user/entity/User.java @@ -84,9 +84,10 @@ private User( Role role, Gender gender, Integer birthYear, - List termsAgreements + List termsAgreements, + String fcmToken ) { - this(null, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, null); + this(null, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, fcmToken); } public static User withId( @@ -97,9 +98,10 @@ public static User withId( Role role, Gender gender, Integer birthYear, - List termsAgreements + List termsAgreements, + String fcmToken ) { - return new User(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, null); + return new User(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, fcmToken); } public static User create(UserCreate userCreate) { @@ -110,19 +112,8 @@ public static User create(UserCreate userCreate) { Role.USER, userCreate.gender(), userCreate.birthYear(), - userCreate.termsAgreements() + userCreate.termsAgreements(), + userCreate.fcmToken() ); } - - public static User of( - SocialInfo socialInfo, - String nickname, - ProfileCharacter profileCharacter, - Role role, - Gender gender, - Integer birthYear, - List termsAgreements - ) { - return new User(socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements); - } } diff --git a/src/main/java/com/projectlyrics/server/domain/user/entity/UserCreate.java b/src/main/java/com/projectlyrics/server/domain/user/entity/UserCreate.java index dba5120c..04fdab1b 100644 --- a/src/main/java/com/projectlyrics/server/domain/user/entity/UserCreate.java +++ b/src/main/java/com/projectlyrics/server/domain/user/entity/UserCreate.java @@ -11,7 +11,8 @@ public record UserCreate( ProfileCharacter profileCharacter, Gender gender, Integer birthYear, - List termsAgreements + List termsAgreements, + String fcmToken ) { public static UserCreate of(SocialInfo socialInfo, AuthSignUpRequest request) { @@ -25,7 +26,8 @@ public static UserCreate of(SocialInfo socialInfo, AuthSignUpRequest request) { request.profileCharacter(), request.gender(), Objects.nonNull(request.birthYear()) ? request.birthYear().getValue() : null, - termsList + termsList, + null ); } } diff --git a/src/test/java/com/projectlyrics/server/support/fixture/UserFixture.java b/src/test/java/com/projectlyrics/server/support/fixture/UserFixture.java index 5b26c58d..f9ad3a3a 100644 --- a/src/test/java/com/projectlyrics/server/support/fixture/UserFixture.java +++ b/src/test/java/com/projectlyrics/server/support/fixture/UserFixture.java @@ -21,8 +21,10 @@ public class UserFixture extends BaseFixture { private int birthYear = 1999; private Role role = Role.USER; private List termsAgreements = List.of(new TermsAgreements(true, "title", "agreement")); + private String fcmToken = "fcmToken"; - private UserFixture() {} + private UserFixture() { + } public static User create() { return User.withId( @@ -33,8 +35,9 @@ public static User create() { Role.USER, Gender.MALE, 1999, - List.of(new TermsAgreements(true, "title", "agreement") - )); + List.of(new TermsAgreements(true, "title", "agreement")), + "fcmToken" + ); } public static UserFixture builder() { @@ -42,7 +45,7 @@ public static UserFixture builder() { } public User build() { - return User.withId(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements); + return User.withId(id, socialInfo, nickname, profileCharacter, role, gender, birthYear, termsAgreements, fcmToken); } public UserFixture role(Role role) { From 05e3027f3bb5be923b7c8555c1083e811e6e9cf7 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 17:07:50 +0900 Subject: [PATCH 22/28] =?UTF-8?q?[SCRUM-164]=20refactor:=20FirebaseMessagi?= =?UTF-8?q?ng=EC=9D=98=20=EB=AA=A8=ED=82=B9=EC=9D=84=20=EC=9C=84=ED=95=B4?= =?UTF-8?q?=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=EB=A5=BC=20=EB=B9=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=93=B1=EB=A1=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationCommandService.java | 3 ++- .../server/global/configuration/FirebaseConfig.java | 8 ++++++++ .../service/NotificationCommandServiceTest.java | 10 ++++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java index 6d02c132..75990d36 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java @@ -20,6 +20,7 @@ public class NotificationCommandService { private final NotificationCommandRepository notificationCommandRepository; + private final FirebaseMessaging firebaseMessaging; @Async @EventListener @@ -30,7 +31,7 @@ public void createCommentNotification(CommentEvent event) { private void send(Notification notification) { try { - FirebaseMessaging.getInstance().send(notification.getMessage()); + firebaseMessaging.send(notification.getMessage()); } catch (FirebaseMessagingException e) { log.info(e.getMessage()); throw new FailedToSendNotificationException(); diff --git a/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java index 7aee7751..652a1c01 100644 --- a/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java +++ b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java @@ -3,8 +3,10 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.FileInputStream; @@ -28,4 +30,10 @@ public void init() { log.error("failed to initialize firebase", e); } } + + @Bean + public FirebaseMessaging firebaseMessaging() { + log.info("firebase messaging initialized"); + return FirebaseMessaging.getInstance(); + } } diff --git a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java index b272f9ba..d89f4ef1 100644 --- a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java +++ b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java @@ -1,10 +1,10 @@ package com.projectlyrics.server.domain.notification.service; +import com.google.firebase.messaging.FirebaseMessaging; import com.projectlyrics.server.domain.artist.entity.Artist; import com.projectlyrics.server.domain.artist.repository.ArtistCommandRepository; import com.projectlyrics.server.domain.comment.domain.Comment; import com.projectlyrics.server.domain.comment.domain.CommentCreate; -import com.projectlyrics.server.domain.comment.dto.request.CommentCreateRequest; import com.projectlyrics.server.domain.comment.repository.CommentCommandRepository; import com.projectlyrics.server.domain.note.dto.request.NoteCreateRequest; import com.projectlyrics.server.domain.note.entity.Note; @@ -26,12 +26,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.*; class NotificationCommandServiceTest extends IntegrationTest { @@ -56,6 +58,9 @@ class NotificationCommandServiceTest extends IntegrationTest { @Autowired NotificationQueryRepository notificationQueryRepository; + @MockBean + FirebaseMessaging firebaseMessaging; + @Autowired NotificationCommandService sut; @@ -81,9 +86,10 @@ void setUp() { } @Test - void 댓글에_대한_알림을_생성한다() { + void 댓글에_대한_알림을_저장한다() throws Exception { // given CommentEvent commentEvent = CommentEvent.from(comment); + when(firebaseMessaging.send(any())).thenReturn(null); // when sut.createCommentNotification(commentEvent); From 97cbc3ba21f113d1d7a544c8de5edb776e9077e0 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 18:02:54 +0900 Subject: [PATCH 23/28] =?UTF-8?q?[SCRUM-164]=20feat:=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/NotificationController.java | 32 +++++++++++ .../PublicNotificationCreateRequest.java | 9 +++ .../PublicNotificationCreateResponse.java | 6 ++ .../notification/domain/Notification.java | 28 ++++++---- .../notification/domain/NotificationType.java | 2 +- .../domain/event/PublicEvent.java | 14 +++++ .../NotificationQueryRepository.java | 1 + .../QueryDslNotificationQueryRepository.java | 20 +++++++ .../service/NotificationCommandService.java | 46 ++++++++++++++++ .../user/repository/UserQueryRepository.java | 3 + .../impl/QueryDslUserQueryRepository.java | 31 ++++++++--- .../api/NotificationControllerTest.java | 55 +++++++++++++++++++ .../NotificationCommandServiceTest.java | 29 +++++++++- .../server/support/ControllerTest.java | 4 ++ 14 files changed, 259 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/api/NotificationController.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/api/dto/request/PublicNotificationCreateRequest.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/api/dto/response/PublicNotificationCreateResponse.java create mode 100644 src/main/java/com/projectlyrics/server/domain/notification/domain/event/PublicEvent.java create mode 100644 src/test/java/com/projectlyrics/server/domain/notification/api/NotificationControllerTest.java diff --git a/src/main/java/com/projectlyrics/server/domain/notification/api/NotificationController.java b/src/main/java/com/projectlyrics/server/domain/notification/api/NotificationController.java new file mode 100644 index 00000000..b83e9245 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/api/NotificationController.java @@ -0,0 +1,32 @@ +package com.projectlyrics.server.domain.notification.api; + +import com.projectlyrics.server.domain.auth.authentication.AuthContext; +import com.projectlyrics.server.domain.auth.authentication.Authenticated; +import com.projectlyrics.server.domain.notification.api.dto.request.PublicNotificationCreateRequest; +import com.projectlyrics.server.domain.notification.api.dto.response.PublicNotificationCreateResponse; +import com.projectlyrics.server.domain.notification.service.NotificationCommandService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/notifications") +@RequiredArgsConstructor +public class NotificationController { + + public final NotificationCommandService notificationCommandService; + + @PostMapping("/public") + public ResponseEntity createPublicNotification( + @Authenticated AuthContext authContext, + @RequestBody @Valid PublicNotificationCreateRequest request + ) { + notificationCommandService.createPublicNotification(authContext.getId(), request.content()); + + return ResponseEntity.ok(new PublicNotificationCreateResponse(true)); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/api/dto/request/PublicNotificationCreateRequest.java b/src/main/java/com/projectlyrics/server/domain/notification/api/dto/request/PublicNotificationCreateRequest.java new file mode 100644 index 00000000..8acb94ed --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/api/dto/request/PublicNotificationCreateRequest.java @@ -0,0 +1,9 @@ +package com.projectlyrics.server.domain.notification.api.dto.request; + +import jakarta.validation.constraints.NotEmpty; + +public record PublicNotificationCreateRequest( + @NotEmpty(message = "알림 내용이 비어 있습니다.") + String content +) { +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/api/dto/response/PublicNotificationCreateResponse.java b/src/main/java/com/projectlyrics/server/domain/notification/api/dto/response/PublicNotificationCreateResponse.java new file mode 100644 index 00000000..20775854 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/api/dto/response/PublicNotificationCreateResponse.java @@ -0,0 +1,6 @@ +package com.projectlyrics.server.domain.notification.api.dto.response; + +public record PublicNotificationCreateResponse( + boolean status +) { +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java index 0ebf1ba5..be5a14b1 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/Notification.java @@ -5,6 +5,7 @@ import com.projectlyrics.server.domain.common.entity.BaseEntity; import com.projectlyrics.server.domain.note.entity.Note; import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.notification.domain.event.PublicEvent; import com.projectlyrics.server.domain.user.entity.User; import jakarta.persistence.*; import lombok.AccessLevel; @@ -24,6 +25,7 @@ public class Notification extends BaseEntity { private Long id; private NotificationType type; + private String content; @ManyToOne(fetch = FetchType.LAZY) private User sender; @@ -38,6 +40,7 @@ public class Notification extends BaseEntity { private Notification( Long id, NotificationType type, + String content, User sender, User receiver, Note note, @@ -46,24 +49,17 @@ private Notification( this.id = id; this.type = type; this.sender = sender; + this.content = content; this.receiver = receiver; this.note = note; this.comment = comment; } - private Notification( - NotificationType type, - User sender, - User receiver, - Note note, - Comment comment - ) { - this(null, type, sender, receiver, note, comment); - } - public static Notification create(CommentEvent event) { return new Notification( + null, NotificationType.COMMENT_ON_NOTE, + null, event.sender(), event.receiver(), event.note(), @@ -71,6 +67,18 @@ public static Notification create(CommentEvent event) { ); } + public static Notification create(PublicEvent event) { + return new Notification( + null, + NotificationType.PUBLIC, + event.content(), + event.sender(), + event.receiver(), + null, + null + ); + } + public Message getMessage() { Message.Builder builder = Message.builder() .setToken(receiver.getFcmToken()); diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java index 73c8b422..5ca819eb 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/NotificationType.java @@ -3,6 +3,6 @@ public enum NotificationType { COMMENT_ON_NOTE, REPORT, - ALL, + PUBLIC, ; } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/domain/event/PublicEvent.java b/src/main/java/com/projectlyrics/server/domain/notification/domain/event/PublicEvent.java new file mode 100644 index 00000000..cff3f667 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/notification/domain/event/PublicEvent.java @@ -0,0 +1,14 @@ +package com.projectlyrics.server.domain.notification.domain.event; + +import com.projectlyrics.server.domain.user.entity.User; + +public record PublicEvent( + User sender, + User receiver, + String content +) { + + public static PublicEvent of(String content, User sender, User receiver) { + return new PublicEvent(sender, receiver, content); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java index ae26f56d..10a2e298 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/NotificationQueryRepository.java @@ -7,4 +7,5 @@ public interface NotificationQueryRepository { Slice findAllByReceiverId(Long receiverId, Long cursorId, Pageable pageable); + Slice findAllBySenderId(Long senderId, Long cursorId, Pageable pageable); } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java index c775e7e8..cb72cd20 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/repository/impl/QueryDslNotificationQueryRepository.java @@ -39,4 +39,24 @@ public Slice findAllByReceiverId(Long receiverId, Long cursorId, P return new SliceImpl<>(content, pageable, QueryDslUtils.checkIfHasNext(pageable, content)); } + + @Override + public Slice findAllBySenderId(Long senderId, Long cursorId, Pageable pageable) { + List content = jpaQueryFactory + .selectFrom(notification) + .leftJoin(notification.sender).fetchJoin() + .leftJoin(notification.receiver).fetchJoin() + .leftJoin(notification.note).fetchJoin() + .leftJoin(notification.comment).fetchJoin() + .where( + notification.sender.id.eq(senderId), + QueryDslUtils.gtCursorId(cursorId, notification.id), + notification.deletedAt.isNull() + ) + .orderBy(notification.id.desc()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + return new SliceImpl<>(content, pageable, QueryDslUtils.checkIfHasNext(pageable, content)); + } } diff --git a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java index 75990d36..ed13c6bd 100644 --- a/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java +++ b/src/main/java/com/projectlyrics/server/domain/notification/service/NotificationCommandService.java @@ -2,10 +2,15 @@ import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.MulticastMessage; import com.projectlyrics.server.domain.notification.domain.Notification; import com.projectlyrics.server.domain.notification.domain.event.CommentEvent; +import com.projectlyrics.server.domain.notification.domain.event.PublicEvent; import com.projectlyrics.server.domain.notification.exception.FailedToSendNotificationException; import com.projectlyrics.server.domain.notification.repository.NotificationCommandRepository; +import com.projectlyrics.server.domain.user.entity.User; +import com.projectlyrics.server.domain.user.exception.UserNotFoundException; +import com.projectlyrics.server.domain.user.repository.UserQueryRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; @@ -13,6 +18,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Slf4j @Service @Transactional @@ -20,6 +27,7 @@ public class NotificationCommandService { private final NotificationCommandRepository notificationCommandRepository; + private final UserQueryRepository userQueryRepository; private final FirebaseMessaging firebaseMessaging; @Async @@ -37,4 +45,42 @@ private void send(Notification notification) { throw new FailedToSendNotificationException(); } } + + public void createPublicNotification(Long adminId, String content) { + // TODO: adminId 확인하는 로직 추가 + User admin = userQueryRepository.findById(adminId) + .orElseThrow(UserNotFoundException::new); + List receivers = userQueryRepository.findAll(); + + List notifications = receivers.stream() + .map(receiver -> Notification.create(PublicEvent.of(content, admin, receiver))) + .toList(); + notificationCommandRepository.saveAll(notifications); + + sendAll(notifications, content); + } + + private void sendAll(List notifications, String content) { + int batchSize = notifications.size() / 500 + 1; + + for (int i = 0; i < batchSize; i++) { + List fcmTokens = notifications.subList(i * 500, Math.min((i + 1) * 500, notifications.size())) + .stream() + .map(Notification::getReceiver) + .map(User::getFcmToken) + .toList(); + + MulticastMessage message = MulticastMessage.builder() + .addAllTokens(fcmTokens) + .putData("content", content) + .build(); + + try { + firebaseMessaging.sendEachForMulticast(message); + } catch (FirebaseMessagingException e) { + log.info(e.getMessage()); + throw new FailedToSendNotificationException(); + } + } + } } diff --git a/src/main/java/com/projectlyrics/server/domain/user/repository/UserQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/user/repository/UserQueryRepository.java index b5494b24..29ac7c07 100644 --- a/src/main/java/com/projectlyrics/server/domain/user/repository/UserQueryRepository.java +++ b/src/main/java/com/projectlyrics/server/domain/user/repository/UserQueryRepository.java @@ -4,6 +4,7 @@ import com.projectlyrics.server.domain.user.entity.SocialInfo; import com.projectlyrics.server.domain.user.entity.User; +import java.util.List; import java.util.Optional; public interface UserQueryRepository { @@ -12,5 +13,7 @@ public interface UserQueryRepository { Optional findById(Long id); + List findAll(); + boolean existsBySocialInfo(SocialInfo socialInfo); } diff --git a/src/main/java/com/projectlyrics/server/domain/user/repository/impl/QueryDslUserQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/user/repository/impl/QueryDslUserQueryRepository.java index 9292460b..156d4c02 100644 --- a/src/main/java/com/projectlyrics/server/domain/user/repository/impl/QueryDslUserQueryRepository.java +++ b/src/main/java/com/projectlyrics/server/domain/user/repository/impl/QueryDslUserQueryRepository.java @@ -7,11 +7,14 @@ import com.projectlyrics.server.domain.user.repository.UserQueryRepository; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import static com.projectlyrics.server.domain.user.entity.QUser.user; + @Repository @RequiredArgsConstructor public class QueryDslUserQueryRepository implements UserQueryRepository { @@ -22,11 +25,11 @@ public class QueryDslUserQueryRepository implements UserQueryRepository { public Optional findBySocialIdAndAuthProvider(String socialId, AuthProvider authProvider) { return Optional.ofNullable( jpaQueryFactory - .selectFrom(QUser.user) + .selectFrom(user) .where( - QUser.user.socialInfo.socialId.eq(socialId), - QUser.user.socialInfo.authProvider.eq(authProvider), - QUser.user.deletedAt.isNull() + user.socialInfo.socialId.eq(socialId), + user.socialInfo.authProvider.eq(authProvider), + user.deletedAt.isNull() ) .fetchOne() ); @@ -36,21 +39,31 @@ public Optional findBySocialIdAndAuthProvider(String socialId, AuthProvide public Optional findById(Long id) { return Optional.ofNullable( jpaQueryFactory - .selectFrom(QUser.user) + .selectFrom(user) .where( - QUser.user.id.eq(id), - QUser.user.deletedAt.isNull() + user.id.eq(id), + user.deletedAt.isNull() ) .fetchOne() ); } + @Override + public List findAll() { + return jpaQueryFactory + .selectFrom(user) + .where( + user.deletedAt.isNull() + ) + .fetch(); + } + @Override public boolean existsBySocialInfo(SocialInfo socialInfo) { return Optional.ofNullable( jpaQueryFactory - .selectFrom(QUser.user) - .where(QUser.user.socialInfo.eq(socialInfo)) + .selectFrom(user) + .where(user.socialInfo.eq(socialInfo)) .fetchOne() ).isPresent(); } diff --git a/src/test/java/com/projectlyrics/server/domain/notification/api/NotificationControllerTest.java b/src/test/java/com/projectlyrics/server/domain/notification/api/NotificationControllerTest.java new file mode 100644 index 00000000..81af4510 --- /dev/null +++ b/src/test/java/com/projectlyrics/server/domain/notification/api/NotificationControllerTest.java @@ -0,0 +1,55 @@ +package com.projectlyrics.server.domain.notification.api; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.projectlyrics.server.domain.notification.api.dto.request.PublicNotificationCreateRequest; +import com.projectlyrics.server.support.RestDocsTest; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.JsonFieldType; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class NotificationControllerTest extends RestDocsTest { + + @Test + void 전체_알림을_등록하면_결과_상태값과_200응답을_해야_한다() throws Exception { + // given + PublicNotificationCreateRequest request = new PublicNotificationCreateRequest("content"); + + // when, then + mockMvc.perform(post("/api/v1/notifications/public") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(getPublicNotificationCreateDocument()); + } + + private RestDocumentationResultHandler getPublicNotificationCreateDocument() { + return restDocs.document( + resource(ResourceSnippetParameters.builder() + .tag("Notification API") + .summary("전체 알림 등록 API") + .requestHeaders(getAuthorizationHeader()) + .requestFields( + fieldWithPath("content").type(JsonFieldType.STRING) + .description("알림 내용") + ) + .requestSchema(Schema.schema("Create Public Notification Request")) + .responseFields( + fieldWithPath("status").type(JsonFieldType.BOOLEAN) + .description("요청 처리 상태") + ) + .responseSchema(Schema.schema("Create Public Notification Response")) + .build()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java index d89f4ef1..b681512a 100644 --- a/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java +++ b/src/test/java/com/projectlyrics/server/domain/notification/service/NotificationCommandServiceTest.java @@ -29,6 +29,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -66,10 +67,15 @@ class NotificationCommandServiceTest extends IntegrationTest { private Comment comment; private User user; + private List users = new ArrayList<>(); @BeforeEach void setUp() { - user = userCommandRepository.save(UserFixture.create()); + for (int i = 0; i < 100; i++) { + users.add(userCommandRepository.save(UserFixture.create())); + } + user = users.getFirst(); + Artist artist = artistCommandRepository.save(ArtistFixture.create()); Song song = songCommandRepository.save(SongFixture.create(artist)); @@ -107,4 +113,25 @@ void setUp() { () -> assertThat(result.get(0).getComment()).isEqualTo(comment) ); } + + @Test + void 공개_알림을_저장한다() throws Exception { + // given + when(firebaseMessaging.sendEachForMulticast(any())).thenReturn(null); + String content = "content"; + + // when + sut.createPublicNotification(user.getId(), content); + + // then + List result = notificationQueryRepository.findAllBySenderId(user.getId(), null, PageRequest.ofSize(10)) + .getContent(); + + assertAll( + () -> assertThat(result.size()).isEqualTo(10), + () -> assertThat(result.stream().allMatch(notification -> notification.getType().equals(NotificationType.PUBLIC))).isTrue(), + () -> assertThat(result.stream().allMatch(notification -> notification.getSender().getId().equals(user.getId()))).isTrue(), + () -> assertThat(result.stream().allMatch(notification -> notification.getContent().equalsIgnoreCase(content))).isTrue() + ); + } } \ No newline at end of file diff --git a/src/test/java/com/projectlyrics/server/support/ControllerTest.java b/src/test/java/com/projectlyrics/server/support/ControllerTest.java index e1325d94..c7bec69b 100644 --- a/src/test/java/com/projectlyrics/server/support/ControllerTest.java +++ b/src/test/java/com/projectlyrics/server/support/ControllerTest.java @@ -18,6 +18,7 @@ import com.projectlyrics.server.domain.like.service.LikeQueryService; import com.projectlyrics.server.domain.note.service.NoteCommandService; import com.projectlyrics.server.domain.note.service.NoteQueryService; +import com.projectlyrics.server.domain.notification.service.NotificationCommandService; import com.projectlyrics.server.domain.song.service.SongQueryService; import com.projectlyrics.server.global.configuration.ClockConfig; import org.junit.jupiter.api.BeforeEach; @@ -85,6 +86,9 @@ public abstract class ControllerTest { @MockBean protected BookmarkCommandService bookmarkCommandService; + @MockBean + protected NotificationCommandService notificationCommandService; + public String accessToken; public String refreshToken; From 8ec99bc040350cfbbd400a26c1a9c31781f4aec5 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Sun, 25 Aug 2024 18:34:54 +0900 Subject: [PATCH 24/28] =?UTF-8?q?[SCRUM-164]=20fix:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=8A=88=ED=8A=B8=20=EC=8B=A4=ED=96=89=20=EC=8B=9C?= =?UTF-8?q?=20FirebaseApp=20=EC=A4=91=EB=B3=B5=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/global/configuration/FirebaseConfig.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java index 652a1c01..30cb5932 100644 --- a/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java +++ b/src/main/java/com/projectlyrics/server/global/configuration/FirebaseConfig.java @@ -19,13 +19,15 @@ public class FirebaseConfig { @PostConstruct public void init() { try { - FileInputStream key = new FileInputStream("src/main/resources/firebase-key.json"); + if (FirebaseApp.getApps().isEmpty()) { + FileInputStream key = new FileInputStream("src/main/resources/firebase-key.json"); - FirebaseOptions options = FirebaseOptions.builder() - .setCredentials(GoogleCredentials.fromStream(key)) - .build(); + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(key)) + .build(); - FirebaseApp.initializeApp(options); + FirebaseApp.initializeApp(options); + } } catch (IOException e) { log.error("failed to initialize firebase", e); } From 8de4249d86a45a09767995cc812c28f66786226a Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Tue, 27 Aug 2024 01:05:20 +0900 Subject: [PATCH 25/28] =?UTF-8?q?[SCRUM-164]=20settings:=20Test=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=82=B4=20firebase=20key=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/Test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 007ab524..07b15ebd 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -26,8 +26,6 @@ jobs: redis-version: 6 - name: overwrite firebase-key.json - if: | - contains(github.ref, 'dev') run: | mkdir -p ./src/main/resources echo "${{ secrets.FIREBASE_KEY }}" > src/main/resources/firebase-key.json From 1ee8f9cdda4af17388e93e2b895ed6ffec3e2493 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Tue, 27 Aug 2024 02:11:24 +0900 Subject: [PATCH 26/28] =?UTF-8?q?[SCRUM-164]=20settings:=20Test.yml=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/Test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 07b15ebd..f9e25796 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -29,7 +29,6 @@ jobs: run: | mkdir -p ./src/main/resources echo "${{ secrets.FIREBASE_KEY }}" > src/main/resources/firebase-key.json - shell: bash - name: Test with Gradle run: | From ad72ef0777b234490482aa388a7d928ea4bd89f8 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Tue, 27 Aug 2024 02:18:50 +0900 Subject: [PATCH 27/28] =?UTF-8?q?[SCRUM-164]=20settings:=20Test.yml=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/Test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index f9e25796..93d6dbd4 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -28,7 +28,7 @@ jobs: - name: overwrite firebase-key.json run: | mkdir -p ./src/main/resources - echo "${{ secrets.FIREBASE_KEY }}" > src/main/resources/firebase-key.json + echo "${{ secrets.FIREBASE_KEY }}" | base64 --decode > src/main/resources/firebase-key.json - name: Test with Gradle run: | From 184aab49a14bf981854e89c4254f6606200df701 Mon Sep 17 00:00:00 2001 From: Jin Geonwoo Date: Tue, 27 Aug 2024 02:24:52 +0900 Subject: [PATCH 28/28] =?UTF-8?q?[SCRUM-164]=20settings:=20Test.yml=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/Test.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 93d6dbd4..89e38d65 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -25,10 +25,18 @@ jobs: with: redis-version: 6 - - name: overwrite firebase-key.json + - name: create-json + id: create-json + uses: jsdaniell/create-json@1.1.2 + with: + name: "firebase-key.json" + json: ${{ secrets.FIREBASE_KEY }} + + - name: Move firebase-key.json to src/main/resources run: | mkdir -p ./src/main/resources - echo "${{ secrets.FIREBASE_KEY }}" | base64 --decode > src/main/resources/firebase-key.json + mv firebase-key.json ./src/main/resources/firebase-key.json + shell: bash - name: Test with Gradle run: |