diff --git a/src/main/java/leets/leenk/domain/birthday/application/usecase/BirthdayLetterUseCase.java b/src/main/java/leets/leenk/domain/birthday/application/usecase/BirthdayLetterUseCase.java index b031cbcf..c558aedc 100644 --- a/src/main/java/leets/leenk/domain/birthday/application/usecase/BirthdayLetterUseCase.java +++ b/src/main/java/leets/leenk/domain/birthday/application/usecase/BirthdayLetterUseCase.java @@ -9,6 +9,7 @@ import leets.leenk.domain.birthday.domain.entity.BirthdayLetterReadMark; import leets.leenk.domain.birthday.domain.service.BirthdayLetterSaveService; import leets.leenk.domain.birthday.domain.service.BirthdayLettersGetService; +import leets.leenk.domain.notification.application.usecase.BirthdayNotificationUsecase; import leets.leenk.domain.user.domain.entity.User; import leets.leenk.domain.user.domain.service.user.UserGetService; import lombok.RequiredArgsConstructor; @@ -28,6 +29,7 @@ public class BirthdayLetterUseCase { private final BirthdayLettersGetService birthdayLettersGetService; private final BirthdayLetterMapper birthdayLetterMapper; private final BirthdayChecker birthdayChecker; + private final BirthdayNotificationUsecase birthdayNotificationUsecase; @Transactional public void writeBirthdayLetter(long senderId, long receiverId, BirthdayLetterRequest request) { @@ -44,6 +46,8 @@ public void writeBirthdayLetter(long senderId, long receiverId, BirthdayLetterRe BirthdayLetter birthdayLetter = birthdayLetterMapper.toBirthdayLetter(sender, receiver, request); birthdayLetterSaveService.save(birthdayLetter); + + birthdayNotificationUsecase.saveBirthdayLetterNotification(birthdayLetter); } @Transactional(readOnly = true) diff --git a/src/main/java/leets/leenk/domain/birthday/domain/service/scheduler/BirthdayNotifyScheduler.java b/src/main/java/leets/leenk/domain/birthday/domain/service/scheduler/BirthdayNotifyScheduler.java new file mode 100644 index 00000000..bcab9dfd --- /dev/null +++ b/src/main/java/leets/leenk/domain/birthday/domain/service/scheduler/BirthdayNotifyScheduler.java @@ -0,0 +1,24 @@ +package leets.leenk.domain.birthday.domain.service.scheduler; + +import leets.leenk.domain.notification.application.usecase.BirthdayNotificationUsecase; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; + +@Service +@RequiredArgsConstructor +public class BirthdayNotifyScheduler { + + private final BirthdayNotificationUsecase birthdayNotificationUsecase; + + @Scheduled(cron = "0 0 0 * * *") + public void sendBirthdayNotifications() { + LocalDate today = LocalDate.now(); + birthdayNotificationUsecase.announceUserBirthday(today); + + birthdayNotificationUsecase.celebrateBirthday(today); + + } +} diff --git a/src/main/java/leets/leenk/domain/media/application/exception/MediaFailedUpdateException.java~ b/src/main/java/leets/leenk/domain/media/application/exception/MediaFailedUpdateException.java~ deleted file mode 100644 index 53cfda10..00000000 --- a/src/main/java/leets/leenk/domain/media/application/exception/MediaFailedUpdateException.java~ +++ /dev/null @@ -1,2 +0,0 @@ -package leets.leenk.domain.media.application.exception;public class MediaFailedUpdateException { -} diff --git a/src/main/java/leets/leenk/domain/notification/application/mapper/BirthdayNotificationMapper.java b/src/main/java/leets/leenk/domain/notification/application/mapper/BirthdayNotificationMapper.java new file mode 100644 index 00000000..b789dd2a --- /dev/null +++ b/src/main/java/leets/leenk/domain/notification/application/mapper/BirthdayNotificationMapper.java @@ -0,0 +1,67 @@ +package leets.leenk.domain.notification.application.mapper; + +import leets.leenk.domain.birthday.domain.entity.BirthdayLetter; +import leets.leenk.domain.notification.domain.entity.Notification; +import leets.leenk.domain.notification.domain.entity.birthdayContent.BirthdayAnnouncementContent; +import leets.leenk.domain.notification.domain.entity.birthdayContent.BirthdayCelebrateContent; +import leets.leenk.domain.notification.domain.entity.birthdayContent.BirthdayLetterContent; +import leets.leenk.domain.notification.domain.entity.enums.NotificationType; +import leets.leenk.domain.user.domain.entity.User; +import org.springframework.stereotype.Component; + +@Component +public class BirthdayNotificationMapper { + public Notification toBirthdayAnnouncementNotification(User birthdayUser, User targetUser) { + return Notification.builder() + .userId(targetUser.getId()) + .notificationType(NotificationType.BIRTHDAY_ANNOUNCEMENT) + .content(toBirthdayAnnouncementContent(birthdayUser)) + .isRead(Boolean.FALSE) + .build(); + } + + public Notification toBirthdayCelebrateNotification(User birthdayUser) { + return Notification.builder() + .userId(birthdayUser.getId()) + .notificationType(NotificationType.BIRTHDAY_CELEBRATE) + .content(toBirthdayCelebrateContent(birthdayUser)) + .isRead(Boolean.FALSE) + .build(); + } + + public Notification toBirthdayLetterNotification(BirthdayLetter birthdayLetter) { + return Notification.builder() + .userId(birthdayLetter.getReceiver().getId()) + .notificationType(NotificationType.BIRTHDAY_LETTER) + .content(toBirthdayLetterContent(birthdayLetter)) + .isRead(Boolean.FALSE) + .build(); + } + + private BirthdayAnnouncementContent toBirthdayAnnouncementContent(User birthdayUser) { + return BirthdayAnnouncementContent.builder() + .birthdayUserId(birthdayUser.getId()) + .birthdayUserName(birthdayUser.getName()) + .title(NotificationType.BIRTHDAY_ANNOUNCEMENT.getTitle()) + .body(NotificationType.BIRTHDAY_ANNOUNCEMENT.getContent()) + .build(); + } + + private BirthdayCelebrateContent toBirthdayCelebrateContent(User birthdayUser){ + return BirthdayCelebrateContent.builder() + .birthdayUserId(birthdayUser.getId()) + .birthdayUserName(birthdayUser.getName()) + .title(NotificationType.BIRTHDAY_CELEBRATE.getTitle()) + .body(NotificationType.BIRTHDAY_CELEBRATE.getContent()) + .build(); + } + + private BirthdayLetterContent toBirthdayLetterContent(BirthdayLetter birthdayLetter) { + return BirthdayLetterContent.builder() + .senderName(birthdayLetter.getSender().getName()) + .birthdayLetterId(birthdayLetter.getId()) + .title(NotificationType.BIRTHDAY_LETTER.getTitle()) + .body(NotificationType.BIRTHDAY_LETTER.getContent()) + .build(); + } +} diff --git a/src/main/java/leets/leenk/domain/notification/application/usecase/BirthdayNotificationUsecase.java b/src/main/java/leets/leenk/domain/notification/application/usecase/BirthdayNotificationUsecase.java new file mode 100644 index 00000000..5cecaa56 --- /dev/null +++ b/src/main/java/leets/leenk/domain/notification/application/usecase/BirthdayNotificationUsecase.java @@ -0,0 +1,129 @@ +package leets.leenk.domain.notification.application.usecase; + +import leets.leenk.domain.birthday.domain.entity.BirthdayLetter; +import leets.leenk.domain.birthday.domain.service.BirthdayGetService; +import leets.leenk.domain.notification.application.mapper.BirthdayNotificationMapper; +import leets.leenk.domain.notification.domain.entity.Notification; +import leets.leenk.domain.notification.domain.service.NotificationSaveService; +import leets.leenk.domain.user.domain.entity.User; +import leets.leenk.domain.user.domain.entity.UserSetting; +import leets.leenk.domain.user.domain.service.usersetting.UserSettingGetService; +import leets.leenk.global.sqs.application.mapper.SqsMessageEventMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BirthdayNotificationUsecase { + + private final UserSettingGetService userSettingGetService; + private final BirthdayGetService birthdayGetService; + + private final BirthdayNotificationMapper birthdayNotificationMapper; + private final SqsMessageEventMapper sqsMessageEventMapper; + + private final ApplicationEventPublisher eventPublisher; + private final NotificationSaveService notificationSaveService; + + @Transactional + public void announceUserBirthday(LocalDate today) { + List birthdayUsers = birthdayGetService.findTodayBirthdayUsers(today); + if (birthdayUsers.isEmpty()) { + return; + } + + List users = userSettingGetService.getUsersToNotifyBirthday(); + + for (User receiver : users) { + for (User birthdayUser : birthdayUsers) { + if (receiver.equals(birthdayUser)) continue; + + try { + Notification notification = birthdayNotificationMapper + .toBirthdayAnnouncementNotification(birthdayUser, receiver); + notificationSaveService.save(notification); + + if (receiver.getFcmToken() != null) { + eventPublisher.publishEvent( + sqsMessageEventMapper.toBirthdaySqsMessageEvent( + notification, + receiver.getFcmToken(), + birthdayUser + ) + ); + } + } catch (Exception e){ + log.error("알림 전송 실패", e); + } + } + } + + } + + @Transactional + public void celebrateBirthday(LocalDate today){ + List birthdayUsers = birthdayGetService.findTodayBirthdayUsers(today); + + for (User birthdayUser : birthdayUsers){ + if (!isBirthdayNotificationEnabled(birthdayUser)) continue; + + try { + Notification notification = birthdayNotificationMapper + .toBirthdayCelebrateNotification(birthdayUser); + notificationSaveService.save(notification); + + + if (birthdayUser.getFcmToken() != null) { + eventPublisher.publishEvent( + sqsMessageEventMapper.toBirthdaySqsMessageEvent( + notification, + birthdayUser.getFcmToken(), + birthdayUser + ) + ); + } + } catch (Exception e){ + log.error("알림 전송 실패", e); + } + } + } + + @Transactional + public void saveBirthdayLetterNotification(BirthdayLetter birthdayLetter){ + User birthdayUser = birthdayLetter.getReceiver(); + + if (!isBirthdayNotificationEnabled(birthdayUser)) return; + + Notification notification = birthdayNotificationMapper.toBirthdayLetterNotification(birthdayLetter); + notificationSaveService.save(notification); + + if(birthdayUser.getFcmToken() != null){ + eventPublisher.publishEvent( + sqsMessageEventMapper.toBirthdaySqsMessageEvent( + notification, + birthdayUser.getFcmToken(), + birthdayLetter.getSender() + ) + ); + } + + } + + private boolean isBirthdayNotificationEnabled(User birthdayUser) { + try { + UserSetting userSetting = userSettingGetService.findByUser(birthdayUser); + return userSetting != null && userSetting.isBirthdayNotify(); + } catch (Exception e) { + log.error("사용자 설정 조회 실패 - userId: {}", birthdayUser.getId(), e); + return false; + } + } + +} diff --git a/src/main/java/leets/leenk/domain/notification/domain/entity/NotificationContent.java b/src/main/java/leets/leenk/domain/notification/domain/entity/NotificationContent.java index 706b9622..bb5917ad 100644 --- a/src/main/java/leets/leenk/domain/notification/domain/entity/NotificationContent.java +++ b/src/main/java/leets/leenk/domain/notification/domain/entity/NotificationContent.java @@ -2,6 +2,9 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; +import leets.leenk.domain.notification.domain.entity.birthdayContent.BirthdayAnnouncementContent; +import leets.leenk.domain.notification.domain.entity.birthdayContent.BirthdayCelebrateContent; +import leets.leenk.domain.notification.domain.entity.birthdayContent.BirthdayLetterContent; import leets.leenk.domain.notification.domain.entity.feedContent.FeedFirstReactionNotificationContent; import leets.leenk.domain.notification.domain.entity.feedContent.FeedReactionCountNotificationContent; import leets.leenk.domain.notification.domain.entity.feedContent.FeedTagNotificationContent; @@ -28,7 +31,10 @@ @JsonSubTypes.Type(value = LeenkStartingSoonNotificationContent.class, name = "LEENK_STARTING_SOON"), @JsonSubTypes.Type(value = LeenkFinishedNotificationContent.class, name = "LEENK_FINISHED"), @JsonSubTypes.Type(value = LeenkStartedHostReminderNotificationContent.class, name = "LEENK_STARTED_HOST_REMINDER"), - @JsonSubTypes.Type(value = LeenkLeftNotificationContent.class, name = "LEENK_LEFT") + @JsonSubTypes.Type(value = LeenkLeftNotificationContent.class, name = "LEENK_LEFT"), + @JsonSubTypes.Type(value = BirthdayAnnouncementContent.class, name = "BIRTHDAY_ANNOUNCEMENT"), + @JsonSubTypes.Type(value = BirthdayCelebrateContent.class, name = "BIRTHDAY_CELEBRATE"), + @JsonSubTypes.Type(value = BirthdayLetterContent.class, name = "BIRTHDAY_LETTER") }) public class NotificationContent { diff --git a/src/main/java/leets/leenk/domain/notification/domain/entity/birthdayContent/BirthdayAnnouncementContent.java b/src/main/java/leets/leenk/domain/notification/domain/entity/birthdayContent/BirthdayAnnouncementContent.java new file mode 100644 index 00000000..4b86f7fe --- /dev/null +++ b/src/main/java/leets/leenk/domain/notification/domain/entity/birthdayContent/BirthdayAnnouncementContent.java @@ -0,0 +1,14 @@ +package leets.leenk.domain.notification.domain.entity.birthdayContent; + +import leets.leenk.domain.notification.domain.entity.NotificationContent; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@NoArgsConstructor +@Getter +public class BirthdayAnnouncementContent extends NotificationContent { + private Long birthdayUserId; + private String birthdayUserName; +} diff --git a/src/main/java/leets/leenk/domain/notification/domain/entity/birthdayContent/BirthdayCelebrateContent.java b/src/main/java/leets/leenk/domain/notification/domain/entity/birthdayContent/BirthdayCelebrateContent.java new file mode 100644 index 00000000..9c878612 --- /dev/null +++ b/src/main/java/leets/leenk/domain/notification/domain/entity/birthdayContent/BirthdayCelebrateContent.java @@ -0,0 +1,14 @@ +package leets.leenk.domain.notification.domain.entity.birthdayContent; + +import leets.leenk.domain.notification.domain.entity.NotificationContent; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@NoArgsConstructor +@Getter +public class BirthdayCelebrateContent extends NotificationContent { + private Long birthdayUserId; + private String birthdayUserName; +} diff --git a/src/main/java/leets/leenk/domain/notification/domain/entity/birthdayContent/BirthdayLetterContent.java b/src/main/java/leets/leenk/domain/notification/domain/entity/birthdayContent/BirthdayLetterContent.java new file mode 100644 index 00000000..1952799e --- /dev/null +++ b/src/main/java/leets/leenk/domain/notification/domain/entity/birthdayContent/BirthdayLetterContent.java @@ -0,0 +1,14 @@ +package leets.leenk.domain.notification.domain.entity.birthdayContent; + +import leets.leenk.domain.notification.domain.entity.NotificationContent; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@NoArgsConstructor +@Getter +public class BirthdayLetterContent extends NotificationContent { + private String senderName; + private Long birthdayLetterId; +} diff --git a/src/main/java/leets/leenk/domain/notification/domain/entity/enums/NotificationType.java b/src/main/java/leets/leenk/domain/notification/domain/entity/enums/NotificationType.java index dcc1fc3d..a150e899 100644 --- a/src/main/java/leets/leenk/domain/notification/domain/entity/enums/NotificationType.java +++ b/src/main/java/leets/leenk/domain/notification/domain/entity/enums/NotificationType.java @@ -22,8 +22,12 @@ public enum NotificationType { FEED_TAG("Leenk", "이 나를 함께한 사람에 추가했어", "feeds"), NEW_FEED("Leenk", "새로운 게시글을 확인해 봐", "feeds"), FEED_FIRST_REACTION("Leenk", "내가 쓴 피드에 좋아요를 받았어", "feeds"), - FEED_REACTION_COUNT("Leenk", "내가 쓴 피드에 좋아요를 %d개 받았어", "feeds"); + FEED_REACTION_COUNT("Leenk", "내가 쓴 피드에 좋아요를 %d개 받았어", "feeds"), + // Birthday + BIRTHDAY_ANNOUNCEMENT("Leenk", "오늘은 {name}의 생일이야\n축하해주러 가볼까?", "birthday/users"), + BIRTHDAY_CELEBRATE("Leenk", "생일 축하해, 멋쟁이 {name}!", "birthday/users"), + BIRTHDAY_LETTER("Leenk", "{name}에게 생일 편지를 받았어!", "birthday/letters/me"); private final String title; private final String content; diff --git a/src/main/java/leets/leenk/domain/user/application/dto/request/NotificationSettingUpdateRequest.java b/src/main/java/leets/leenk/domain/user/application/dto/request/NotificationSettingUpdateRequest.java index db3959d0..fd18467b 100644 --- a/src/main/java/leets/leenk/domain/user/application/dto/request/NotificationSettingUpdateRequest.java +++ b/src/main/java/leets/leenk/domain/user/application/dto/request/NotificationSettingUpdateRequest.java @@ -13,6 +13,9 @@ public record NotificationSettingUpdateRequest( Boolean newFeedNotify, @Schema(description = "새로운 공감 알림 여부 수정", example = "false") - Boolean newReactionNotify + Boolean newReactionNotify, + + @Schema(description = "생일 알림 여부 수정", example = "false") + Boolean birthdayNotify ) { } diff --git a/src/main/java/leets/leenk/domain/user/domain/entity/UserSetting.java b/src/main/java/leets/leenk/domain/user/domain/entity/UserSetting.java index 5caeea1f..0109307a 100644 --- a/src/main/java/leets/leenk/domain/user/domain/entity/UserSetting.java +++ b/src/main/java/leets/leenk/domain/user/domain/entity/UserSetting.java @@ -31,6 +31,10 @@ public class UserSetting extends BaseEntity { @Column(nullable = false, columnDefinition = "boolean default true") private boolean isNewReactionNotify; + @Column(nullable = false, columnDefinition = "boolean default true") + private boolean isBirthdayNotify; + + @OneToOne @JoinColumn(name = "user_id") private User user; @@ -51,4 +55,8 @@ public void updateIsNewFeedNotify(boolean isNewFeedNotify) { public void updateIsNewReactionNotify(boolean isNewReactionNotify) { this.isNewReactionNotify = isNewReactionNotify; } + + public void updateIsBirthdayNotify(boolean isBirthdayNotify) { + this.isBirthdayNotify = isBirthdayNotify; + } } diff --git a/src/main/java/leets/leenk/domain/user/domain/repository/UserSettingRepository.java b/src/main/java/leets/leenk/domain/user/domain/repository/UserSettingRepository.java index f024cf2b..62015990 100644 --- a/src/main/java/leets/leenk/domain/user/domain/repository/UserSettingRepository.java +++ b/src/main/java/leets/leenk/domain/user/domain/repository/UserSettingRepository.java @@ -22,4 +22,9 @@ public interface UserSettingRepository extends JpaRepository "AND us.user.leaveDate IS NULL " + "AND us.user.id <> :authorUserId") List findAllActiveUsersWithNewLeenkNotifyTrueExcludingUserId(@Param("authorUserId") Long authorUserId); + + @Query("SELECT us.user FROM UserSetting us WHERE us.isBirthdayNotify = true " + + "AND us.user.leaveDate IS NULL " + + "AND us.user.deleteDate IS NULL") + List findAllActiveUsersWithBirthdayNotifyTrue(); } diff --git a/src/main/java/leets/leenk/domain/user/domain/service/usersetting/UserSettingGetService.java b/src/main/java/leets/leenk/domain/user/domain/service/usersetting/UserSettingGetService.java index 97c3dd8a..a855add3 100644 --- a/src/main/java/leets/leenk/domain/user/domain/service/usersetting/UserSettingGetService.java +++ b/src/main/java/leets/leenk/domain/user/domain/service/usersetting/UserSettingGetService.java @@ -26,4 +26,8 @@ public List getUsersToNotifyNewLeenk(Long authorId) { public UserSetting findByUser(User user) { return userSettingRepository.findByUser(user).orElseThrow(UserSettingNotFoundException::new); } + + public List getUsersToNotifyBirthday() { + return userSettingRepository.findAllActiveUsersWithBirthdayNotifyTrue(); + } } diff --git a/src/main/java/leets/leenk/domain/user/domain/service/usersetting/UserSettingUpdateService.java b/src/main/java/leets/leenk/domain/user/domain/service/usersetting/UserSettingUpdateService.java index 11a5e95d..f5ffcacf 100644 --- a/src/main/java/leets/leenk/domain/user/domain/service/usersetting/UserSettingUpdateService.java +++ b/src/main/java/leets/leenk/domain/user/domain/service/usersetting/UserSettingUpdateService.java @@ -23,5 +23,9 @@ public void updateNotificationSetting(UserSetting userSetting, NotificationSetti if (request.newReactionNotify() != null) { userSetting.updateIsNewReactionNotify(request.newReactionNotify()); } + + if (request.birthdayNotify() != null) { + userSetting.updateIsBirthdayNotify(request.birthdayNotify()); + } } } diff --git a/src/main/java/leets/leenk/global/sqs/application/mapper/AwsSqsManager.java b/src/main/java/leets/leenk/global/sqs/application/mapper/AwsSqsManager.java index d16d4eda..74848cbe 100644 --- a/src/main/java/leets/leenk/global/sqs/application/mapper/AwsSqsManager.java +++ b/src/main/java/leets/leenk/global/sqs/application/mapper/AwsSqsManager.java @@ -40,14 +40,14 @@ public SendMessageRequest createPushAlarmMessageRequest(SqsMessageEvent event) { private MessageAttributeValue convertToAttributeValue(String value) { return MessageAttributeValue.builder() .dataType("String") - .stringValue(value) + .stringValue(value == null ? " " : value) .build(); } private MessageAttributeValue convertToAttributeValue(Long value) { return MessageAttributeValue.builder() .dataType("Number") - .stringValue(value.toString()) + .stringValue(value == null ? "-1" : value.toString()) .build(); } } diff --git a/src/main/java/leets/leenk/global/sqs/application/mapper/SqsMessageEventMapper.java b/src/main/java/leets/leenk/global/sqs/application/mapper/SqsMessageEventMapper.java index 9a07bec5..a19a02ff 100644 --- a/src/main/java/leets/leenk/global/sqs/application/mapper/SqsMessageEventMapper.java +++ b/src/main/java/leets/leenk/global/sqs/application/mapper/SqsMessageEventMapper.java @@ -93,4 +93,14 @@ public SqsMessageEvent fromLeenkLeft(Notification notification, String fcmToken, .id(id) .build(); } + + public SqsMessageEvent toBirthdaySqsMessageEvent(Notification notification, String fcmToken, User user) { + return SqsMessageEvent.builder() + .title(notification.getContent().getTitle()) + .content(notification.getContent().getBody().replace("{name}", + "[" + user.getName() + "]")) + .fcmToken(fcmToken) + .path(notification.getNotificationType().getPath()) + .build(); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 37f2e618..64f63945 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,6 +3,8 @@ spring: active: local jpa: open-in-view: false + jackson: + time-zone: Asia/Seoul springdoc: swagger-ui: