From fd97b7d40c5b6ebdcda692bfc280d7cb81cf9da2 Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 13 Mar 2025 06:59:34 +0900 Subject: [PATCH 01/29] =?UTF-8?q?fix(#10):=20User,=20Notification=EC=9D=98?= =?UTF-8?q?=20=ED=85=8C=EC=9D=B4=EB=B8=94=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 네이밍 일관성을 위해 엔티티는 단수형, 테이블명은 복수형으로 작성함 (cherry picked from commit fd78ebbfbb9da3c77c1aa60f46c19c100b5dced0) # Conflicts: # src/main/generated/com/example/moim/notification/entity/QNotification.java # src/main/generated/com/example/moim/user/entity/QUser.java # src/main/java/com/example/moim/notification/entity/Notifications.java # src/main/java/com/example/moim/user/entity/User.java --- .../notification/dto/NotificationOutput.java | 18 +-- .../notification/entity/Notification.java | 97 +++++++++++++++ .../notification/entity/Notifications.java | 116 ------------------ .../repository/NotificationRepository.java | 6 +- .../service/NotificationEventHandler.java | 29 +++-- .../com/example/moim/user/entity/User.java | 3 +- 6 files changed, 128 insertions(+), 141 deletions(-) create mode 100644 src/main/java/com/example/moim/notification/entity/Notification.java delete mode 100644 src/main/java/com/example/moim/notification/entity/Notifications.java diff --git a/src/main/java/com/example/moim/notification/dto/NotificationOutput.java b/src/main/java/com/example/moim/notification/dto/NotificationOutput.java index 5d7a4e1..ced90fe 100644 --- a/src/main/java/com/example/moim/notification/dto/NotificationOutput.java +++ b/src/main/java/com/example/moim/notification/dto/NotificationOutput.java @@ -1,6 +1,6 @@ package com.example.moim.notification.dto; -import com.example.moim.notification.entity.Notifications; +import com.example.moim.notification.entity.Notification; import lombok.Data; import java.time.format.DateTimeFormatter; @@ -14,13 +14,13 @@ public class NotificationOutput { private String time; private Boolean isRead; - public NotificationOutput(Notifications notifications) { - this.id = notifications.getId(); - this.title = notifications.getTitle(); - this.category = notifications.getCategory(); - this.content = notifications.getContents(); - this.time = notifications.getCreatedDate().format(DateTimeFormatter.ofPattern("MM/dd HH:mm")); - notifications.setRead(true); - this.isRead = notifications.getIsRead(); + public NotificationOutput(Notification notification) { + this.id = notification.getId(); + this.title = notification.getTitle(); + this.category = notification.getCategory(); + this.content = notification.getContents(); + this.time = notification.getCreatedDate().format(DateTimeFormatter.ofPattern("MM/dd HH:mm")); + notification.setRead(true); + this.isRead = notification.getIsRead(); } } diff --git a/src/main/java/com/example/moim/notification/entity/Notification.java b/src/main/java/com/example/moim/notification/entity/Notification.java new file mode 100644 index 0000000..4dc8845 --- /dev/null +++ b/src/main/java/com/example/moim/notification/entity/Notification.java @@ -0,0 +1,97 @@ +package com.example.moim.notification.entity; + +import com.example.moim.schedule.entity.Schedule; +import com.example.moim.global.entity.BaseEntity; +import com.example.moim.notification.dto.*; +import com.example.moim.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "notifications") +public class Notification extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + private User targetUser; + private String title; + private String category; + private String contents; + private Boolean isRead; + + public static Notification createClubJoinNotification(ClubJoinEvent clubJoinEvent, User targetUser) { + Notification notification = new Notification(); + notification.targetUser = targetUser; + notification.title = "가입 알림"; + notification.category = "모임"; + notification.contents = clubJoinEvent.getUser().getName() + " 님이 " + clubJoinEvent.getClub().getTitle() + "에 가입했습니다."; + notification.isRead = false; + return notification; + } + + public static Notification createScheduleSaveNotification(ScheduleSaveEvent scheduleSaveEvent, User targetUser) { + Notification notification = new Notification(); + notification.targetUser = targetUser; + Schedule schedule = scheduleSaveEvent.getSchedule(); + notification.title = "일정 등록 알림"; + notification.category = "일정"; + notification.contents = schedule.getTitle() + " 일정이 등록되었습니다.\n참가신청 바로가기!"; + notification.isRead = false; + return notification; + } + +// public static Notification createScheduleVoteNotification(ScheduleVoteEvent scheduleVoteEvent) { +// Notification notifications = new Notification(); +// notifications.targetUser = scheduleVoteEvent.getUser(); +// Schedule schedule = scheduleVoteEvent.getSchedule(); +// notifications.title = schedule.getClub().getTitle() + ": 일정 참여"; +// notifications.category = "일정"; +// notifications.contents = schedule.getTitle() + " 일정에 참여했습니다."; +// notifications.isRead = false; +// return notifications; +// } + + public static Notification createScheduleEncourageEvent(Schedule schedule, User targetUser) { + Notification notification = new Notification(); + notification.targetUser = targetUser; + notification.title = " 일정 참가 투표 알림"; + notification.category = "일정"; + notification.contents = schedule.getTitle() + " 일정 참가 투표가 곧 마감됩니다.\n참가 투표를 해주세요."; + notification.isRead = false; + return notification; + } + + public static Notification createMatchRequestEvent(MatchRequestEvent matchRequestEvent, User targetUser) { + Notification notification = new Notification(); + notification.targetUser = targetUser; + notification.title = "매치 건의 알림"; + notification.category = "친선 매치"; + notification.contents = matchRequestEvent.getUser().getName() + "님이 " + + matchRequestEvent.getMatch().getStartTime().toLocalDate() + + matchRequestEvent.getMatch().getEvent() + "매치를 원합니다. \nt승인하시겠습니까?"; + notification.isRead = false; + return notification; + } + + public static Notification createMatchInviteEvent(MatchInviteEvent matchInviteEvent, User targetUser) { + Notification notification = new Notification(); + notification.targetUser = targetUser; + notification.title = "매치 초청 알림"; + notification.category = "친선 매치"; + notification.contents = "<" + matchInviteEvent.getMatch().getEvent() + " 한판 해요~> - " + + matchInviteEvent.getMatch().getHomeClub().getTitle() + "\n" + + matchInviteEvent.getMatch().getHomeClub().getTitle() + "가 친선 매치를 제안했습니다!"; + notification.isRead = false; + return notification; + } + + public void setRead(Boolean read) { + isRead = read; + } + + +} diff --git a/src/main/java/com/example/moim/notification/entity/Notifications.java b/src/main/java/com/example/moim/notification/entity/Notifications.java deleted file mode 100644 index 014f548..0000000 --- a/src/main/java/com/example/moim/notification/entity/Notifications.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.example.moim.notification.entity; - -import com.example.moim.schedule.entity.Schedule; -import com.example.moim.global.entity.BaseEntity; -import com.example.moim.notification.dto.*; -import com.example.moim.user.entity.User; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor -public class Notifications extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @ManyToOne(fetch = FetchType.LAZY) - private User targetUser; - private String title; - private String category; - private String contents; - private Boolean isRead; - - public static Notifications createClubJoinNotification(ClubJoinEvent clubJoinEvent, User targetUser) { - Notifications notifications = new Notifications(); - notifications.targetUser = targetUser; - notifications.title = "가입 알림"; - notifications.category = "모임"; - notifications.contents = clubJoinEvent.getUser().getName() + " 님이 " + clubJoinEvent.getClub().getTitle() + "에 가입했습니다."; - notifications.isRead = false; - return notifications; - } - - public static Notifications createScheduleSaveNotification(ScheduleSaveEvent scheduleSaveEvent, User targetUser) { - Notifications notifications = new Notifications(); - notifications.targetUser = targetUser; - Schedule schedule = scheduleSaveEvent.getSchedule(); - notifications.title = "일정 등록 알림"; - notifications.category = "일정"; - notifications.contents = schedule.getTitle() + " 일정이 등록되었습니다.\n참가신청 바로가기!"; - notifications.isRead = false; - return notifications; - } - -// public static Notifications createScheduleVoteNotification(ScheduleVoteEvent scheduleVoteEvent) { -// Notifications notifications = new Notifications(); -// notifications.targetUser = scheduleVoteEvent.getUser(); -// Schedule schedule = scheduleVoteEvent.getSchedule(); -// notifications.title = schedule.getClub().getTitle() + ": 일정 참여"; -// notifications.category = "일정"; -// notifications.contents = schedule.getTitle() + " 일정에 참여했습니다."; -// notifications.isRead = false; -// return notifications; -// } - - public static Notifications createScheduleEncourageEvent(Schedule schedule, User targetUser) { - Notifications notifications = new Notifications(); - notifications.targetUser = targetUser; - notifications.title = " 일정 참가 투표 알림"; - notifications.category = "일정"; - notifications.contents = schedule.getTitle() + " 일정 참가 투표가 곧 마감됩니다.\n참가 투표를 해주세요."; - notifications.isRead = false; - return notifications; - } - - public static Notifications createMatchRequestEvent(MatchRequestEvent matchRequestEvent, User targetUser) { - Notifications notifications = new Notifications(); - notifications.targetUser = targetUser; - notifications.title = "매치 건의 알림"; - notifications.category = "친선 매치"; - notifications.contents = matchRequestEvent.getUser().getName() + "님이 " - + matchRequestEvent.getMatch().getStartTime().toLocalDate() - + matchRequestEvent.getMatch().getEvent() + "매치를 원합니다. \nt승인하시겠습니까?"; - notifications.isRead = false; - return notifications; - } - - public static Notifications createMatchInviteEvent(MatchInviteEvent matchInviteEvent, User targetUser) { - Notifications notifications = new Notifications(); - notifications.targetUser = targetUser; - notifications.title = "매치 초청 알림"; - notifications.category = "친선 매치"; - notifications.contents = "<" + matchInviteEvent.getMatch().getEvent() + " 한판 해요~> - " + - matchInviteEvent.getMatch().getHomeClub().getTitle() + "\n" + - matchInviteEvent.getMatch().getHomeClub().getTitle() + "가 친선 매치를 제안했습니다!"; - notifications.isRead = false; - return notifications; - } - - public static Notifications createMatchCancelUserNotification(MatchCancelUserEvent event, User targetUser) { - Notifications notification = new Notifications(); - notification.targetUser = targetUser; - notification.title = "매치 취소 알림"; - notification.category = "친선 매치"; - notification.contents = "매치 '" + event.getMatch().getName() + "'가 취소되었습니다."; - notification.isRead = false; - return notification; - } - - public static Notifications createMatchCancelClubNotification(MatchCancelClubEvent event, User targetUser) { - Notifications notification = new Notifications(); - notification.targetUser = targetUser; - notification.title = "매치 신청 취소 알림"; - notification.category = "친선 매치"; - notification.contents = "신청하신 매치 '" + event.getMatch().getName() + "'가 취소되었습니다."; - notification.isRead = false; - return notification; - } - - public void setRead(Boolean read) { - isRead = read; - } - - -} diff --git a/src/main/java/com/example/moim/notification/repository/NotificationRepository.java b/src/main/java/com/example/moim/notification/repository/NotificationRepository.java index 12b045d..cff7744 100644 --- a/src/main/java/com/example/moim/notification/repository/NotificationRepository.java +++ b/src/main/java/com/example/moim/notification/repository/NotificationRepository.java @@ -1,6 +1,6 @@ package com.example.moim.notification.repository; -import com.example.moim.notification.entity.Notifications; +import com.example.moim.notification.entity.Notification; import com.example.moim.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -9,9 +9,9 @@ import java.util.List; @Repository -public interface NotificationRepository extends JpaRepository { +public interface NotificationRepository extends JpaRepository { @Transactional(readOnly = true) Boolean existsByTargetUserAndIsRead(User user, Boolean isRead); - List findByTargetUser(User targetUser); + List findByTargetUser(User targetUser); } diff --git a/src/main/java/com/example/moim/notification/service/NotificationEventHandler.java b/src/main/java/com/example/moim/notification/service/NotificationEventHandler.java index 7358638..2e1b998 100644 --- a/src/main/java/com/example/moim/notification/service/NotificationEventHandler.java +++ b/src/main/java/com/example/moim/notification/service/NotificationEventHandler.java @@ -1,11 +1,9 @@ package com.example.moim.notification.service; -import com.example.moim.club.entity.UserClub; import com.example.moim.club.repository.UserClubRepository; import com.example.moim.notification.dto.*; -import com.example.moim.notification.entity.Notifications; +import com.example.moim.notification.entity.Notification; import com.example.moim.notification.repository.NotificationRepository; -import com.example.moim.user.entity.User; import com.example.moim.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -28,7 +26,7 @@ public class NotificationEventHandler { public void handleClubJoinEvent(ClubJoinEvent clubJoinEvent) { log.info("이벤트 들어옴"); sendEachNotification(userClubRepository.findAllByClub(clubJoinEvent.getClub()).stream() - .map(userClub -> Notifications.createClubJoinNotification(clubJoinEvent, userClub.getUser())).toList()); + .map(userClub -> Notification.createClubJoinNotification(clubJoinEvent, userClub.getUser())).toList()); } @Async @@ -36,7 +34,7 @@ public void handleClubJoinEvent(ClubJoinEvent clubJoinEvent) { public void handleScheduleSaveEvent(ScheduleSaveEvent scheduleSaveEvent) { log.info("이벤트 들어옴"); sendEachNotification(userClubRepository.findAllByClub(scheduleSaveEvent.getSchedule().getClub()).stream() - .map(userClub -> Notifications.createScheduleSaveNotification(scheduleSaveEvent, userClub.getUser())).toList()); + .map(userClub -> Notification.createScheduleSaveNotification(scheduleSaveEvent, userClub.getUser())).toList()); } @Async @@ -64,31 +62,34 @@ public void handleMatchCancelClubEvent(MatchCancelClubEvent event) { // @TransactionalEventListener // public void handleScheduleVoteEvent(ScheduleVoteEvent scheduleVoteEvent) { // log.info("이벤트 들어옴"); -// sendNotification(Notifications.createScheduleVoteNotification(scheduleVoteEvent)); +// sendNotification(Notification.createScheduleVoteNotification(scheduleVoteEvent)); // } @EventListener public void handleScheduleEncourageEvent(ScheduleEncourageEvent scheduleEncourageEvent) { log.info("이벤트 들어옴"); - sendEachNotification(scheduleEncourageEvent.getUserList().stream().map(user -> Notifications.createScheduleEncourageEvent(scheduleEncourageEvent.getSchedule(), user)).toList()); + sendEachNotification(scheduleEncourageEvent.getUserList().stream().map(user -> Notification.createScheduleEncourageEvent(scheduleEncourageEvent.getSchedule(), user)).toList()); } @EventListener public void handleMatchRequestEvent(MatchRequestEvent matchRequestEvent) { log.info("이벤트 들어옴"); sendEachNotification(userClubRepository.findAllByClub(matchRequestEvent.getClub()).stream() - .map(userClub -> Notifications.createMatchRequestEvent(matchRequestEvent, userClub.getUser())).toList()); + .map(userClub -> Notification.createMatchRequestEvent(matchRequestEvent, userClub.getUser())).toList()); } @EventListener public void handMatchInviteEvent(MatchInviteEvent matchInviteEvent) { log.info("이벤트 들어옴"); sendEachNotification(userClubRepository.findAdminByClub(matchInviteEvent.getClub()).stream() - .map(userClub -> Notifications.createMatchInviteEvent(matchInviteEvent, userClub.getUser())).toList()); + .map(userClub -> Notification.createMatchInviteEvent(matchInviteEvent, userClub.getUser())).toList()); } - - private void sendNotification(Notifications notification) { + /** + * FIXME: 사용자에게 알림을 보내는 메서드. sendEachNotification을 쓴다면 필요 없는거 아닌가? + * @param notification + */ + private void sendNotification(Notification notification) { // Message message = Message.builder() // .setToken(notification.getTargetUser().getFcmToken()) // .setNotification(Notification.builder() @@ -113,7 +114,11 @@ private void sendNotification(Notifications notification) { // } } - private void sendEachNotification(List notification) { + /** + * FIXME: 여러 사용자에게 알림을 보내는 메서드. 현재는 Notification 객체 저장 기능만 구현되어 있음. 추후 FCM 메시지 전송 기능 추가 필요. + * @param notification + */ + private void sendEachNotification(List notification) { // Message message = Message.builder() // .setToken(notification.getTargetUser().getFcmToken()) // .setNotification(Notification.builder() diff --git a/src/main/java/com/example/moim/user/entity/User.java b/src/main/java/com/example/moim/user/entity/User.java index 9d01e0d..7b0d866 100644 --- a/src/main/java/com/example/moim/user/entity/User.java +++ b/src/main/java/com/example/moim/user/entity/User.java @@ -21,6 +21,7 @@ @Table(name = "users") @Getter @NoArgsConstructor +@Table(name = "users") public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -52,7 +53,7 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) private List userClub = new ArrayList<>(); @OneToMany(mappedBy = "targetUser", cascade = CascadeType.REMOVE) - private List notifications = new ArrayList<>(); + private List notifications = new ArrayList<>(); public static User createUser(SignupInput signupInput) { User user = new User(); From 43664c64f8ff1e27f6c21fc1ef09938cf4b35e87 Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 13 Mar 2025 08:47:55 +0900 Subject: [PATCH 02/29] =?UTF-8?q?test(#10):=20NotificationService=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=86=8C=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 4243cbd83d7ebfe618208cf184dc258fd412d1d6) --- .../service/NotificationServiceTest.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/test/java/com/example/moim/notification/service/NotificationServiceTest.java diff --git a/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java new file mode 100644 index 0000000..bf72233 --- /dev/null +++ b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java @@ -0,0 +1,128 @@ +package com.example.moim.notification.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.example.moim.club.entity.Club; +import com.example.moim.notification.dto.ClubJoinEvent; +import com.example.moim.notification.dto.NotificationExistOutput; +import com.example.moim.notification.dto.NotificationOutput; +import com.example.moim.notification.entity.Notification; +import com.example.moim.notification.repository.NotificationRepository; +import com.example.moim.user.entity.User; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class NotificationServiceTest { + + @Mock + private NotificationRepository notificationRepository; + + @InjectMocks + private NotificationService notificationService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("사용자에게 읽지 않은 알림이 없으면 false 반환한다") + void unreadNotificationsNonExist() { + // given + User user = new User(); + when(notificationRepository.existsByTargetUserAndIsRead(user, false)).thenReturn(false); + + // when + NotificationExistOutput result = notificationService.checkNotice(user); + + // then + assertFalse(result.getHasNotice()); + } + + @Test + @DisplayName("사용자에게 읽지 않은 알림이 있으면 true 반환한다") + void unreadNotificationsExist() { + // given + User user = new User(); + when(notificationRepository.existsByTargetUserAndIsRead(user, false)).thenReturn(true); + + // when + NotificationExistOutput result = notificationService.checkNotice(user); + + // then + assertTrue(result.getHasNotice()); + } + + @Test + @DisplayName("사용자에게 온 알림이 없으면 빈 목록을 반환한다") + void findNoticeFail() { + // given + User user = new User(); + when(notificationRepository.findByTargetUser(user)).thenReturn(Collections.emptyList()); + + // when + List result = notificationService.findNotice(user); + + // then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("사용자에게 온 알림이 있으면 사용자의 알림 목록을 반환한다") + void findNoticeSuccess() { + // given + User user = new User(); + Club club = new Club(); + ClubJoinEvent clubJoinEvent = new ClubJoinEvent(user, club); + Notification notification = Notification.createClubJoinNotification(clubJoinEvent, user); + notification.setCreatedDate(); + List notifications = List.of(notification); + when(notificationRepository.findByTargetUser(user)).thenReturn(notifications); + + // when + List result = notificationService.findNotice(user); + + // then + assertEquals(1, result.size()); + } + + @Test + @DisplayName("존재하지 않는 ID를 삭제하려고 할 때 예외가 발생하지 않아도 된다") + void removeNoticeFail() { + // given + Long nonExistentId = 999L; + doNothing().when(notificationRepository).deleteById(nonExistentId); + + // when + notificationService.removeNotice(nonExistentId); + + // then + verify(notificationRepository, times(1)).deleteById(nonExistentId); + } + + @Test + @DisplayName("알림 ID로 알림을 삭제할 수 있다") + void removeNoticeSuccess() { + // given + Long notificationId = 1L; + doNothing().when(notificationRepository).deleteById(notificationId); + + // when + notificationService.removeNotice(notificationId); + + // then + verify(notificationRepository, times(1)).deleteById(notificationId); + } +} \ No newline at end of file From 1ea5797958b76728669b474e25b75c92a24f0731 Mon Sep 17 00:00:00 2001 From: yebin Date: Mon, 17 Mar 2025 01:30:10 +0900 Subject: [PATCH 03/29] =?UTF-8?q?test(#10):=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=AA=85=EC=9D=B4=20=ED=96=89=EB=8F=99=EC=9D=84=20=EC=9E=98=20?= =?UTF-8?q?=EB=AC=98=EC=82=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=86=8C=EB=93=9C=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 16c7c990b399a1fc010207dd6d96d69c5e4f4ac1) --- .../service/NotificationServiceTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java index bf72233..b25f310 100644 --- a/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java +++ b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java @@ -39,7 +39,7 @@ void setUp() { @Test @DisplayName("사용자에게 읽지 않은 알림이 없으면 false 반환한다") - void unreadNotificationsNonExist() { + void shouldReturnFalseWhenNoUnreadNotifications() { // given User user = new User(); when(notificationRepository.existsByTargetUserAndIsRead(user, false)).thenReturn(false); @@ -53,7 +53,7 @@ void unreadNotificationsNonExist() { @Test @DisplayName("사용자에게 읽지 않은 알림이 있으면 true 반환한다") - void unreadNotificationsExist() { + void shouldReturnTrueWhenUnreadNotificationsExist() { // given User user = new User(); when(notificationRepository.existsByTargetUserAndIsRead(user, false)).thenReturn(true); @@ -67,7 +67,7 @@ void unreadNotificationsExist() { @Test @DisplayName("사용자에게 온 알림이 없으면 빈 목록을 반환한다") - void findNoticeFail() { + void shouldReturnEmptyListWhenNoNotificationsReceived() { // given User user = new User(); when(notificationRepository.findByTargetUser(user)).thenReturn(Collections.emptyList()); @@ -81,7 +81,7 @@ void findNoticeFail() { @Test @DisplayName("사용자에게 온 알림이 있으면 사용자의 알림 목록을 반환한다") - void findNoticeSuccess() { + void shouldReturnNotificationListWhenNotificationsExist() { // given User user = new User(); Club club = new Club(); @@ -100,7 +100,7 @@ void findNoticeSuccess() { @Test @DisplayName("존재하지 않는 ID를 삭제하려고 할 때 예외가 발생하지 않아도 된다") - void removeNoticeFail() { + void shouldNotThrowExceptionWhenRemovingNonExistentNotification() { // given Long nonExistentId = 999L; doNothing().when(notificationRepository).deleteById(nonExistentId); @@ -114,7 +114,7 @@ void removeNoticeFail() { @Test @DisplayName("알림 ID로 알림을 삭제할 수 있다") - void removeNoticeSuccess() { + void shouldDeleteNotificationById() { // given Long notificationId = 1L; doNothing().when(notificationRepository).deleteById(notificationId); From b793501f70b73e7639968fc2126e36025471c046 Mon Sep 17 00:00:00 2001 From: yebin Date: Fri, 21 Mar 2025 18:58:03 +0900 Subject: [PATCH 04/29] =?UTF-8?q?test(#10):=20Mockito=20=EC=A3=BC=EC=9E=85?= =?UTF-8?q?=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit c7c8f6903cfd69c3fad54dd8f458c823d27bbc67) --- .../notification/service/NotificationServiceTest.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java index b25f310..78b3569 100644 --- a/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java +++ b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java @@ -17,13 +17,14 @@ import com.example.moim.user.entity.User; import java.util.Collections; import java.util.List; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) class NotificationServiceTest { @Mock @@ -32,11 +33,6 @@ class NotificationServiceTest { @InjectMocks private NotificationService notificationService; - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - @Test @DisplayName("사용자에게 읽지 않은 알림이 없으면 false 반환한다") void shouldReturnFalseWhenNoUnreadNotifications() { From c35f31b8e0560a4739b93d74d6747c0ea7582255 Mon Sep 17 00:00:00 2001 From: yebin Date: Fri, 28 Mar 2025 16:32:41 +0900 Subject: [PATCH 05/29] =?UTF-8?q?refactor(#20):=20API=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 9cbea0dbe4d509aa3141a49c298134a9dcc70dcb) --- .../moim/user/controller/MypageController.java | 12 ++++++++---- .../moim/user/controller/MypageControllerDocs.java | 7 ++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/moim/user/controller/MypageController.java b/src/main/java/com/example/moim/user/controller/MypageController.java index 8c2f8b5..10e96d4 100644 --- a/src/main/java/com/example/moim/user/controller/MypageController.java +++ b/src/main/java/com/example/moim/user/controller/MypageController.java @@ -1,5 +1,7 @@ package com.example.moim.user.controller; +import com.example.moim.global.exception.BaseResponse; +import com.example.moim.global.exception.ResponseCode; import com.example.moim.user.dto.MypageClubOutput; import com.example.moim.user.dto.UserDetailsImpl; import com.example.moim.user.dto.UserUpdateInput; @@ -18,17 +20,19 @@ public class MypageController implements MypageControllerDocs { private final UserService userService; @PatchMapping(value = "/user/info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public void userInfoUpdate(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @ModelAttribute UserUpdateInput userUpdateInput) throws IOException { + public BaseResponse userInfoUpdate(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @ModelAttribute UserUpdateInput userUpdateInput) throws IOException { userService.updateUserInfo(userDetailsImpl.getUser(), userUpdateInput); + return BaseResponse.onSuccess(null, ResponseCode.OK); } @GetMapping("/user/club/mypage") - public List findMypageClub(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl) { - return userService.findMypageClub(userDetailsImpl.getUser()); + public BaseResponse> findMypageClub(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl) { + return BaseResponse.onSuccess(userService.findMypageClub(userDetailsImpl.getUser()), ResponseCode.OK); } @DeleteMapping("/user/club/{userClubId}") - public void userClubDelete(@PathVariable Long userClubId, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl) { + public BaseResponse userClubDelete(@PathVariable Long userClubId, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl) { userService.deleteUserClub(userClubId); + return BaseResponse.onSuccess(null, ResponseCode.OK); } } diff --git a/src/main/java/com/example/moim/user/controller/MypageControllerDocs.java b/src/main/java/com/example/moim/user/controller/MypageControllerDocs.java index 3ffc25e..05418cc 100644 --- a/src/main/java/com/example/moim/user/controller/MypageControllerDocs.java +++ b/src/main/java/com/example/moim/user/controller/MypageControllerDocs.java @@ -1,5 +1,6 @@ package com.example.moim.user.controller; +import com.example.moim.global.exception.BaseResponse; import com.example.moim.user.dto.MypageClubOutput; import com.example.moim.user.dto.UserDetailsImpl; import com.example.moim.user.dto.UserUpdateInput; @@ -17,11 +18,11 @@ public interface MypageControllerDocs { @Operation(summary = "유저 정보 수정") - void userInfoUpdate(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @ModelAttribute @Valid UserUpdateInput userUpdateInput) throws IOException; + BaseResponse userInfoUpdate(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @ModelAttribute @Valid UserUpdateInput userUpdateInput) throws IOException; @Operation(summary = "마이페이지 내가 속한 모임 조회") - List findMypageClub(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl); + BaseResponse> findMypageClub(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl); @Operation(summary = "유저 소속 모임 탈퇴") - void userClubDelete(@PathVariable Long clubId, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl); + BaseResponse userClubDelete(@PathVariable Long clubId, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl); } From 6cf35159a5e7bcdab1f8488f9727e96f32a9dbdc Mon Sep 17 00:00:00 2001 From: yebin Date: Fri, 28 Mar 2025 19:43:38 +0900 Subject: [PATCH 06/29] =?UTF-8?q?refactor(#20):=20NotificationController?= =?UTF-8?q?=EC=97=90=EC=84=9C=20Notice=EB=A1=9C=20=EC=9E=98=EB=AA=BB=20?= =?UTF-8?q?=EC=A4=84=EC=97=AC=20=EC=93=B4=20=EB=B6=80=EB=B6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit eb5511c8e0e3b8512b3453e141544d2736b65e5c) # Conflicts: # src/main/java/com/example/moim/notification/controller/NotificationController.java --- .../controller/NotificationController.java | 18 +++++++++--------- .../controller/NotificationControllerDocs.java | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/example/moim/notification/controller/NotificationController.java b/src/main/java/com/example/moim/notification/controller/NotificationController.java index c0508c3..b6e3b80 100644 --- a/src/main/java/com/example/moim/notification/controller/NotificationController.java +++ b/src/main/java/com/example/moim/notification/controller/NotificationController.java @@ -18,18 +18,18 @@ public class NotificationController implements NotificationControllerDocs{ private final NotificationService notificationService; - @GetMapping(value = "/notice") - public NotificationExistOutput noticeCheck(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl) { - return notificationService.checkNotice(userDetailsImpl.getUser()); + @GetMapping(value = "/notifications/unread-count") + public NotificationExistOutput notificationUnreadCount(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl) { + return notificationService.checkNotificationUnread(userDetailsImpl.getUser()); } - @GetMapping("/notices") - public List noticeFind(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl) { - return notificationService.findNotice(userDetailsImpl.getUser()); + @GetMapping("/notifications") + public List notificationFind(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl) { + return notificationService.findNotifications(userDetailsImpl.getUser()); } - @DeleteMapping("/notices/{id}") - public void noticeRemove(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @PathVariable Long id) { - notificationService.removeNotice(id); + @DeleteMapping("/notifications/{id}") + public void notificationRemove(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @PathVariable Long id) { + notificationService.removeNotification(id); } } diff --git a/src/main/java/com/example/moim/notification/controller/NotificationControllerDocs.java b/src/main/java/com/example/moim/notification/controller/NotificationControllerDocs.java index 55ada0a..791ce83 100644 --- a/src/main/java/com/example/moim/notification/controller/NotificationControllerDocs.java +++ b/src/main/java/com/example/moim/notification/controller/NotificationControllerDocs.java @@ -14,11 +14,11 @@ public interface NotificationControllerDocs { @Operation(summary = "새로운 알림 있는지 체크") - NotificationExistOutput noticeCheck(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl); + NotificationExistOutput notificationUnreadCount(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl); @Operation(summary = "알림 조회") - List noticeFind(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl); + List notificationFind(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl); @Operation(summary = "알림 삭제") - void noticeRemove(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @PathVariable Long id); + void notificationRemove(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @PathVariable Long id); } From 23166a1f4caf2216e8e8257325aa1313f2d8af25 Mon Sep 17 00:00:00 2001 From: yebin Date: Fri, 28 Mar 2025 20:23:31 +0900 Subject: [PATCH 07/29] =?UTF-8?q?chore(#20):=20FCM=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=B9=84=EA=B3=B5=EA=B0=9C=20?= =?UTF-8?q?=ED=82=A4=20=EC=88=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 877b3f29542ab31d4bb6518853b7de181c93f170) # Conflicts: # .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2ff654f..87be0d8 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ build/generated/ application-local.properties application-test.properties application-s3.properties -test_img_dir \ No newline at end of file +test_img_dir + +**/src/main/resources/secret From 83db59d4eb43b1a535e9cc58445dc544b24c4b80 Mon Sep 17 00:00:00 2001 From: yebin Date: Sun, 30 Mar 2025 23:38:32 +0900 Subject: [PATCH 08/29] =?UTF-8?q?test(#10):=20Notification=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EB=8B=A8=EC=9C=84=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 (cherry picked from commit 0d8343af1e7ea12119beab94ce3277473ca54857) --- .../service/NotificationEventHandlerTest.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/test/java/com/example/moim/notification/service/NotificationEventHandlerTest.java diff --git a/src/test/java/com/example/moim/notification/service/NotificationEventHandlerTest.java b/src/test/java/com/example/moim/notification/service/NotificationEventHandlerTest.java new file mode 100644 index 0000000..e70bec4 --- /dev/null +++ b/src/test/java/com/example/moim/notification/service/NotificationEventHandlerTest.java @@ -0,0 +1,85 @@ +package com.example.moim.notification.service; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.example.moim.club.dto.ClubInput; +import com.example.moim.club.entity.Club; +import com.example.moim.club.entity.UserClub; +import com.example.moim.club.repository.UserClubRepository; +import com.example.moim.notification.dto.ClubJoinEvent; +import com.example.moim.notification.entity.Notification; +import com.example.moim.user.dto.SignupInput; +import com.example.moim.user.entity.Gender; +import com.example.moim.user.entity.User; +import java.util.List; +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.mock.web.MockMultipartFile; + +@ExtendWith(MockitoExtension.class) +class NotificationEventHandlerTest { + + @Mock + private UserClubRepository userClubRepository; + + @Mock + private PushNotificationService fcmPushNotificationService; + + @InjectMocks + private NotificationEventHandler notificationEventHandler; + + @Test + void handleClubJoinEvent() { + // given + SignupInput signupInput = SignupInput.builder() + .email("test@email.com") + .password("password") + .name("name") + .birthday("2000/05/05") + .gender(Gender.MAN) + .phone("01012341234") + .build(); + User joinedMember = User.createUser(signupInput); + + ClubInput clubInput = ClubInput.builder() + .title("amazing title") + .explanation("explanation") + .introduction("introduction") + .category("category") + .university("university") + .gender("gender") + .activityArea("activityArea") + .ageRange("ageRange") + .mainEvent("mainEvent") + .clubPassword("clubPassword") + .profileImg(new MockMultipartFile("profileImg", "profileImg".getBytes())) + .mainUniformColor("mainUniformColor") + .subUniformColor("subUniformColor") + .build(); + Club club = Club.createClub(clubInput, null); + + UserClub userClub = UserClub.createUserClub(joinedMember, club); + List userClubs = List.of(userClub); + + ClubJoinEvent clubJoinEvent = new ClubJoinEvent(joinedMember, club); + + // when + when(userClubRepository.findAllByClub(club)).thenReturn(userClubs); + notificationEventHandler.handleClubJoinEvent(clubJoinEvent); + + // then + verify(fcmPushNotificationService, times(1)).sendEachNotification(argThat(notifications -> { + Notification notification = notifications.get(0); + return notification.getTargetUser().equals(joinedMember) && + notification.getTitle().equals("가입 알림") && + notification.getCategory().equals("모임") && + notification.getContents().equals(joinedMember.getName() + " 님이 " + club.getTitle() + "에 가입했습니다."); + })); + } +} \ No newline at end of file From 704dbea5875660bea7d4f8b9943ce9c960ac8757 Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 3 Apr 2025 06:23:45 +0900 Subject: [PATCH 09/29] =?UTF-8?q?feat(#20):=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=96=B4=EB=93=9C=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 35024db4bf1390077bfefe37b33c1c21e03fd1d9) --- .../advice/NotificationControllerAdvice.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/java/com/example/moim/notification/exceptions/advice/NotificationControllerAdvice.java diff --git a/src/main/java/com/example/moim/notification/exceptions/advice/NotificationControllerAdvice.java b/src/main/java/com/example/moim/notification/exceptions/advice/NotificationControllerAdvice.java new file mode 100644 index 0000000..0c7bbe5 --- /dev/null +++ b/src/main/java/com/example/moim/notification/exceptions/advice/NotificationControllerAdvice.java @@ -0,0 +1,10 @@ +package com.example.moim.notification.exceptions.advice; + +import com.example.moim.global.exception.GeneralException; +import com.example.moim.global.exception.ResponseCode; + +public class NotificationControllerAdvice extends GeneralException { + public NotificationControllerAdvice(ResponseCode responseCode) { + super(responseCode); + } +} From 10ebfa56c973a6b8d2429789395296c896b4e8bf Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:07:25 +0900 Subject: [PATCH 10/29] =?UTF-8?q?refactor(#20):=20OCP=20=EC=9C=84=EB=B0=98?= =?UTF-8?q?=ED=95=98=EB=8A=94=20NotificationEventHandler=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationEventHandler.java | 145 ------------------ .../service/NotificationEventHandlerTest.java | 85 ---------- 2 files changed, 230 deletions(-) delete mode 100644 src/main/java/com/example/moim/notification/service/NotificationEventHandler.java delete mode 100644 src/test/java/com/example/moim/notification/service/NotificationEventHandlerTest.java diff --git a/src/main/java/com/example/moim/notification/service/NotificationEventHandler.java b/src/main/java/com/example/moim/notification/service/NotificationEventHandler.java deleted file mode 100644 index 2e1b998..0000000 --- a/src/main/java/com/example/moim/notification/service/NotificationEventHandler.java +++ /dev/null @@ -1,145 +0,0 @@ -package com.example.moim.notification.service; - -import com.example.moim.club.repository.UserClubRepository; -import com.example.moim.notification.dto.*; -import com.example.moim.notification.entity.Notification; -import com.example.moim.notification.repository.NotificationRepository; -import com.example.moim.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Slf4j -@Component -@RequiredArgsConstructor -public class NotificationEventHandler { - private final NotificationRepository notificationRepository; - private final UserClubRepository userClubRepository; - private final UserRepository userRepository; - - @Async - @EventListener - public void handleClubJoinEvent(ClubJoinEvent clubJoinEvent) { - log.info("이벤트 들어옴"); - sendEachNotification(userClubRepository.findAllByClub(clubJoinEvent.getClub()).stream() - .map(userClub -> Notification.createClubJoinNotification(clubJoinEvent, userClub.getUser())).toList()); - } - - @Async - @EventListener - public void handleScheduleSaveEvent(ScheduleSaveEvent scheduleSaveEvent) { - log.info("이벤트 들어옴"); - sendEachNotification(userClubRepository.findAllByClub(scheduleSaveEvent.getSchedule().getClub()).stream() - .map(userClub -> Notification.createScheduleSaveNotification(scheduleSaveEvent, userClub.getUser())).toList()); - } - - @Async - @EventListener - public void handleMatchCancelUserEvent(MatchCancelUserEvent event) { - log.info("매치 취소 이벤트 발생 매치 ID: {}", event.getMatch().getId()); - Notifications notification = Notifications.createMatchCancelUserNotification(event, event.getTargetUser()); - sendNotification(notification); - } - - @Async - @EventListener - public void handleMatchCancelClubEvent(MatchCancelClubEvent event) { - log.info("매치 취소 이벤트 발생 매치 ID: {}", event.getMatch().getId()); - // 해당 이벤트 대상 클럽의 모든 사용자에게 알림 생성 - List notifications = userClubRepository.findAllByClub(event.getTargetClub()) - .stream() - .map(userClub -> Notifications.createMatchCancelClubNotification(event, userClub.getUser())) - .toList(); - sendEachNotification(notifications); - } - -// @Async -// @Transactional(propagation = Propagation.REQUIRES_NEW) -// @TransactionalEventListener -// public void handleScheduleVoteEvent(ScheduleVoteEvent scheduleVoteEvent) { -// log.info("이벤트 들어옴"); -// sendNotification(Notification.createScheduleVoteNotification(scheduleVoteEvent)); -// } - - @EventListener - public void handleScheduleEncourageEvent(ScheduleEncourageEvent scheduleEncourageEvent) { - log.info("이벤트 들어옴"); - sendEachNotification(scheduleEncourageEvent.getUserList().stream().map(user -> Notification.createScheduleEncourageEvent(scheduleEncourageEvent.getSchedule(), user)).toList()); - } - - @EventListener - public void handleMatchRequestEvent(MatchRequestEvent matchRequestEvent) { - log.info("이벤트 들어옴"); - sendEachNotification(userClubRepository.findAllByClub(matchRequestEvent.getClub()).stream() - .map(userClub -> Notification.createMatchRequestEvent(matchRequestEvent, userClub.getUser())).toList()); - } - - @EventListener - public void handMatchInviteEvent(MatchInviteEvent matchInviteEvent) { - log.info("이벤트 들어옴"); - sendEachNotification(userClubRepository.findAdminByClub(matchInviteEvent.getClub()).stream() - .map(userClub -> Notification.createMatchInviteEvent(matchInviteEvent, userClub.getUser())).toList()); - } - - /** - * FIXME: 사용자에게 알림을 보내는 메서드. sendEachNotification을 쓴다면 필요 없는거 아닌가? - * @param notification - */ - private void sendNotification(Notification notification) { -// Message message = Message.builder() -// .setToken(notification.getTargetUser().getFcmToken()) -// .setNotification(Notification.builder() -// .setTitle(notification.getTitle()) -// .setBody(notification.getContent()) -// .build()) -// .setWebpushConfig(WebpushConfig.builder().setNotification(WebpushNotification.builder() -// .setBadge("https://media.discordapp.net/attachments/1081467110200451092/1199660879071952906/MG.png?ex=65c35a42&is=65b0e542&hm=4b109d5d3ab5850eeacbd46d8f8eed253ab7bdb7ac4d28111e57cad3aba58b98&=&format=webp&quality=lossless") -// .build()).build()) -// .setAndroidConfig(AndroidConfig.builder().setNotification(AndroidNotification.builder() -// .setPriority(AndroidNotification.Priority.HIGH) -// .build()).build()) -// .setApnsConfig() -// .build(); -// -// try { -// String response = FirebaseMessaging.getInstance().send(message); -// log.info("Successfully sent message: {}", response); - notificationRepository.save(notification); -// } catch (FirebaseMessagingException e) { -// log.error("cannot send to user push message. error info : {}", e.getMessage()); -// } - } - - /** - * FIXME: 여러 사용자에게 알림을 보내는 메서드. 현재는 Notification 객체 저장 기능만 구현되어 있음. 추후 FCM 메시지 전송 기능 추가 필요. - * @param notification - */ - private void sendEachNotification(List notification) { -// Message message = Message.builder() -// .setToken(notification.getTargetUser().getFcmToken()) -// .setNotification(Notification.builder() -// .setTitle(notification.getTitle()) -// .setBody(notification.getContent()) -// .build()) -// .setWebpushConfig(WebpushConfig.builder().setNotification(WebpushNotification.builder() -// .setBadge("https://media.discordapp.net/attachments/1081467110200451092/1199660879071952906/MG.png?ex=65c35a42&is=65b0e542&hm=4b109d5d3ab5850eeacbd46d8f8eed253ab7bdb7ac4d28111e57cad3aba58b98&=&format=webp&quality=lossless") -// .build()).build()) -// .setAndroidConfig(AndroidConfig.builder().setNotification(AndroidNotification.builder() -// .setPriority(AndroidNotification.Priority.HIGH) -// .build()).build()) -// .setApnsConfig() -// .build(); -// -// try { -// String response = FirebaseMessaging.getInstance().sendEach(message); -// log.info("Successfully sent message: {}", response); - notificationRepository.saveAll(notification); -// } catch (FirebaseMessagingException e) { -// log.error("cannot send to user push message. error info : {}", e.getMessage()); -// } - } -} diff --git a/src/test/java/com/example/moim/notification/service/NotificationEventHandlerTest.java b/src/test/java/com/example/moim/notification/service/NotificationEventHandlerTest.java deleted file mode 100644 index e70bec4..0000000 --- a/src/test/java/com/example/moim/notification/service/NotificationEventHandlerTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.example.moim.notification.service; - -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.example.moim.club.dto.ClubInput; -import com.example.moim.club.entity.Club; -import com.example.moim.club.entity.UserClub; -import com.example.moim.club.repository.UserClubRepository; -import com.example.moim.notification.dto.ClubJoinEvent; -import com.example.moim.notification.entity.Notification; -import com.example.moim.user.dto.SignupInput; -import com.example.moim.user.entity.Gender; -import com.example.moim.user.entity.User; -import java.util.List; -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.mock.web.MockMultipartFile; - -@ExtendWith(MockitoExtension.class) -class NotificationEventHandlerTest { - - @Mock - private UserClubRepository userClubRepository; - - @Mock - private PushNotificationService fcmPushNotificationService; - - @InjectMocks - private NotificationEventHandler notificationEventHandler; - - @Test - void handleClubJoinEvent() { - // given - SignupInput signupInput = SignupInput.builder() - .email("test@email.com") - .password("password") - .name("name") - .birthday("2000/05/05") - .gender(Gender.MAN) - .phone("01012341234") - .build(); - User joinedMember = User.createUser(signupInput); - - ClubInput clubInput = ClubInput.builder() - .title("amazing title") - .explanation("explanation") - .introduction("introduction") - .category("category") - .university("university") - .gender("gender") - .activityArea("activityArea") - .ageRange("ageRange") - .mainEvent("mainEvent") - .clubPassword("clubPassword") - .profileImg(new MockMultipartFile("profileImg", "profileImg".getBytes())) - .mainUniformColor("mainUniformColor") - .subUniformColor("subUniformColor") - .build(); - Club club = Club.createClub(clubInput, null); - - UserClub userClub = UserClub.createUserClub(joinedMember, club); - List userClubs = List.of(userClub); - - ClubJoinEvent clubJoinEvent = new ClubJoinEvent(joinedMember, club); - - // when - when(userClubRepository.findAllByClub(club)).thenReturn(userClubs); - notificationEventHandler.handleClubJoinEvent(clubJoinEvent); - - // then - verify(fcmPushNotificationService, times(1)).sendEachNotification(argThat(notifications -> { - Notification notification = notifications.get(0); - return notification.getTargetUser().equals(joinedMember) && - notification.getTitle().equals("가입 알림") && - notification.getCategory().equals("모임") && - notification.getContents().equals(joinedMember.getName() + " 님이 " + club.getTitle() + "에 가입했습니다."); - })); - } -} \ No newline at end of file From f9064d8e12532999bb85c3c5d7b386a8f595cceb Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:08:52 +0900 Subject: [PATCH 11/29] =?UTF-8?q?refactor(#20):=20Notification=20=EC=9D=B4?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=EB=90=98=EB=8A=94=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=B4=EB=9D=BC=20NotificationEntity=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - com.google.firebase.messaging.Notification과 중복 --- .../notification/dto/NotificationOutput.java | 26 ++--- .../notification/entity/Notification.java | 97 ------------------- .../entity/NotificationEntity.java | 68 +++++++++++++ .../entity/NotificationStatus.java | 8 ++ .../notification/entity/NotificationType.java | 37 +++++++ .../repository/NotificationRepository.java | 6 +- 6 files changed, 129 insertions(+), 113 deletions(-) delete mode 100644 src/main/java/com/example/moim/notification/entity/Notification.java create mode 100644 src/main/java/com/example/moim/notification/entity/NotificationEntity.java create mode 100644 src/main/java/com/example/moim/notification/entity/NotificationStatus.java create mode 100644 src/main/java/com/example/moim/notification/entity/NotificationType.java diff --git a/src/main/java/com/example/moim/notification/dto/NotificationOutput.java b/src/main/java/com/example/moim/notification/dto/NotificationOutput.java index ced90fe..f1aacc9 100644 --- a/src/main/java/com/example/moim/notification/dto/NotificationOutput.java +++ b/src/main/java/com/example/moim/notification/dto/NotificationOutput.java @@ -1,26 +1,26 @@ package com.example.moim.notification.dto; -import com.example.moim.notification.entity.Notification; -import lombok.Data; - +import com.example.moim.notification.entity.NotificationEntity; import java.time.format.DateTimeFormatter; +import lombok.Data; @Data public class NotificationOutput { private Long id; + private String notificationType; private String title; - private String category; private String content; - private String time; + private String createdAt; private Boolean isRead; + private String actionUrl; - public NotificationOutput(Notification notification) { - this.id = notification.getId(); - this.title = notification.getTitle(); - this.category = notification.getCategory(); - this.content = notification.getContents(); - this.time = notification.getCreatedDate().format(DateTimeFormatter.ofPattern("MM/dd HH:mm")); - notification.setRead(true); - this.isRead = notification.getIsRead(); + public NotificationOutput(NotificationEntity notificationEntity) { + this.id = notificationEntity.getId(); + this.title = notificationEntity.getTitle(); + this.notificationType = notificationEntity.getType().name(); + this.content = notificationEntity.getContent(); + this.createdAt = notificationEntity.getCreatedDate().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm")); + this.isRead = notificationEntity.getIsRead(); + this.actionUrl = notificationEntity.getType().getCategory() + "/" + notificationEntity.getLinkedId(); } } diff --git a/src/main/java/com/example/moim/notification/entity/Notification.java b/src/main/java/com/example/moim/notification/entity/Notification.java deleted file mode 100644 index 4dc8845..0000000 --- a/src/main/java/com/example/moim/notification/entity/Notification.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.example.moim.notification.entity; - -import com.example.moim.schedule.entity.Schedule; -import com.example.moim.global.entity.BaseEntity; -import com.example.moim.notification.dto.*; -import com.example.moim.user.entity.User; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor -@Table(name = "notifications") -public class Notification extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @ManyToOne(fetch = FetchType.LAZY) - private User targetUser; - private String title; - private String category; - private String contents; - private Boolean isRead; - - public static Notification createClubJoinNotification(ClubJoinEvent clubJoinEvent, User targetUser) { - Notification notification = new Notification(); - notification.targetUser = targetUser; - notification.title = "가입 알림"; - notification.category = "모임"; - notification.contents = clubJoinEvent.getUser().getName() + " 님이 " + clubJoinEvent.getClub().getTitle() + "에 가입했습니다."; - notification.isRead = false; - return notification; - } - - public static Notification createScheduleSaveNotification(ScheduleSaveEvent scheduleSaveEvent, User targetUser) { - Notification notification = new Notification(); - notification.targetUser = targetUser; - Schedule schedule = scheduleSaveEvent.getSchedule(); - notification.title = "일정 등록 알림"; - notification.category = "일정"; - notification.contents = schedule.getTitle() + " 일정이 등록되었습니다.\n참가신청 바로가기!"; - notification.isRead = false; - return notification; - } - -// public static Notification createScheduleVoteNotification(ScheduleVoteEvent scheduleVoteEvent) { -// Notification notifications = new Notification(); -// notifications.targetUser = scheduleVoteEvent.getUser(); -// Schedule schedule = scheduleVoteEvent.getSchedule(); -// notifications.title = schedule.getClub().getTitle() + ": 일정 참여"; -// notifications.category = "일정"; -// notifications.contents = schedule.getTitle() + " 일정에 참여했습니다."; -// notifications.isRead = false; -// return notifications; -// } - - public static Notification createScheduleEncourageEvent(Schedule schedule, User targetUser) { - Notification notification = new Notification(); - notification.targetUser = targetUser; - notification.title = " 일정 참가 투표 알림"; - notification.category = "일정"; - notification.contents = schedule.getTitle() + " 일정 참가 투표가 곧 마감됩니다.\n참가 투표를 해주세요."; - notification.isRead = false; - return notification; - } - - public static Notification createMatchRequestEvent(MatchRequestEvent matchRequestEvent, User targetUser) { - Notification notification = new Notification(); - notification.targetUser = targetUser; - notification.title = "매치 건의 알림"; - notification.category = "친선 매치"; - notification.contents = matchRequestEvent.getUser().getName() + "님이 " - + matchRequestEvent.getMatch().getStartTime().toLocalDate() - + matchRequestEvent.getMatch().getEvent() + "매치를 원합니다. \nt승인하시겠습니까?"; - notification.isRead = false; - return notification; - } - - public static Notification createMatchInviteEvent(MatchInviteEvent matchInviteEvent, User targetUser) { - Notification notification = new Notification(); - notification.targetUser = targetUser; - notification.title = "매치 초청 알림"; - notification.category = "친선 매치"; - notification.contents = "<" + matchInviteEvent.getMatch().getEvent() + " 한판 해요~> - " + - matchInviteEvent.getMatch().getHomeClub().getTitle() + "\n" + - matchInviteEvent.getMatch().getHomeClub().getTitle() + "가 친선 매치를 제안했습니다!"; - notification.isRead = false; - return notification; - } - - public void setRead(Boolean read) { - isRead = read; - } - - -} diff --git a/src/main/java/com/example/moim/notification/entity/NotificationEntity.java b/src/main/java/com/example/moim/notification/entity/NotificationEntity.java new file mode 100644 index 0000000..7ba2fad --- /dev/null +++ b/src/main/java/com/example/moim/notification/entity/NotificationEntity.java @@ -0,0 +1,68 @@ +package com.example.moim.notification.entity; + +import com.example.moim.global.entity.BaseEntity; +import com.example.moim.user.entity.User; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "notifications") +public class NotificationEntity extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + private User targetUser; + @Enumerated(EnumType.STRING) + private NotificationType type; + private String title; + private String content; + private Long linkedId; // 알림과 연관된 클럽, 일정, 매치 등의 ID + private Boolean isRead; + private NotificationStatus status; + + @Builder + private NotificationEntity(User targetUser, NotificationType type, String title, String content, Long linkedId) { + this.targetUser = targetUser; + this.type = type; + this.title = title; + this.content = content; + this.linkedId = linkedId; + this.isRead = false; + this.status = NotificationStatus.READY; + } + + public static NotificationEntity create(User targetUser, NotificationType type, String content, String title, Long linkedId) { + return NotificationEntity.builder() + .targetUser(targetUser) + .type(type) + .content(content) + .title(title) + .linkedId(linkedId) + .build(); + } + + public void read() { + isRead = true; + } + + public void sent() { + status = NotificationStatus.SENT; + } + + public void failed() { + status = NotificationStatus.FAILED; + } +} diff --git a/src/main/java/com/example/moim/notification/entity/NotificationStatus.java b/src/main/java/com/example/moim/notification/entity/NotificationStatus.java new file mode 100644 index 0000000..f465522 --- /dev/null +++ b/src/main/java/com/example/moim/notification/entity/NotificationStatus.java @@ -0,0 +1,8 @@ +package com.example.moim.notification.entity; + +public enum NotificationStatus { + READY, + SENT, + FAILED, + ; +} diff --git a/src/main/java/com/example/moim/notification/entity/NotificationType.java b/src/main/java/com/example/moim/notification/entity/NotificationType.java new file mode 100644 index 0000000..937e9ed --- /dev/null +++ b/src/main/java/com/example/moim/notification/entity/NotificationType.java @@ -0,0 +1,37 @@ +package com.example.moim.notification.entity; + +import java.util.IllegalFormatException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NotificationType { + CLUB_JOIN("클럽 가입", "%s님이 %s에 가입했습니다."), + SCHEDULE_SAVE("일정 등록", "%s 일정이 등록되었습니다. \n 참가 여부를 투표해주세요!!"), + SCHEDULE_REMINDER("일정 하루 전", "내일 %s 일정이 있습니다."), + SCHEDULE_ENCOURAGE("투표 독려", "%s 일정이 참가투표가 곧 마감됩니다.\n 참가 여부를 투표해주세요!!"), + SCHEDULE_JOIN("일정 참여", "%s 일정에 참여했습니다."), + MATCH_SCHEDULED("매치 등록", "%s 클럽 %s %s 매치가 등록되었습니다.\n 매치정보를 확인하고 신청해주세요!"), + MATCH_SUCCESS("매칭 성공", "%s 클럽과의 %s %s 매치가 확정되었습니다.\n 매치정보를 다시 한 번 확인해주세요!"), + MATCH_REVIEW("매치 리뷰", "%s 클럽과의 매치는 즐거우셨나요?\n %s 님의 득점 기록을 입력해주세요!"), + MATCH_SUGGESTION("매치 건의", "클럽원이 %s 클럽과의 %s %s 매치를 원합니다.\n 매치 정보를 확인하고 신청해주세요!"), + MATCH_REQUEST("매치 요청", "%s 클럽이 %s 매치에 신청했습니다.\n 클럽 정보를 확인하고 매치를 확정해주세요!"), + MATCH_INVITE("매치 초대", "%s 클럽에서 친선 매치를 제안했습니다.\n 클럽 정보를 확인하고 매치를 확정해주세요!"), + MATCH_FAILED_UNREQUESTED("매치 실패", "신청 클럽이 없어 <%s> 매치가 성사되지않았습니다 \uD83D\uDE2D\n 다음에 다시 등록해주세요!"), + MATCH_FAILED_UNSELECTED("매치 실패", "<%s> 매치 등록 클럽이 다른 클럽을 선택했어요\uD83E\uDEE3\n 다음에 다시 신청해주세요!"), + MATCH_CANCEL_USER("매치 취소", "<%s> 매치가 취소되었습니다.\n 다음에 다시 신청해주세요!"), + MATCH_CANCEL_CLUB("매치 취소", "<%s> 매치가 취소되었습니다.\n 다음에 다시 신청해주세요!"), + ; + + private final String title; + private final String messageTemplate; + + public String formatMessage(Object... args) throws IllegalFormatException { + return String.format(messageTemplate, args); + } + + public String getCategory() { + return name().substring(0, name().indexOf("_")).toLowerCase(); + } +} diff --git a/src/main/java/com/example/moim/notification/repository/NotificationRepository.java b/src/main/java/com/example/moim/notification/repository/NotificationRepository.java index cff7744..e632071 100644 --- a/src/main/java/com/example/moim/notification/repository/NotificationRepository.java +++ b/src/main/java/com/example/moim/notification/repository/NotificationRepository.java @@ -1,6 +1,6 @@ package com.example.moim.notification.repository; -import com.example.moim.notification.entity.Notification; +import com.example.moim.notification.entity.NotificationEntity; import com.example.moim.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -9,9 +9,9 @@ import java.util.List; @Repository -public interface NotificationRepository extends JpaRepository { +public interface NotificationRepository extends JpaRepository { @Transactional(readOnly = true) Boolean existsByTargetUserAndIsRead(User user, Boolean isRead); - List findByTargetUser(User targetUser); + List findByTargetUser(User targetUser); } From 51dec4bbb5b9166406a1134061aae7eaa4942918 Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:12:24 +0900 Subject: [PATCH 12/29] =?UTF-8?q?refactor(#20):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=EC=99=80=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=AA=85=20?= =?UTF-8?q?=EA=B0=84=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미 Notification을 명시한 클래스 안에 있으므로 메서드에서 또 언급할 필요 없음. 의미 중복임. --- .../controller/NotificationController.java | 17 ++++++++--------- .../service/NotificationService.java | 4 ++++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/example/moim/notification/controller/NotificationController.java b/src/main/java/com/example/moim/notification/controller/NotificationController.java index b6e3b80..272b535 100644 --- a/src/main/java/com/example/moim/notification/controller/NotificationController.java +++ b/src/main/java/com/example/moim/notification/controller/NotificationController.java @@ -4,6 +4,7 @@ import com.example.moim.notification.dto.NotificationOutput; import com.example.moim.notification.service.NotificationService; import com.example.moim.user.dto.UserDetailsImpl; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; @@ -11,25 +12,23 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - @RestController @RequiredArgsConstructor -public class NotificationController implements NotificationControllerDocs{ +public class NotificationController implements NotificationControllerDocs { private final NotificationService notificationService; - @GetMapping(value = "/notifications/unread-count") + @GetMapping(value = "/notification/unread-count") public NotificationExistOutput notificationUnreadCount(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl) { - return notificationService.checkNotificationUnread(userDetailsImpl.getUser()); + return notificationService.checkUnread(userDetailsImpl.getUser()); } - @GetMapping("/notifications") + @GetMapping("/notification") public List notificationFind(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl) { - return notificationService.findNotifications(userDetailsImpl.getUser()); + return notificationService.findAll(userDetailsImpl.getUser()); } - @DeleteMapping("/notifications/{id}") + @DeleteMapping("/notification/{id}") public void notificationRemove(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @PathVariable Long id) { - notificationService.removeNotification(id); + notificationService.remove(id); } } diff --git a/src/main/java/com/example/moim/notification/service/NotificationService.java b/src/main/java/com/example/moim/notification/service/NotificationService.java index eec60de..d72c552 100644 --- a/src/main/java/com/example/moim/notification/service/NotificationService.java +++ b/src/main/java/com/example/moim/notification/service/NotificationService.java @@ -14,17 +14,21 @@ @RequiredArgsConstructor public class NotificationService { private final NotificationRepository notificationRepository; + private final NotificationSender notificationSender; public NotificationExistOutput checkNotice(User user) { + public NotificationExistOutput checkUnread(User user) { return new NotificationExistOutput(notificationRepository.existsByTargetUserAndIsRead(user, false)); } public List findNotice(User user) { + public List findAll(User user) { return notificationRepository.findByTargetUser(user).stream().map(NotificationOutput::new).toList(); } @Transactional public void removeNotice(Long id) { + public void remove(Long id) { notificationRepository.deleteById(id); } } From 7c9236fda4495288f1051df6f8e31f3e3e29d48d Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:14:57 +0900 Subject: [PATCH 13/29] =?UTF-8?q?refactor(#20):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/FcmNotificationSender.java | 65 +++++++++++++++++++ .../repository/NotificationSender.java | 7 ++ 2 files changed, 72 insertions(+) create mode 100644 src/main/java/com/example/moim/notification/repository/FcmNotificationSender.java create mode 100644 src/main/java/com/example/moim/notification/repository/NotificationSender.java diff --git a/src/main/java/com/example/moim/notification/repository/FcmNotificationSender.java b/src/main/java/com/example/moim/notification/repository/FcmNotificationSender.java new file mode 100644 index 0000000..2358d1b --- /dev/null +++ b/src/main/java/com/example/moim/notification/repository/FcmNotificationSender.java @@ -0,0 +1,65 @@ +package com.example.moim.notification.repository; + +import com.example.moim.notification.entity.NotificationEntity; +import com.google.firebase.messaging.AndroidConfig; +import com.google.firebase.messaging.AndroidNotification; +import com.google.firebase.messaging.ApnsConfig; +import com.google.firebase.messaging.Aps; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import com.google.firebase.messaging.WebpushConfig; +import com.google.firebase.messaging.WebpushNotification; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class FcmNotificationSender implements NotificationSender { + @Override + public void send(NotificationEntity notificationEntity) { + String fcmToken = notificationEntity.getTargetUser().getFcmToken(); + if (fcmToken == null || fcmToken.isBlank()) { + log.warn("🔴 FCM 토큰이 존재하지 않아 푸시 전송 생략: userId={}", notificationEntity.getTargetUser().getId()); + notificationEntity.failed(); + return; + } + + Message message = Message.builder() + .setToken(fcmToken) + .setNotification(Notification.builder() + .setTitle(notificationEntity.getTitle()) + .setBody(notificationEntity.getContent()) + .build()) + .setAndroidConfig(AndroidConfig.builder() + .setNotification(AndroidNotification.builder() + .setPriority(AndroidNotification.Priority.HIGH) + .build()) + .build()) + .setWebpushConfig(WebpushConfig.builder() + .setNotification( + WebpushNotification.builder() + .setBadge( + "https://media.discordapp.net/attachments/1081467110200451092/1199660879071952906/MG.png?ex=65c35a42&is=65b0e542&hm=4b109d5d3ab5850eeacbd46d8f8eed253ab7bdb7ac4d28111e57cad3aba58b98&=&format=webp&quality=lossless") + .build()) + .build()) + .setApnsConfig(ApnsConfig.builder() + .setAps(Aps.builder() + .setBadge(1) + .build()) + .build()) + .build(); + + try { + String response = FirebaseMessaging.getInstance().send(message); + log.info("✅ FCM 푸시 전송 성공: userId={}, response={}", notificationEntity.getTargetUser().getId(), response); + notificationEntity.sent(); + } catch (FirebaseMessagingException e) { + log.error("🔴 FCM 푸시 전송 실패: userId={}, 이유={}", notificationEntity.getTargetUser().getId(), e.getMessage(), + e); + notificationEntity.failed(); + // TODO: 실패한 알림을 재전송하는 로직 추가 필요 + } + } +} diff --git a/src/main/java/com/example/moim/notification/repository/NotificationSender.java b/src/main/java/com/example/moim/notification/repository/NotificationSender.java new file mode 100644 index 0000000..1935bcd --- /dev/null +++ b/src/main/java/com/example/moim/notification/repository/NotificationSender.java @@ -0,0 +1,7 @@ +package com.example.moim.notification.repository; + +import com.example.moim.notification.entity.NotificationEntity; + +public interface NotificationSender { + void send(NotificationEntity notificationEntity); +} From e52a448f21453f840d2b718c0043b2fcc04313d2 Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:21:11 +0900 Subject: [PATCH 14/29] =?UTF-8?q?refactor(#20):=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A2=85=EB=A5=98=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?=EC=A0=84=EB=9E=B5=EC=9D=84=20=EC=84=A0=ED=83=9D=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비즈니스 로직과 알림 전송/저장 로직의 책임 분리하여 도메인 로직의 복잡도 낮춤 - 푸시 알림은 외부 호출이라 느릴 수 있어서 비동기로 응답 속도 개선/요청 처리 성능 향상 - 각 이벤트마다 메시지를 하드코딩 하지 않고 템플릿 기반으로 생성하도록 변경 --- .../service/NotificationEventDispatcher.java | 30 +++++++++++++++++++ .../service/NotificationStrategy.java | 9 ++++++ 2 files changed, 39 insertions(+) create mode 100644 src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java create mode 100644 src/main/java/com/example/moim/notification/service/NotificationStrategy.java diff --git a/src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java b/src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java new file mode 100644 index 0000000..04ec820 --- /dev/null +++ b/src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java @@ -0,0 +1,30 @@ +package com.example.moim.notification.service; + +import com.example.moim.notification.entity.NotificationEntity; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class NotificationEventDispatcher { + + private final List> strategies; + private final NotificationService notificationService; + + @Async + @EventListener + public void dispatchEvent(Object event) { + strategies.stream() + .filter(strategy -> strategy.supports(event)) + .findFirst() + .ifPresent(strategy -> { + @SuppressWarnings("unchecked") + NotificationStrategy s = (NotificationStrategy) strategy; + List notificationEntities = s.generate(event); +// notificationService.sendAll(notificationEntities); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/moim/notification/service/NotificationStrategy.java b/src/main/java/com/example/moim/notification/service/NotificationStrategy.java new file mode 100644 index 0000000..ef9b053 --- /dev/null +++ b/src/main/java/com/example/moim/notification/service/NotificationStrategy.java @@ -0,0 +1,9 @@ +package com.example.moim.notification.service; + +import com.example.moim.notification.entity.NotificationEntity; +import java.util.List; + +public interface NotificationStrategy { + boolean supports(Object event); // 타입 판별용 + List generate(T event); +} From 234bfb6a3701e77900b6fa934e562ee344ee1aef Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:21:52 +0900 Subject: [PATCH 15/29] =?UTF-8?q?refactor(#20):=20=EA=B0=81=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EB=B3=84=20=EC=A0=84=EB=9E=B5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ClubJoinNotificationStrategy.java | 36 +++++++++++++++++++ .../MatchCancelClubNotificationStrategy.java | 34 ++++++++++++++++++ ...chCancelUserEventNotificationStrategy.java | 32 +++++++++++++++++ .../MatchInviteNotificationStrategy.java | 34 ++++++++++++++++++ .../MatchRequestNotificationStrategy.java | 35 ++++++++++++++++++ ...ScheduleEncourageNotificationStrategy.java | 30 ++++++++++++++++ .../ScheduleSaveNotificationStrategy.java | 34 ++++++++++++++++++ 7 files changed, 235 insertions(+) create mode 100644 src/main/java/com/example/moim/notification/service/ClubJoinNotificationStrategy.java create mode 100644 src/main/java/com/example/moim/notification/service/MatchCancelClubNotificationStrategy.java create mode 100644 src/main/java/com/example/moim/notification/service/MatchCancelUserEventNotificationStrategy.java create mode 100644 src/main/java/com/example/moim/notification/service/MatchInviteNotificationStrategy.java create mode 100644 src/main/java/com/example/moim/notification/service/MatchRequestNotificationStrategy.java create mode 100644 src/main/java/com/example/moim/notification/service/ScheduleEncourageNotificationStrategy.java create mode 100644 src/main/java/com/example/moim/notification/service/ScheduleSaveNotificationStrategy.java diff --git a/src/main/java/com/example/moim/notification/service/ClubJoinNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/ClubJoinNotificationStrategy.java new file mode 100644 index 0000000..3ea8531 --- /dev/null +++ b/src/main/java/com/example/moim/notification/service/ClubJoinNotificationStrategy.java @@ -0,0 +1,36 @@ +package com.example.moim.notification.service; + +import com.example.moim.club.repository.UserClubRepository; +import com.example.moim.notification.dto.ClubJoinEvent; +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.notification.entity.NotificationType; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ClubJoinNotificationStrategy implements NotificationStrategy { + + private final UserClubRepository userClubRepository; + + @Override + public boolean supports(Object event) { + return event instanceof ClubJoinEvent; + } + + @Override + public List generate(ClubJoinEvent event) { + return userClubRepository.findAllByClub(event.getClub()) + .stream() + .map(userClub -> NotificationEntity.create(userClub.getUser() + , NotificationType.CLUB_JOIN + , NotificationType.CLUB_JOIN.formatMessage( + event.getUser().getName(), + event.getClub().getTitle()) + , event.getClub().getTitle() + , event.getClub().getId()) + ) + .toList(); + } +} diff --git a/src/main/java/com/example/moim/notification/service/MatchCancelClubNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/MatchCancelClubNotificationStrategy.java new file mode 100644 index 0000000..91fd54a --- /dev/null +++ b/src/main/java/com/example/moim/notification/service/MatchCancelClubNotificationStrategy.java @@ -0,0 +1,34 @@ +package com.example.moim.notification.service; + +import com.example.moim.club.repository.UserClubRepository; +import com.example.moim.notification.dto.MatchCancelClubEvent; +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.notification.entity.NotificationType; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MatchCancelClubNotificationStrategy implements NotificationStrategy { + + private final UserClubRepository userClubRepository; + + @Override + public boolean supports(Object event) { + return event instanceof MatchCancelClubEvent; + } + + @Override + public List generate(MatchCancelClubEvent event) { + return userClubRepository.findAllByClub(event.getTargetClub()).stream() + .map(userClub -> NotificationEntity.create(userClub.getUser() + , NotificationType.MATCH_CANCEL_CLUB + , NotificationType.MATCH_CANCEL_CLUB.formatMessage( + event.getTargetClub().getTitle() + ) + , event.getMatch().getName() + , event.getMatch().getId() + )).toList(); + } +} diff --git a/src/main/java/com/example/moim/notification/service/MatchCancelUserEventNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/MatchCancelUserEventNotificationStrategy.java new file mode 100644 index 0000000..5263e54 --- /dev/null +++ b/src/main/java/com/example/moim/notification/service/MatchCancelUserEventNotificationStrategy.java @@ -0,0 +1,32 @@ +package com.example.moim.notification.service; + +import com.example.moim.notification.dto.MatchCancelUserEvent; +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.notification.entity.NotificationType; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MatchCancelUserEventNotificationStrategy implements NotificationStrategy { + + @Override + public boolean supports(Object event) { + return event instanceof MatchCancelUserEvent; + } + + @Override + public List generate(MatchCancelUserEvent event) { + return List.of( + NotificationEntity.create(event.getTargetUser() + , NotificationType.MATCH_CANCEL_USER + , NotificationType.MATCH_CANCEL_USER.formatMessage( + event.getMatch().getName() + ) + , event.getMatch().getName() + , event.getMatch().getId() + ) + ); + } +} diff --git a/src/main/java/com/example/moim/notification/service/MatchInviteNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/MatchInviteNotificationStrategy.java new file mode 100644 index 0000000..498451e --- /dev/null +++ b/src/main/java/com/example/moim/notification/service/MatchInviteNotificationStrategy.java @@ -0,0 +1,34 @@ +package com.example.moim.notification.service; + +import com.example.moim.club.repository.UserClubRepository; +import com.example.moim.notification.dto.MatchInviteEvent; +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.notification.entity.NotificationType; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MatchInviteNotificationStrategy implements NotificationStrategy { + + private final UserClubRepository userClubRepository; + + @Override + public boolean supports(Object event) { + return event instanceof MatchInviteEvent; + } + + @Override + public List generate(MatchInviteEvent event) { + return userClubRepository.findAllByClub(event.getClub()) + .stream() + .map(userClub -> NotificationEntity.create(userClub.getUser() + , NotificationType.MATCH_INVITE + , NotificationType.MATCH_INVITE.formatMessage(event.getClub().getTitle()) + , event.getClub().getTitle() + , event.getClub().getId()) + ) + .toList(); + } +} diff --git a/src/main/java/com/example/moim/notification/service/MatchRequestNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/MatchRequestNotificationStrategy.java new file mode 100644 index 0000000..a03d4e2 --- /dev/null +++ b/src/main/java/com/example/moim/notification/service/MatchRequestNotificationStrategy.java @@ -0,0 +1,35 @@ +package com.example.moim.notification.service; + +import com.example.moim.club.repository.UserClubRepository; +import com.example.moim.notification.dto.MatchRequestEvent; +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.notification.entity.NotificationType; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MatchRequestNotificationStrategy implements NotificationStrategy { + + private final UserClubRepository userClubRepository; + + @Override + public boolean supports(Object event) { + return event instanceof MatchRequestEvent; + } + + @Override + public List generate(MatchRequestEvent event) { + return userClubRepository.findAllByClub(event.getClub()).stream() + .map(userClub -> + NotificationEntity.create(event.getUser() + , NotificationType.MATCH_SUGGESTION + , NotificationType.MATCH_SUGGESTION.formatMessage( + event.getClub().getTitle() + ) + , event.getMatch().getName() + , event.getMatch().getId())) + .toList(); + } +} diff --git a/src/main/java/com/example/moim/notification/service/ScheduleEncourageNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/ScheduleEncourageNotificationStrategy.java new file mode 100644 index 0000000..d590635 --- /dev/null +++ b/src/main/java/com/example/moim/notification/service/ScheduleEncourageNotificationStrategy.java @@ -0,0 +1,30 @@ +package com.example.moim.notification.service; + +import com.example.moim.notification.dto.ScheduleEncourageEvent; +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.notification.entity.NotificationType; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class ScheduleEncourageNotificationStrategy implements NotificationStrategy { + + @Override + public boolean supports(Object event) { + return event instanceof ScheduleEncourageEvent; + } + + @Override + public List generate(ScheduleEncourageEvent event) { + return event.getUserList().stream() + .map(user -> NotificationEntity.create( + user + , NotificationType.SCHEDULE_ENCOURAGE + , NotificationType.SCHEDULE_ENCOURAGE.formatMessage( + event.getSchedule().getTitle() + ) + , event.getSchedule().getTitle() + , event.getSchedule().getId() + )).toList(); + } +} diff --git a/src/main/java/com/example/moim/notification/service/ScheduleSaveNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/ScheduleSaveNotificationStrategy.java new file mode 100644 index 0000000..e74cf40 --- /dev/null +++ b/src/main/java/com/example/moim/notification/service/ScheduleSaveNotificationStrategy.java @@ -0,0 +1,34 @@ +package com.example.moim.notification.service; + +import com.example.moim.club.repository.UserClubRepository; +import com.example.moim.notification.dto.ScheduleSaveEvent; +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.notification.entity.NotificationType; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ScheduleSaveNotificationStrategy implements NotificationStrategy { + + private final UserClubRepository userClubRepository; + + @Override + public boolean supports(Object event) { + return event instanceof ScheduleSaveEvent; + } + + @Override + public List generate(ScheduleSaveEvent event) { + return userClubRepository.findAllByClub(event.getSchedule().getClub()) + .stream() + .map(userClub -> NotificationEntity.create(event.getUser() + , NotificationType.SCHEDULE_SAVE + , NotificationType.SCHEDULE_SAVE.formatMessage(event.getSchedule().getTitle()) + , event.getSchedule().getTitle() + , event.getSchedule().getId() + )) + .toList(); + } +} From 8eaeeea9f840538d3a4d148224933bfeb427795e Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:23:40 +0900 Subject: [PATCH 16/29] =?UTF-8?q?refactor(#20):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=95=A0=ED=94=8C=EB=A6=AC=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/service/NotificationService.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/moim/notification/service/NotificationService.java b/src/main/java/com/example/moim/notification/service/NotificationService.java index d72c552..012933e 100644 --- a/src/main/java/com/example/moim/notification/service/NotificationService.java +++ b/src/main/java/com/example/moim/notification/service/NotificationService.java @@ -2,33 +2,37 @@ import com.example.moim.notification.dto.NotificationExistOutput; import com.example.moim.notification.dto.NotificationOutput; +import com.example.moim.notification.entity.NotificationEntity; import com.example.moim.notification.repository.NotificationRepository; +import com.example.moim.notification.repository.NotificationSender; import com.example.moim.user.entity.User; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Service @RequiredArgsConstructor public class NotificationService { private final NotificationRepository notificationRepository; private final NotificationSender notificationSender; - public NotificationExistOutput checkNotice(User user) { public NotificationExistOutput checkUnread(User user) { return new NotificationExistOutput(notificationRepository.existsByTargetUserAndIsRead(user, false)); } - public List findNotice(User user) { public List findAll(User user) { return notificationRepository.findByTargetUser(user).stream().map(NotificationOutput::new).toList(); } @Transactional - public void removeNotice(Long id) { public void remove(Long id) { notificationRepository.deleteById(id); } + + @Transactional + public void sendAll(List notificationEntities) { + notificationRepository.saveAll(notificationEntities); + notificationEntities.forEach(notificationSender::send); + } } From 5cc3dad888abaff701559fce8d0aed0d22231503 Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:32:01 +0900 Subject: [PATCH 17/29] =?UTF-8?q?refactor(#20):=20NotificationEntity?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=A6=84=20=EB=B0=94=EA=BF=A8=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/moim/user/entity/User.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/moim/user/entity/User.java b/src/main/java/com/example/moim/user/entity/User.java index 7b0d866..fba6fd5 100644 --- a/src/main/java/com/example/moim/user/entity/User.java +++ b/src/main/java/com/example/moim/user/entity/User.java @@ -6,7 +6,7 @@ import com.example.moim.global.enums.Gender; import com.example.moim.global.enums.Position; import com.example.moim.global.exception.ResponseCode; -import com.example.moim.notification.entity.Notifications; +import com.example.moim.notification.entity.NotificationEntity; import com.example.moim.user.dto.*; import com.example.moim.user.exceptions.advice.UserControllerAdvice; import jakarta.persistence.*; @@ -21,7 +21,6 @@ @Table(name = "users") @Getter @NoArgsConstructor -@Table(name = "users") public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -53,7 +52,7 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) private List userClub = new ArrayList<>(); @OneToMany(mappedBy = "targetUser", cascade = CascadeType.REMOVE) - private List notifications = new ArrayList<>(); + private List notifications = new ArrayList<>(); public static User createUser(SignupInput signupInput) { User user = new User(); From 4b0f6da7a866304628d1d9e076d9e356b948a379 Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:33:04 +0900 Subject: [PATCH 18/29] =?UTF-8?q?test(#20):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B3=80=EA=B2=BD=EB=90=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationServiceTest.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java index 78b3569..809b878 100644 --- a/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java +++ b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java @@ -12,7 +12,8 @@ import com.example.moim.notification.dto.ClubJoinEvent; import com.example.moim.notification.dto.NotificationExistOutput; import com.example.moim.notification.dto.NotificationOutput; -import com.example.moim.notification.entity.Notification; +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.notification.entity.NotificationType; import com.example.moim.notification.repository.NotificationRepository; import com.example.moim.user.entity.User; import java.util.Collections; @@ -41,7 +42,7 @@ void shouldReturnFalseWhenNoUnreadNotifications() { when(notificationRepository.existsByTargetUserAndIsRead(user, false)).thenReturn(false); // when - NotificationExistOutput result = notificationService.checkNotice(user); + NotificationExistOutput result = notificationService.checkUnread(user); // then assertFalse(result.getHasNotice()); @@ -55,7 +56,7 @@ void shouldReturnTrueWhenUnreadNotificationsExist() { when(notificationRepository.existsByTargetUserAndIsRead(user, false)).thenReturn(true); // when - NotificationExistOutput result = notificationService.checkNotice(user); + NotificationExistOutput result = notificationService.checkUnread(user); // then assertTrue(result.getHasNotice()); @@ -69,7 +70,7 @@ void shouldReturnEmptyListWhenNoNotificationsReceived() { when(notificationRepository.findByTargetUser(user)).thenReturn(Collections.emptyList()); // when - List result = notificationService.findNotice(user); + List result = notificationService.findAll(user); // then assertTrue(result.isEmpty()); @@ -82,13 +83,19 @@ void shouldReturnNotificationListWhenNotificationsExist() { User user = new User(); Club club = new Club(); ClubJoinEvent clubJoinEvent = new ClubJoinEvent(user, club); - Notification notification = Notification.createClubJoinNotification(clubJoinEvent, user); - notification.setCreatedDate(); - List notifications = List.of(notification); - when(notificationRepository.findByTargetUser(user)).thenReturn(notifications); + NotificationEntity notificationEntity = NotificationEntity.create(clubJoinEvent.getUser() + , NotificationType.CLUB_JOIN + , NotificationType.CLUB_JOIN.formatMessage( + clubJoinEvent.getUser().getName() + , clubJoinEvent.getClub().getTitle()) + , clubJoinEvent.getClub().getTitle() + , clubJoinEvent.getClub().getId()); + notificationEntity.setCreatedDate(); + List notificationEntities = List.of(notificationEntity); + when(notificationRepository.findByTargetUser(user)).thenReturn(notificationEntities); // when - List result = notificationService.findNotice(user); + List result = notificationService.findAll(user); // then assertEquals(1, result.size()); @@ -102,21 +109,21 @@ void shouldNotThrowExceptionWhenRemovingNonExistentNotification() { doNothing().when(notificationRepository).deleteById(nonExistentId); // when - notificationService.removeNotice(nonExistentId); + notificationService.remove(nonExistentId); // then verify(notificationRepository, times(1)).deleteById(nonExistentId); } @Test - @DisplayName("알림 ID로 알림을 삭제할 수 있다") + @DisplayName("알림 ID로 알림을 삭제할 수 있다") // FIXME : 근데 알림 삭제가 왜 필요하지? 읽음 처리도 아니고? void shouldDeleteNotificationById() { // given Long notificationId = 1L; doNothing().when(notificationRepository).deleteById(notificationId); // when - notificationService.removeNotice(notificationId); + notificationService.remove(notificationId); // then verify(notificationRepository, times(1)).deleteById(notificationId); From f9913ea84f90c13400af71a02cbc62333779e74c Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:48:51 +0900 Subject: [PATCH 19/29] =?UTF-8?q?refactor(#20):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=97=AD=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ry.java => NotificationJpaRepository.java} | 7 ++-- .../NotificationRepositoryImpl.java | 36 +++++++++++++++++++ .../service/port/NotificationRepository.java | 15 ++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) rename src/main/java/com/example/moim/notification/repository/{NotificationRepository.java => NotificationJpaRepository.java} (75%) create mode 100644 src/main/java/com/example/moim/notification/repository/NotificationRepositoryImpl.java create mode 100644 src/main/java/com/example/moim/notification/service/port/NotificationRepository.java diff --git a/src/main/java/com/example/moim/notification/repository/NotificationRepository.java b/src/main/java/com/example/moim/notification/repository/NotificationJpaRepository.java similarity index 75% rename from src/main/java/com/example/moim/notification/repository/NotificationRepository.java rename to src/main/java/com/example/moim/notification/repository/NotificationJpaRepository.java index e632071..4e75625 100644 --- a/src/main/java/com/example/moim/notification/repository/NotificationRepository.java +++ b/src/main/java/com/example/moim/notification/repository/NotificationJpaRepository.java @@ -2,14 +2,11 @@ import com.example.moim.notification.entity.NotificationEntity; import com.example.moim.user.entity.User; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - -@Repository -public interface NotificationRepository extends JpaRepository { +public interface NotificationJpaRepository extends JpaRepository { @Transactional(readOnly = true) Boolean existsByTargetUserAndIsRead(User user, Boolean isRead); diff --git a/src/main/java/com/example/moim/notification/repository/NotificationRepositoryImpl.java b/src/main/java/com/example/moim/notification/repository/NotificationRepositoryImpl.java new file mode 100644 index 0000000..3cbb74c --- /dev/null +++ b/src/main/java/com/example/moim/notification/repository/NotificationRepositoryImpl.java @@ -0,0 +1,36 @@ +package com.example.moim.notification.repository; + +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.notification.service.port.NotificationRepository; +import com.example.moim.user.entity.User; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class NotificationRepositoryImpl implements + NotificationRepository { + + private final NotificationJpaRepository notificationJpaRepository; + + @Override + public Boolean existsByTargetUserAndIsRead(User user, Boolean isRead) { + return notificationJpaRepository.existsByTargetUserAndIsRead(user, isRead); + } + + @Override + public List findByTargetUser(User user) { + return notificationJpaRepository.findByTargetUser(user); + } + + @Override + public void deleteById(Long id) { + notificationJpaRepository.deleteById(id); + } + + @Override + public void saveAll(List notificationEntities) { + notificationJpaRepository.saveAll(notificationEntities); + } +} diff --git a/src/main/java/com/example/moim/notification/service/port/NotificationRepository.java b/src/main/java/com/example/moim/notification/service/port/NotificationRepository.java new file mode 100644 index 0000000..ab5a71f --- /dev/null +++ b/src/main/java/com/example/moim/notification/service/port/NotificationRepository.java @@ -0,0 +1,15 @@ +package com.example.moim.notification.service.port; + +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.user.entity.User; +import java.util.List; + +public interface NotificationRepository { + Boolean existsByTargetUserAndIsRead(User user, Boolean isRead); + + List findByTargetUser(User user); + + void deleteById(Long id); + + void saveAll(List notificationEntities); +} From 0abd7dbaa35817b3e7513cb7bddd7b126d98af3a Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:49:42 +0900 Subject: [PATCH 20/29] =?UTF-8?q?fix(#20):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moim/notification/service/NotificationEventDispatcher.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java b/src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java index 04ec820..78af6bf 100644 --- a/src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java +++ b/src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java @@ -1,5 +1,6 @@ package com.example.moim.notification.service; +import com.example.moim.notification.controller.port.NotificationService; import com.example.moim.notification.entity.NotificationEntity; import java.util.List; import lombok.RequiredArgsConstructor; @@ -24,7 +25,7 @@ public void dispatchEvent(Object event) { @SuppressWarnings("unchecked") NotificationStrategy s = (NotificationStrategy) strategy; List notificationEntities = s.generate(event); -// notificationService.sendAll(notificationEntities); + notificationService.sendAll(notificationEntities); }); } } \ No newline at end of file From 38e09f1ecd6ada429372dc2de6d77ec22c8ddf8f Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 06:51:00 +0900 Subject: [PATCH 21/29] =?UTF-8?q?refactore(#20):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A0=88=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=97=AD=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/NotificationController.java | 2 +- .../controller/port/NotificationService.java | 17 +++++++++++++++++ ...ervice.java => NotificationServiceImpl.java} | 5 +++-- .../service/NotificationServiceTest.java | 6 ++++-- 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/moim/notification/controller/port/NotificationService.java rename src/main/java/com/example/moim/notification/service/{NotificationService.java => NotificationServiceImpl.java} (86%) diff --git a/src/main/java/com/example/moim/notification/controller/NotificationController.java b/src/main/java/com/example/moim/notification/controller/NotificationController.java index 272b535..3016415 100644 --- a/src/main/java/com/example/moim/notification/controller/NotificationController.java +++ b/src/main/java/com/example/moim/notification/controller/NotificationController.java @@ -1,8 +1,8 @@ package com.example.moim.notification.controller; +import com.example.moim.notification.controller.port.NotificationService; import com.example.moim.notification.dto.NotificationExistOutput; import com.example.moim.notification.dto.NotificationOutput; -import com.example.moim.notification.service.NotificationService; import com.example.moim.user.dto.UserDetailsImpl; import java.util.List; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/moim/notification/controller/port/NotificationService.java b/src/main/java/com/example/moim/notification/controller/port/NotificationService.java new file mode 100644 index 0000000..364244d --- /dev/null +++ b/src/main/java/com/example/moim/notification/controller/port/NotificationService.java @@ -0,0 +1,17 @@ +package com.example.moim.notification.controller.port; + +import com.example.moim.notification.dto.NotificationExistOutput; +import com.example.moim.notification.dto.NotificationOutput; +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.user.entity.User; +import java.util.List; + +public interface NotificationService { + NotificationExistOutput checkUnread(User user); + + List findAll(User user); + + void remove(Long id); + + void sendAll(List notificationEntities); +} diff --git a/src/main/java/com/example/moim/notification/service/NotificationService.java b/src/main/java/com/example/moim/notification/service/NotificationServiceImpl.java similarity index 86% rename from src/main/java/com/example/moim/notification/service/NotificationService.java rename to src/main/java/com/example/moim/notification/service/NotificationServiceImpl.java index 012933e..132e687 100644 --- a/src/main/java/com/example/moim/notification/service/NotificationService.java +++ b/src/main/java/com/example/moim/notification/service/NotificationServiceImpl.java @@ -1,10 +1,11 @@ package com.example.moim.notification.service; +import com.example.moim.notification.controller.port.NotificationService; import com.example.moim.notification.dto.NotificationExistOutput; import com.example.moim.notification.dto.NotificationOutput; import com.example.moim.notification.entity.NotificationEntity; -import com.example.moim.notification.repository.NotificationRepository; import com.example.moim.notification.repository.NotificationSender; +import com.example.moim.notification.service.port.NotificationRepository; import com.example.moim.user.entity.User; import java.util.List; import lombok.RequiredArgsConstructor; @@ -13,7 +14,7 @@ @Service @RequiredArgsConstructor -public class NotificationService { +public class NotificationServiceImpl implements NotificationService { private final NotificationRepository notificationRepository; private final NotificationSender notificationSender; diff --git a/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java index 809b878..fef4b2b 100644 --- a/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java +++ b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java @@ -9,12 +9,14 @@ import static org.mockito.Mockito.when; import com.example.moim.club.entity.Club; +import com.example.moim.notification.controller.port.NotificationService; import com.example.moim.notification.dto.ClubJoinEvent; import com.example.moim.notification.dto.NotificationExistOutput; import com.example.moim.notification.dto.NotificationOutput; import com.example.moim.notification.entity.NotificationEntity; import com.example.moim.notification.entity.NotificationType; -import com.example.moim.notification.repository.NotificationRepository; +import com.example.moim.notification.repository.NotificationJpaRepository; +import com.example.moim.notification.service.port.NotificationRepository; import com.example.moim.user.entity.User; import java.util.Collections; import java.util.List; @@ -32,7 +34,7 @@ class NotificationServiceTest { private NotificationRepository notificationRepository; @InjectMocks - private NotificationService notificationService; + private NotificationServiceImpl notificationService; @Test @DisplayName("사용자에게 읽지 않은 알림이 없으면 false 반환한다") From a0fb47f109965152814c70f9924c46c82adc9de0 Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 07:12:57 +0900 Subject: [PATCH 22/29] =?UTF-8?q?test(#20):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EC=A0=84=EC=86=A1=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationServiceTest.java | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java index fef4b2b..16a694a 100644 --- a/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java +++ b/src/test/java/com/example/moim/notification/service/NotificationServiceTest.java @@ -8,15 +8,16 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.example.moim.club.dto.request.ClubInput; import com.example.moim.club.entity.Club; -import com.example.moim.notification.controller.port.NotificationService; import com.example.moim.notification.dto.ClubJoinEvent; import com.example.moim.notification.dto.NotificationExistOutput; import com.example.moim.notification.dto.NotificationOutput; import com.example.moim.notification.entity.NotificationEntity; import com.example.moim.notification.entity.NotificationType; -import com.example.moim.notification.repository.NotificationJpaRepository; +import com.example.moim.notification.repository.NotificationSender; import com.example.moim.notification.service.port.NotificationRepository; +import com.example.moim.user.dto.SignupInput; import com.example.moim.user.entity.User; import java.util.Collections; import java.util.List; @@ -36,6 +37,9 @@ class NotificationServiceTest { @InjectMocks private NotificationServiceImpl notificationService; + @Mock + private NotificationSender notificationSender; + @Test @DisplayName("사용자에게 읽지 않은 알림이 없으면 false 반환한다") void shouldReturnFalseWhenNoUnreadNotifications() { @@ -118,7 +122,8 @@ void shouldNotThrowExceptionWhenRemovingNonExistentNotification() { } @Test - @DisplayName("알림 ID로 알림을 삭제할 수 있다") // FIXME : 근데 알림 삭제가 왜 필요하지? 읽음 처리도 아니고? + @DisplayName("알림 ID로 알림을 삭제할 수 있다") + // FIXME : 근데 알림 삭제가 왜 필요하지? 읽음 처리도 아니고? void shouldDeleteNotificationById() { // given Long notificationId = 1L; @@ -130,4 +135,49 @@ void shouldDeleteNotificationById() { // then verify(notificationRepository, times(1)).deleteById(notificationId); } + + + @Test + @DisplayName("알림을 저장하고 전송한다") + void shouldSaveAndSendNotifications() { + // given + User targetUser = User.createUser( + SignupInput.builder() + .phone("010-1234-5678") + .name("John Doe") + .password("password") + .email("email@gmail.com") + .birthday("2000-01-01") + .gender("남성") + .build() + ); + + Club joinedClub = Club.createClub( + ClubInput.builder() + .title("Club Title") + .clubCategory("동아리") + .gender("남성") + .activityArea("서울") + .sportsType("축구") + .ageRange("20대") + .build() + , "path/to/image" + ); + + NotificationEntity n1 = NotificationEntity.create(targetUser + , NotificationType.CLUB_JOIN + , NotificationType.CLUB_JOIN.formatMessage( + targetUser.getName() + , joinedClub.getTitle()) + , joinedClub.getTitle() + , 1L); + List notifications = List.of(n1); + + // when + notificationService.sendAll(notifications); + + // then + verify(notificationRepository).saveAll(notifications); // 저장이 호출되었는지 + verify(notificationSender).send(n1); // 전송이 각 알림마다 호출되었는지 + } } \ No newline at end of file From 0cb639f2a1debc87abbf4366cfdf6ad19f5e798f Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 07:44:03 +0900 Subject: [PATCH 23/29] =?UTF-8?q?test(#20):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 근데 테스트 할 때 원래 빌더를 이렇게 많이 썼던가? --- .../entity/NotificationEntity.java | 2 +- .../com/example/moim/user/entity/User.java | 63 +++++++++-- .../repository/FcmNotificationSenderTest.java | 103 ++++++++++++++++++ 3 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/example/moim/notification/repository/FcmNotificationSenderTest.java diff --git a/src/main/java/com/example/moim/notification/entity/NotificationEntity.java b/src/main/java/com/example/moim/notification/entity/NotificationEntity.java index 7ba2fad..f097bbe 100644 --- a/src/main/java/com/example/moim/notification/entity/NotificationEntity.java +++ b/src/main/java/com/example/moim/notification/entity/NotificationEntity.java @@ -34,7 +34,7 @@ public class NotificationEntity extends BaseEntity { private NotificationStatus status; @Builder - private NotificationEntity(User targetUser, NotificationType type, String title, String content, Long linkedId) { + public NotificationEntity(User targetUser, NotificationType type, String title, String content, Long linkedId) { this.targetUser = targetUser; this.type = type; this.title = title; diff --git a/src/main/java/com/example/moim/user/entity/User.java b/src/main/java/com/example/moim/user/entity/User.java index fba6fd5..72ef3f4 100644 --- a/src/main/java/com/example/moim/user/entity/User.java +++ b/src/main/java/com/example/moim/user/entity/User.java @@ -7,15 +7,30 @@ import com.example.moim.global.enums.Position; import com.example.moim.global.exception.ResponseCode; import com.example.moim.notification.entity.NotificationEntity; -import com.example.moim.user.dto.*; +import com.example.moim.user.dto.GoogleUserSignup; +import com.example.moim.user.dto.KakaoUserSignup; +import com.example.moim.user.dto.NaverUserSignup; +import com.example.moim.user.dto.SignupInput; +import com.example.moim.user.dto.SocialSignupInput; +import com.example.moim.user.dto.UserUpdateInput; import com.example.moim.user.exceptions.advice.UserControllerAdvice; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; import java.io.File; import java.util.ArrayList; import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Table(name = "users") @@ -54,13 +69,40 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "targetUser", cascade = CascadeType.REMOVE) private List notifications = new ArrayList<>(); + @Builder + public User(String email, String password, String name, String birthday, Gender gender, String phone, + String imgPath, + Role role, ActivityArea activityArea, int height, int weight, String mainFoot, Position mainPosition, + Position subPosition, String refreshToken, String fcmToken, List userClub, + List notifications) { + this.email = email; + this.password = password; + this.name = name; + this.birthday = birthday; + this.gender = gender; + this.phone = phone; + this.imgPath = imgPath; + this.role = role; + this.activityArea = activityArea; + this.height = height; + this.weight = weight; + this.mainFoot = mainFoot; + this.mainPosition = mainPosition; + this.subPosition = subPosition; + this.refreshToken = refreshToken; + this.fcmToken = fcmToken; + this.userClub = userClub; + this.notifications = notifications; + } + public static User createUser(SignupInput signupInput) { User user = new User(); user.email = signupInput.getEmail(); user.password = signupInput.getPassword(); user.name = signupInput.getName(); user.birthday = signupInput.getBirthday(); - user.gender = Gender.fromKoreanName(signupInput.getGender()).orElseThrow(() -> new UserControllerAdvice(ResponseCode.INVALID_GENDER)); + user.gender = Gender.fromKoreanName(signupInput.getGender()) + .orElseThrow(() -> new UserControllerAdvice(ResponseCode.INVALID_GENDER)); user.phone = signupInput.getPhone(); user.role = Role.USER; return user; @@ -93,7 +135,8 @@ public static User createKakaoUser(KakaoUserSignup kakaoUserSignup) { public static User createNaverUser(NaverUserSignup naverUserSignup) { User user = new User(); user.email = naverUserSignup.getEmail(); - user.gender = Gender.fromKoreanName(naverUserSignup.getGender()).orElseThrow(() -> new UserControllerAdvice(ResponseCode.INVALID_GENDER)); + user.gender = Gender.fromKoreanName(naverUserSignup.getGender()) + .orElseThrow(() -> new UserControllerAdvice(ResponseCode.INVALID_GENDER)); user.role = Role.USER; return user; } @@ -104,7 +147,8 @@ public void fillUserInfo(SocialSignupInput socialSignupInput, String imgPath) { this.phone = socialSignupInput.getPhone(); this.imgPath = imgPath; // this.gender = Gender.from(socialSignupInput.getGender()); - this.gender = Gender.fromKoreanName(socialSignupInput.getGender()).orElseThrow(() -> new UserControllerAdvice(ResponseCode.INVALID_GENDER)); + this.gender = Gender.fromKoreanName(socialSignupInput.getGender()) + .orElseThrow(() -> new UserControllerAdvice(ResponseCode.INVALID_GENDER)); this.activityArea = socialSignupInput.getActivityArea(); this.height = socialSignupInput.getHeight(); this.weight = socialSignupInput.getWeight(); @@ -133,7 +177,8 @@ public void updateUserInfo(UserUpdateInput userUpdateInput, String imgPath) { this.imgPath = imgPath; } if (userUpdateInput.getGender() != null && !userUpdateInput.getGender().isBlank()) { - this.gender = Gender.fromKoreanName(userUpdateInput.getGender()).orElseThrow(() -> new UserControllerAdvice(ResponseCode.INVALID_GENDER)); + this.gender = Gender.fromKoreanName(userUpdateInput.getGender()) + .orElseThrow(() -> new UserControllerAdvice(ResponseCode.INVALID_GENDER)); } if (userUpdateInput.getActivityArea() != null && !userUpdateInput.getActivityArea().name().isBlank()) { this.activityArea = userUpdateInput.getActivityArea(); diff --git a/src/test/java/com/example/moim/notification/repository/FcmNotificationSenderTest.java b/src/test/java/com/example/moim/notification/repository/FcmNotificationSenderTest.java new file mode 100644 index 0000000..d5e9cc8 --- /dev/null +++ b/src/test/java/com/example/moim/notification/repository/FcmNotificationSenderTest.java @@ -0,0 +1,103 @@ +package com.example.moim.notification.repository; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.notification.entity.NotificationStatus; +import com.example.moim.notification.entity.NotificationType; +import com.example.moim.user.entity.User; +import com.google.firebase.ErrorCode; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class FcmNotificationSenderTest { + + @Mock + private FirebaseMessaging firebaseMessaging; + + @InjectMocks + private FcmNotificationSender fcmNotificationSender; + + @Test + @DisplayName("FCM 토큰이 유효한 경우 FirebaseMessaging.send()가 호출되고 상태가 SENT로 설정된다") + void shouldCallFirebaseMessaging_andMarkAsSent() throws FirebaseMessagingException { + // given + User user = User.builder().fcmToken("valid_token").build(); + NotificationEntity notification = NotificationEntity.builder() + .title("제목") + .content("내용") + .type(NotificationType.CLUB_JOIN) + .targetUser(user) + .build(); + + try (MockedStatic mocked = Mockito.mockStatic(FirebaseMessaging.class)) { + mocked.when(FirebaseMessaging::getInstance).thenReturn(firebaseMessaging); + when(firebaseMessaging.send(any(Message.class))).thenReturn("success_response"); + + // when + fcmNotificationSender.send(notification); + + // then + verify(firebaseMessaging).send(any(Message.class)); + assertThat(notification.getStatus()).isEqualTo(NotificationStatus.SENT); + } + } + + @Test + @DisplayName("FCM 토큰이 유효하지 않은 경우 FirebaseMessaging.send()가 호출되고 상태가 FAILED로 설정된다") + void send_shouldMarkAsFailed_whenTokenIsNull() { + // given + User user = User.builder().fcmToken(null).build(); + NotificationEntity notification = NotificationEntity.builder() + .title("제목") + .content("내용") + .type(NotificationType.CLUB_JOIN) + .targetUser(user) + .build(); + // when + fcmNotificationSender.send(notification); + + // then + assertThat(notification.getStatus()).isEqualTo(NotificationStatus.FAILED); + } + + @Test + @DisplayName("FirebaseMessagingException이 발생하면 상태가 FAILED로 설정된다") + void shouldHandleFirebaseMessagingException() throws FirebaseMessagingException { + // given + User user = User.builder().fcmToken("valid-token").build(); + NotificationEntity notification = NotificationEntity.builder() + .title("제목") + .content("내용") + .type(NotificationType.CLUB_JOIN) + .targetUser(user) + .build(); + + try (MockedStatic mocked = Mockito.mockStatic(FirebaseMessaging.class)) { + mocked.when(FirebaseMessaging::getInstance).thenReturn(firebaseMessaging); + FirebaseMessagingException exception = mock(FirebaseMessagingException.class); + when(firebaseMessaging.send(any(Message.class))).thenThrow(exception); + + // when + fcmNotificationSender.send(notification); + + // then + assertThat(notification.getStatus()).isEqualTo(NotificationStatus.FAILED); + } + } +} From 1e59b0cd57202e0f8513d646906bcb696f6af48c Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 07:45:02 +0900 Subject: [PATCH 24/29] =?UTF-8?q?chore(#20):=20Fcm=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/moim/external/fcm/FcmConfig.java | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/example/moim/external/fcm/FcmConfig.java b/src/main/java/com/example/moim/external/fcm/FcmConfig.java index c22c496..ddb7d5c 100644 --- a/src/main/java/com/example/moim/external/fcm/FcmConfig.java +++ b/src/main/java/com/example/moim/external/fcm/FcmConfig.java @@ -1,32 +1,41 @@ package com.example.moim.external.fcm; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; @Configuration public class FcmConfig { -// @Bean -// FirebaseMessaging firebaseMessaging() throws IOException { -// ClassPathResource resource = new ClassPathResource("firebase/meta-gachon-fcm-firebase-adminsdk-fyr7b-30929b486f.json"); -// InputStream refreshToken = resource.getInputStream(); -// -// FirebaseApp firebaseApp = null; -// List firebaseAppList = FirebaseApp.getApps(); -// -// if (firebaseAppList != null && !firebaseAppList.isEmpty()) { -// for (FirebaseApp app : firebaseAppList) { -// if (app.getName().equals(FirebaseApp.DEFAULT_APP_NAME)) { -// firebaseApp = app; -// } -// } -// } else { -// FirebaseOptions options = FirebaseOptions.builder() -// .setCredentials(GoogleCredentials.fromStream(refreshToken)) -// .build(); -// -// firebaseApp = FirebaseApp.initializeApp(options); -// } -// -// return FirebaseMessaging.getInstance(firebaseApp); -// } + @Bean + FirebaseMessaging firebaseMessaging() throws IOException { + ClassPathResource resource = new ClassPathResource("secret/sample-firebase-test-f3c27-firebase-adminsdk-fbsvc-4a5440cd44.json"); + InputStream refreshToken = resource.getInputStream(); + + FirebaseApp firebaseApp = null; + List firebaseAppList = FirebaseApp.getApps(); + + if (firebaseAppList != null && !firebaseAppList.isEmpty()) { + for (FirebaseApp app : firebaseAppList) { + if (app.getName().equals(FirebaseApp.DEFAULT_APP_NAME)) { + firebaseApp = app; + } + } + } else { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(refreshToken)) + .build(); + + firebaseApp = FirebaseApp.initializeApp(options); + } + + return FirebaseMessaging.getInstance(firebaseApp); + } } From 080d901a80d3dfc379328eb65855ef70eb1ba5c4 Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 08:06:42 +0900 Subject: [PATCH 25/29] =?UTF-8?q?chore(#20):=20test=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EC=9D=98=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=93=A4=EC=9D=80=20test/resource=EC=97=90=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20properties=EB=A5=BC=20=EC=9A=B0=EC=84=A0=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=EC=8B=9D=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/application.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/test/resources/application.properties diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..b2d2a5a --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1 @@ +spring.profiles.active=test \ No newline at end of file From c1e80319e2121708d873347055a799a16a14be2b Mon Sep 17 00:00:00 2001 From: yebin Date: Thu, 10 Apr 2025 08:07:12 +0900 Subject: [PATCH 26/29] =?UTF-8?q?test(#20):=20=EB=B9=84=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 ++- .../NotificationEventDispatcherTest.java | 76 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java diff --git a/build.gradle b/build.gradle index f017ab5..3dfc6bd 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,7 @@ dependencies { testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' testRuntimeOnly 'com.h2database:h2' + testImplementation 'org.awaitility:awaitility:4.2.0' } sourceSets { @@ -70,4 +71,10 @@ sourceSets { tasks.named('test') { useJUnitPlatform() -} \ No newline at end of file +} + +test { + jvmArgs += [ + '--add-opens=java.base/java.lang=ALL-UNNAMED' + ] +} diff --git a/src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java b/src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java new file mode 100644 index 0000000..1c998e9 --- /dev/null +++ b/src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java @@ -0,0 +1,76 @@ +package com.example.moim.notification.service; + +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.verify; + +import com.example.moim.notification.controller.port.NotificationService; +import com.example.moim.notification.entity.NotificationEntity; +import com.example.moim.notification.entity.NotificationType; +import com.example.moim.user.entity.User; +import java.time.Duration; +import java.util.List; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; + +@SpringBootTest +class NotificationEventDispatcherTest { + + @Autowired + private ApplicationEventPublisher eventPublisher; + + @MockBean + private NotificationService notificationService; + + @TestConfiguration + static class TestStrategyConfig { + @Bean + public NotificationStrategy dummyStrategy() { + return new NotificationStrategy<>() { + @Override + public boolean supports(Object event) { + return event instanceof DummyEvent; + } + + @Override + public List generate(DummyEvent event) { + return List.of(NotificationEntity.builder() + .title("test") + .content("test") + .type(event.type()) + .targetUser(event.user()) + .build()); + } + }; + } + } + + @Test + @DisplayName("이벤트가 발생하면 비동기로 알림을 전송한다") + void shouldSendNotificationWhenEventIsDispatched() { + // given + User user = User.builder().fcmToken("test-token").build(); + DummyEvent event = new DummyEvent(NotificationType.MATCH_SUCCESS, user); + + // when + eventPublisher.publishEvent(event); + + // then + // 비동기 처리를 기다리기 위해 Awaitility 또는 CountDownLatch 사용 + Awaitility.await() + .atMost(Duration.ofSeconds(3)) + .untilAsserted(() -> + verify(notificationService).sendAll(anyList()) + ); + } + + private record DummyEvent(NotificationType type, User user) { + } + +} \ No newline at end of file From ffc2d852c2a429650c6dbcc53828518c0aab4e52 Mon Sep 17 00:00:00 2001 From: yebin Date: Sun, 13 Apr 2025 19:48:08 +0900 Subject: [PATCH 27/29] =?UTF-8?q?refactor(#20):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=84=9C=EB=B9=84=EC=8A=A4=20Strategy=20-?= =?UTF-8?q?>=20Handler=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tegy.java => ClubJoinNotificationEventHandler.java} | 6 +++--- ...va => MatchCancelClubNotificationEventHandler.java} | 6 +++--- ... MatchCancelUserEventNotificationEventHandler.java} | 6 +++--- ...y.java => MatchInviteNotificationEventHandler.java} | 6 +++--- ....java => MatchRequestNotificationEventHandler.java} | 6 +++--- .../service/NotificationEventDispatcher.java | 10 +++++----- .../notification/service/NotificationEventHandler.java | 9 +++++++++ .../notification/service/NotificationStrategy.java | 9 --------- ... => ScheduleEncourageNotificationEventHandler.java} | 6 +++--- ....java => ScheduleSaveNotificationEventHandler.java} | 6 +++--- .../service/NotificationEventDispatcherTest.java | 8 ++++---- 11 files changed, 39 insertions(+), 39 deletions(-) rename src/main/java/com/example/moim/notification/service/{ClubJoinNotificationStrategy.java => ClubJoinNotificationEventHandler.java} (84%) rename src/main/java/com/example/moim/notification/service/{MatchCancelClubNotificationStrategy.java => MatchCancelClubNotificationEventHandler.java} (82%) rename src/main/java/com/example/moim/notification/service/{MatchCancelUserEventNotificationStrategy.java => MatchCancelUserEventNotificationEventHandler.java} (79%) rename src/main/java/com/example/moim/notification/service/{MatchInviteNotificationStrategy.java => MatchInviteNotificationEventHandler.java} (82%) rename src/main/java/com/example/moim/notification/service/{MatchRequestNotificationStrategy.java => MatchRequestNotificationEventHandler.java} (84%) create mode 100644 src/main/java/com/example/moim/notification/service/NotificationEventHandler.java delete mode 100644 src/main/java/com/example/moim/notification/service/NotificationStrategy.java rename src/main/java/com/example/moim/notification/service/{ScheduleEncourageNotificationStrategy.java => ScheduleEncourageNotificationEventHandler.java} (79%) rename src/main/java/com/example/moim/notification/service/{ScheduleSaveNotificationStrategy.java => ScheduleSaveNotificationEventHandler.java} (83%) diff --git a/src/main/java/com/example/moim/notification/service/ClubJoinNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/ClubJoinNotificationEventHandler.java similarity index 84% rename from src/main/java/com/example/moim/notification/service/ClubJoinNotificationStrategy.java rename to src/main/java/com/example/moim/notification/service/ClubJoinNotificationEventHandler.java index 3ea8531..f95a2bd 100644 --- a/src/main/java/com/example/moim/notification/service/ClubJoinNotificationStrategy.java +++ b/src/main/java/com/example/moim/notification/service/ClubJoinNotificationEventHandler.java @@ -10,17 +10,17 @@ @Component @RequiredArgsConstructor -public class ClubJoinNotificationStrategy implements NotificationStrategy { +public class ClubJoinNotificationEventHandler implements NotificationEventHandler { private final UserClubRepository userClubRepository; @Override - public boolean supports(Object event) { + public boolean canHandle(Object event) { return event instanceof ClubJoinEvent; } @Override - public List generate(ClubJoinEvent event) { + public List handle(ClubJoinEvent event) { return userClubRepository.findAllByClub(event.getClub()) .stream() .map(userClub -> NotificationEntity.create(userClub.getUser() diff --git a/src/main/java/com/example/moim/notification/service/MatchCancelClubNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/MatchCancelClubNotificationEventHandler.java similarity index 82% rename from src/main/java/com/example/moim/notification/service/MatchCancelClubNotificationStrategy.java rename to src/main/java/com/example/moim/notification/service/MatchCancelClubNotificationEventHandler.java index 91fd54a..063342f 100644 --- a/src/main/java/com/example/moim/notification/service/MatchCancelClubNotificationStrategy.java +++ b/src/main/java/com/example/moim/notification/service/MatchCancelClubNotificationEventHandler.java @@ -10,17 +10,17 @@ @Component @RequiredArgsConstructor -public class MatchCancelClubNotificationStrategy implements NotificationStrategy { +public class MatchCancelClubNotificationEventHandler implements NotificationEventHandler { private final UserClubRepository userClubRepository; @Override - public boolean supports(Object event) { + public boolean canHandle(Object event) { return event instanceof MatchCancelClubEvent; } @Override - public List generate(MatchCancelClubEvent event) { + public List handle(MatchCancelClubEvent event) { return userClubRepository.findAllByClub(event.getTargetClub()).stream() .map(userClub -> NotificationEntity.create(userClub.getUser() , NotificationType.MATCH_CANCEL_CLUB diff --git a/src/main/java/com/example/moim/notification/service/MatchCancelUserEventNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/MatchCancelUserEventNotificationEventHandler.java similarity index 79% rename from src/main/java/com/example/moim/notification/service/MatchCancelUserEventNotificationStrategy.java rename to src/main/java/com/example/moim/notification/service/MatchCancelUserEventNotificationEventHandler.java index 5263e54..8a83c7e 100644 --- a/src/main/java/com/example/moim/notification/service/MatchCancelUserEventNotificationStrategy.java +++ b/src/main/java/com/example/moim/notification/service/MatchCancelUserEventNotificationEventHandler.java @@ -9,15 +9,15 @@ @Component @RequiredArgsConstructor -public class MatchCancelUserEventNotificationStrategy implements NotificationStrategy { +public class MatchCancelUserEventNotificationEventHandler implements NotificationEventHandler { @Override - public boolean supports(Object event) { + public boolean canHandle(Object event) { return event instanceof MatchCancelUserEvent; } @Override - public List generate(MatchCancelUserEvent event) { + public List handle(MatchCancelUserEvent event) { return List.of( NotificationEntity.create(event.getTargetUser() , NotificationType.MATCH_CANCEL_USER diff --git a/src/main/java/com/example/moim/notification/service/MatchInviteNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/MatchInviteNotificationEventHandler.java similarity index 82% rename from src/main/java/com/example/moim/notification/service/MatchInviteNotificationStrategy.java rename to src/main/java/com/example/moim/notification/service/MatchInviteNotificationEventHandler.java index 498451e..d575203 100644 --- a/src/main/java/com/example/moim/notification/service/MatchInviteNotificationStrategy.java +++ b/src/main/java/com/example/moim/notification/service/MatchInviteNotificationEventHandler.java @@ -10,17 +10,17 @@ @Component @RequiredArgsConstructor -public class MatchInviteNotificationStrategy implements NotificationStrategy { +public class MatchInviteNotificationEventHandler implements NotificationEventHandler { private final UserClubRepository userClubRepository; @Override - public boolean supports(Object event) { + public boolean canHandle(Object event) { return event instanceof MatchInviteEvent; } @Override - public List generate(MatchInviteEvent event) { + public List handle(MatchInviteEvent event) { return userClubRepository.findAllByClub(event.getClub()) .stream() .map(userClub -> NotificationEntity.create(userClub.getUser() diff --git a/src/main/java/com/example/moim/notification/service/MatchRequestNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/MatchRequestNotificationEventHandler.java similarity index 84% rename from src/main/java/com/example/moim/notification/service/MatchRequestNotificationStrategy.java rename to src/main/java/com/example/moim/notification/service/MatchRequestNotificationEventHandler.java index a03d4e2..48682d7 100644 --- a/src/main/java/com/example/moim/notification/service/MatchRequestNotificationStrategy.java +++ b/src/main/java/com/example/moim/notification/service/MatchRequestNotificationEventHandler.java @@ -10,17 +10,17 @@ @Component @RequiredArgsConstructor -public class MatchRequestNotificationStrategy implements NotificationStrategy { +public class MatchRequestNotificationEventHandler implements NotificationEventHandler { private final UserClubRepository userClubRepository; @Override - public boolean supports(Object event) { + public boolean canHandle(Object event) { return event instanceof MatchRequestEvent; } @Override - public List generate(MatchRequestEvent event) { + public List handle(MatchRequestEvent event) { return userClubRepository.findAllByClub(event.getClub()).stream() .map(userClub -> NotificationEntity.create(event.getUser() diff --git a/src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java b/src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java index 78af6bf..cd18711 100644 --- a/src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java +++ b/src/main/java/com/example/moim/notification/service/NotificationEventDispatcher.java @@ -12,19 +12,19 @@ @RequiredArgsConstructor public class NotificationEventDispatcher { - private final List> strategies; + private final List> handlers; private final NotificationService notificationService; @Async @EventListener public void dispatchEvent(Object event) { - strategies.stream() - .filter(strategy -> strategy.supports(event)) + handlers.stream() + .filter(strategy -> strategy.canHandle(event)) .findFirst() .ifPresent(strategy -> { @SuppressWarnings("unchecked") - NotificationStrategy s = (NotificationStrategy) strategy; - List notificationEntities = s.generate(event); + NotificationEventHandler s = (NotificationEventHandler) strategy; + List notificationEntities = s.handle(event); notificationService.sendAll(notificationEntities); }); } diff --git a/src/main/java/com/example/moim/notification/service/NotificationEventHandler.java b/src/main/java/com/example/moim/notification/service/NotificationEventHandler.java new file mode 100644 index 0000000..3cf4595 --- /dev/null +++ b/src/main/java/com/example/moim/notification/service/NotificationEventHandler.java @@ -0,0 +1,9 @@ +package com.example.moim.notification.service; + +import com.example.moim.notification.entity.NotificationEntity; +import java.util.List; + +public interface NotificationEventHandler { + boolean canHandle(Object event); // 타입 판별용 + List handle(T event); +} diff --git a/src/main/java/com/example/moim/notification/service/NotificationStrategy.java b/src/main/java/com/example/moim/notification/service/NotificationStrategy.java deleted file mode 100644 index ef9b053..0000000 --- a/src/main/java/com/example/moim/notification/service/NotificationStrategy.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.moim.notification.service; - -import com.example.moim.notification.entity.NotificationEntity; -import java.util.List; - -public interface NotificationStrategy { - boolean supports(Object event); // 타입 판별용 - List generate(T event); -} diff --git a/src/main/java/com/example/moim/notification/service/ScheduleEncourageNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/ScheduleEncourageNotificationEventHandler.java similarity index 79% rename from src/main/java/com/example/moim/notification/service/ScheduleEncourageNotificationStrategy.java rename to src/main/java/com/example/moim/notification/service/ScheduleEncourageNotificationEventHandler.java index d590635..8cae39d 100644 --- a/src/main/java/com/example/moim/notification/service/ScheduleEncourageNotificationStrategy.java +++ b/src/main/java/com/example/moim/notification/service/ScheduleEncourageNotificationEventHandler.java @@ -7,15 +7,15 @@ import org.springframework.stereotype.Component; @Component -public class ScheduleEncourageNotificationStrategy implements NotificationStrategy { +public class ScheduleEncourageNotificationEventHandler implements NotificationEventHandler { @Override - public boolean supports(Object event) { + public boolean canHandle(Object event) { return event instanceof ScheduleEncourageEvent; } @Override - public List generate(ScheduleEncourageEvent event) { + public List handle(ScheduleEncourageEvent event) { return event.getUserList().stream() .map(user -> NotificationEntity.create( user diff --git a/src/main/java/com/example/moim/notification/service/ScheduleSaveNotificationStrategy.java b/src/main/java/com/example/moim/notification/service/ScheduleSaveNotificationEventHandler.java similarity index 83% rename from src/main/java/com/example/moim/notification/service/ScheduleSaveNotificationStrategy.java rename to src/main/java/com/example/moim/notification/service/ScheduleSaveNotificationEventHandler.java index e74cf40..878f3ce 100644 --- a/src/main/java/com/example/moim/notification/service/ScheduleSaveNotificationStrategy.java +++ b/src/main/java/com/example/moim/notification/service/ScheduleSaveNotificationEventHandler.java @@ -10,17 +10,17 @@ @Component @RequiredArgsConstructor -public class ScheduleSaveNotificationStrategy implements NotificationStrategy { +public class ScheduleSaveNotificationEventHandler implements NotificationEventHandler { private final UserClubRepository userClubRepository; @Override - public boolean supports(Object event) { + public boolean canHandle(Object event) { return event instanceof ScheduleSaveEvent; } @Override - public List generate(ScheduleSaveEvent event) { + public List handle(ScheduleSaveEvent event) { return userClubRepository.findAllByClub(event.getSchedule().getClub()) .stream() .map(userClub -> NotificationEntity.create(event.getUser() diff --git a/src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java b/src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java index 1c998e9..e2003ad 100644 --- a/src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java +++ b/src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java @@ -31,15 +31,15 @@ class NotificationEventDispatcherTest { @TestConfiguration static class TestStrategyConfig { @Bean - public NotificationStrategy dummyStrategy() { - return new NotificationStrategy<>() { + public NotificationEventHandler dummyStrategy() { + return new NotificationEventHandler<>() { @Override - public boolean supports(Object event) { + public boolean canHandle(Object event) { return event instanceof DummyEvent; } @Override - public List generate(DummyEvent event) { + public List handle(DummyEvent event) { return List.of(NotificationEntity.builder() .title("test") .content("test") From 173c5a576bcc44df7282b3f34b0e6f707a93ef57 Mon Sep 17 00:00:00 2001 From: yebin Date: Sun, 13 Apr 2025 19:51:04 +0900 Subject: [PATCH 28/29] =?UTF-8?q?test(#20):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=EB=8B=A8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=AA=85=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/repository/FcmNotificationSenderTest.java | 4 ++-- .../notification/service/NotificationEventDispatcherTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/example/moim/notification/repository/FcmNotificationSenderTest.java b/src/test/java/com/example/moim/notification/repository/FcmNotificationSenderTest.java index d5e9cc8..ce31cda 100644 --- a/src/test/java/com/example/moim/notification/repository/FcmNotificationSenderTest.java +++ b/src/test/java/com/example/moim/notification/repository/FcmNotificationSenderTest.java @@ -35,7 +35,7 @@ class FcmNotificationSenderTest { @Test @DisplayName("FCM 토큰이 유효한 경우 FirebaseMessaging.send()가 호출되고 상태가 SENT로 설정된다") - void shouldCallFirebaseMessaging_andMarkAsSent() throws FirebaseMessagingException { + void send_shouldCallFirebaseMessaging_andMarkAsSent() throws FirebaseMessagingException { // given User user = User.builder().fcmToken("valid_token").build(); NotificationEntity notification = NotificationEntity.builder() @@ -78,7 +78,7 @@ void send_shouldMarkAsFailed_whenTokenIsNull() { @Test @DisplayName("FirebaseMessagingException이 발생하면 상태가 FAILED로 설정된다") - void shouldHandleFirebaseMessagingException() throws FirebaseMessagingException { + void send_shouldHandleFirebaseMessagingException() throws FirebaseMessagingException { // given User user = User.builder().fcmToken("valid-token").build(); NotificationEntity notification = NotificationEntity.builder() diff --git a/src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java b/src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java index e2003ad..2e85c5d 100644 --- a/src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java +++ b/src/test/java/com/example/moim/notification/service/NotificationEventDispatcherTest.java @@ -53,7 +53,7 @@ public List handle(DummyEvent event) { @Test @DisplayName("이벤트가 발생하면 비동기로 알림을 전송한다") - void shouldSendNotificationWhenEventIsDispatched() { + void dispatchEvent_shouldSendNotification_whenEventIsDispatched() { // given User user = User.builder().fcmToken("test-token").build(); DummyEvent event = new DummyEvent(NotificationType.MATCH_SUCCESS, user); From dca10eb81a9786f0d03bfb80b16c1182a5c68a7f Mon Sep 17 00:00:00 2001 From: yebin Date: Sun, 13 Apr 2025 21:21:26 +0900 Subject: [PATCH 29/29] =?UTF-8?q?fix(#20):=20Firebase=20=ED=82=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/moim/external/fcm/FcmConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/moim/external/fcm/FcmConfig.java b/src/main/java/com/example/moim/external/fcm/FcmConfig.java index ddb7d5c..77a68be 100644 --- a/src/main/java/com/example/moim/external/fcm/FcmConfig.java +++ b/src/main/java/com/example/moim/external/fcm/FcmConfig.java @@ -16,7 +16,7 @@ public class FcmConfig { @Bean FirebaseMessaging firebaseMessaging() throws IOException { - ClassPathResource resource = new ClassPathResource("secret/sample-firebase-test-f3c27-firebase-adminsdk-fbsvc-4a5440cd44.json"); + ClassPathResource resource = new ClassPathResource("secret/sample-firebase-test-f3c27-firebase-adminsdk-fbsvc-c15b4b66c6.json"); InputStream refreshToken = resource.getInputStream(); FirebaseApp firebaseApp = null;