diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/group/presentation/dto/response/GroupBriefInfoDto.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/group/presentation/dto/response/GroupBriefInfoDto.java index 802f9a33..326e4320 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/group/presentation/dto/response/GroupBriefInfoDto.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/group/presentation/dto/response/GroupBriefInfoDto.java @@ -29,16 +29,6 @@ public class GroupBriefInfoDto { private GroupType groupType; - public GroupBriefInfoDto(GroupBaseInfoVo groupBaseInfoVo) { - groupId = groupBaseInfoVo.getGroupId(); - title = groupBaseInfoVo.getTitle(); - description = groupBaseInfoVo.getDescription(); - publicAccess = groupBaseInfoVo.getPublicAccess(); - thumbnailPath = groupBaseInfoVo.getThumbnailPath(); - groupType = groupBaseInfoVo.getGroupType(); - category = new CategoryDto(groupBaseInfoVo.getCategory()); - } - public GroupBriefInfoDto(GroupBaseInfoVo groupBaseInfoVo, Integer memberCount) { groupId = groupBaseInfoVo.getGroupId(); title = groupBaseInfoVo.getTitle(); diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/group/presentation/dto/response/GroupInfoForNotificationDto.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/group/presentation/dto/response/GroupInfoForNotificationDto.java new file mode 100644 index 00000000..b06a1386 --- /dev/null +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/group/presentation/dto/response/GroupInfoForNotificationDto.java @@ -0,0 +1,34 @@ +package io.github.depromeet.knockknockbackend.domain.group.presentation.dto.response; + + +import io.github.depromeet.knockknockbackend.domain.group.domain.GroupType; +import io.github.depromeet.knockknockbackend.domain.group.domain.vo.GroupBaseInfoVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class GroupInfoForNotificationDto { + private Long groupId; + + private String title; + + private String description; + + private String thumbnailPath; + + @Schema(description = "공개 그룹 여부 ture 면 공개임") + private Boolean publicAccess; + + private GroupType groupType; + + public GroupInfoForNotificationDto(GroupBaseInfoVo groupBaseInfoVo) { + groupId = groupBaseInfoVo.getGroupId(); + title = groupBaseInfoVo.getTitle(); + description = groupBaseInfoVo.getDescription(); + publicAccess = groupBaseInfoVo.getPublicAccess(); + thumbnailPath = groupBaseInfoVo.getThumbnailPath(); + groupType = groupBaseInfoVo.getGroupType(); + } +} diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/Notification.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/Notification.java index 7eab8d4d..9d624dd1 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/Notification.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/Notification.java @@ -5,11 +5,13 @@ import io.github.depromeet.knockknockbackend.domain.reaction.domain.NotificationReaction; import io.github.depromeet.knockknockbackend.domain.user.domain.User; import io.github.depromeet.knockknockbackend.global.database.BaseTimeEntity; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; @@ -61,10 +63,20 @@ public class Notification extends BaseTimeEntity { @OneToMany(mappedBy = "notification", fetch = FetchType.LAZY) private Set notificationReactions = new HashSet<>(); + private LocalDateTime reservedAt; + private boolean deleted; - public void addReceivers(List receivers) { - this.receivers.addAll(receivers); + private void addReceivers(List deviceTokens) { + this.receivers.addAll( + deviceTokens.stream() + .map( + deviceToken -> + new NotificationReceiver( + this, + User.of(deviceToken.getUserId()), + deviceToken.getToken())) + .collect(Collectors.toList())); } public void deleteNotification() { @@ -75,15 +87,25 @@ public static Notification of(Long notificationId) { return Notification.builder().id(notificationId).build(); } - public static Notification of( - String title, String content, String imageUrl, Group group, User sendUser) { - return Notification.builder() - .title(title) - .content(content) - .imageUrl(imageUrl) - .group(group) - .sendUser(sendUser) - .build(); + public static Notification makeNotificationWithReceivers( + List deviceTokens, + String title, + String content, + String imageUrl, + Group group, + User sendUser, + LocalDateTime reservedAt) { + Notification notification = + Notification.builder() + .title(title) + .content(content) + .imageUrl(imageUrl) + .group(group) + .sendUser(sendUser) + .reservedAt(reservedAt) + .build(); + notification.addReceivers(deviceTokens); + return notification; } @Override diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/CustomNotificationRepository.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/CustomNotificationRepository.java index f9d5b664..24901021 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/CustomNotificationRepository.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/CustomNotificationRepository.java @@ -1,6 +1,7 @@ package io.github.depromeet.knockknockbackend.domain.notification.domain.repository; +import io.github.depromeet.knockknockbackend.domain.group.domain.Group; import io.github.depromeet.knockknockbackend.domain.notification.domain.DeviceToken; import io.github.depromeet.knockknockbackend.domain.notification.domain.Notification; import java.util.List; @@ -10,10 +11,13 @@ public interface CustomNotificationRepository { Slice findSliceFromStorage( - Long userId, Long groupId, Integer periodOfMonth, Pageable pageable); + Long userId, List groups, Integer periodOfMonth, Pageable pageable); List findTokenByGroupAndOptionAndNonBlock( Long userId, Long groupId, Boolean nightOption); List findSliceLatestByReceiver(Long receiveUserId); + + Slice findSliceByGroupId( + Long userId, Long groupId, boolean deleted, Pageable pageable); } diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/CustomNotificationRepositoryImpl.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/CustomNotificationRepositoryImpl.java index 06878afb..ae901f69 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/CustomNotificationRepositoryImpl.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/CustomNotificationRepositoryImpl.java @@ -1,5 +1,6 @@ package io.github.depromeet.knockknockbackend.domain.notification.domain.repository; +import static io.github.depromeet.knockknockbackend.domain.group.domain.QGroup.group; import static io.github.depromeet.knockknockbackend.domain.group.domain.QGroupUser.groupUser; import static io.github.depromeet.knockknockbackend.domain.notification.domain.QDeviceToken.deviceToken; import static io.github.depromeet.knockknockbackend.domain.notification.domain.QNotification.notification; @@ -12,6 +13,7 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; +import io.github.depromeet.knockknockbackend.domain.group.domain.Group; import io.github.depromeet.knockknockbackend.domain.notification.domain.DeviceToken; import io.github.depromeet.knockknockbackend.domain.notification.domain.Notification; import java.time.LocalDate; @@ -32,18 +34,42 @@ public class CustomNotificationRepositoryImpl implements CustomNotificationRepos private static final int NUMBER_OF_LATEST_NOTIFICATIONS = 10; private final JPAQueryFactory queryFactory; - private boolean hasNext(List notifications, Pageable pageable) { + private boolean hasNext(List list, Pageable pageable) { boolean hasNext = false; - if (notifications.size() > pageable.getPageSize()) { - notifications.remove(pageable.getPageSize()); + if (list.size() > pageable.getPageSize()) { + list.remove(pageable.getPageSize()); hasNext = true; } return hasNext; } + @Override + public Slice findSliceByGroupId( + Long userId, Long groupId, boolean deleted, Pageable pageable) { + List notifications = + queryFactory + .selectFrom(notification) + .join(notification.group, group) + .fetchJoin() + .where( + group.id.eq(groupId), + notification.deleted.eq(deleted), + JPAExpressions.selectFrom(blockUser) + .where( + blockUser.blockedUser.eq(notification.sendUser), + blockUser.user.id.eq(userId)) + .notExists()) + .orderBy(sort("notification", pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + NEXT_SLICE_CHECK) + .fetch(); + + return new SliceImpl<>(notifications, pageable, hasNext(notifications, pageable)); + } + @Override public Slice findSliceFromStorage( - Long userId, Long groupId, Integer periodOfMonth, Pageable pageable) { + Long userId, List groups, Integer periodOfMonth, Pageable pageable) { List notifications = queryFactory .select(notification) @@ -51,7 +77,7 @@ public Slice findSliceFromStorage( .innerJoin(storage.notification, notification) .where( storage.user.id.eq(userId), - eqGroupId(groupId), + eqGroupIdIn(groups), greaterEqualPeriodOfMonth(periodOfMonth)) .orderBy(sort("storage", pageable)) .offset(pageable.getOffset()) @@ -73,6 +99,7 @@ public List findTokenByGroupAndOptionAndNonBlock( .on(groupUser.user.id.eq(option.userId)) .where( groupUser.group.id.eq(groupId), + deviceToken.user.id.ne(userId), option.newOption.eq(true), eqNightOption(nightOption), JPAExpressions.selectFrom(blockUser) @@ -87,6 +114,8 @@ public List findTokenByGroupAndOptionAndNonBlock( public List findSliceLatestByReceiver(Long receiveUserId) { return queryFactory .selectFrom(notification) + .join(notification.group, group) + .fetchJoin() .where( notification.deleted.eq(false), JPAExpressions.selectFrom(notificationReceiver) @@ -106,11 +135,11 @@ private BooleanExpression eqNightOption(Boolean nightOption) { return option.nightOption.eq(nightOption); } - private BooleanExpression eqGroupId(Long groupId) { - if (groupId == null) { + private BooleanExpression eqGroupIdIn(List groups) { + if (groups.isEmpty()) { return null; } - return notification.group.id.eq(groupId); + return notification.group.in(groups); } private BooleanExpression greaterEqualPeriodOfMonth(Integer periodOfMonth) { diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/NotificationRepository.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/NotificationRepository.java index 1c07cd13..bef07e6a 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/NotificationRepository.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/NotificationRepository.java @@ -3,18 +3,12 @@ import io.github.depromeet.knockknockbackend.domain.notification.domain.Notification; import java.util.Optional; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; public interface NotificationRepository extends JpaRepository, CustomNotificationRepository { - @EntityGraph(attributePaths = {"group"}) - Slice findAllByGroupIdAndDeleted( - Long groupId, boolean deleted, Pageable pageable); - @EntityGraph(attributePaths = {"group"}) Optional findById(Long notificationId); } diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/ReservationRepository.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/ReservationRepository.java index cc9b9c84..0373eac1 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/ReservationRepository.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/domain/repository/ReservationRepository.java @@ -4,9 +4,19 @@ import io.github.depromeet.knockknockbackend.domain.group.domain.Group; import io.github.depromeet.knockknockbackend.domain.notification.domain.Reservation; import io.github.depromeet.knockknockbackend.domain.user.domain.User; +import java.time.LocalDateTime; import java.util.List; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; public interface ReservationRepository extends CrudRepository { List findByGroupAndSendUserOrderBySendAtAsc(Group group, User sendUser); + + List findBySendAtLessThan(LocalDateTime sendAt); + + @Modifying + @Query("delete from Reservation r where r.id in :reservationIds") + void deleteByIdIn(@Param("reservationIds") List reservationIds); } diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/NotificationController.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/NotificationController.java index 0bf5621f..284c8f81 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/NotificationController.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/NotificationController.java @@ -5,6 +5,7 @@ import io.github.depromeet.knockknockbackend.domain.notification.presentation.dto.response.QueryNotificationListLatestResponse; import io.github.depromeet.knockknockbackend.domain.notification.presentation.dto.response.QueryNotificationListResponse; import io.github.depromeet.knockknockbackend.domain.notification.service.NotificationService; +import io.github.depromeet.knockknockbackend.domain.notification.service.ReservationService; import io.github.depromeet.knockknockbackend.global.annotation.DisableSecurity; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -24,6 +25,7 @@ public class NotificationController { private final NotificationService notificationService; + private final ReservationService reservationService; @Operation(summary = "최신 푸쉬알림 리스트") @GetMapping @@ -45,13 +47,6 @@ public void sendInstance(@RequestBody SendInstanceRequest request) { notificationService.sendInstance(request); } - @Operation(summary = "예약 푸쉬알림 발송") - @ResponseStatus(HttpStatus.CREATED) - @PostMapping("/reservation") - public void sendReservation(@RequestBody SendReservationRequest request) { - notificationService.sendReservation(request); - } - @Operation(summary = "알림방 푸쉬알림 리스트") @GetMapping("/{group_id}") public QueryNotificationListResponse queryListByGroupId( @@ -78,17 +73,24 @@ public void sendInstanceToMeBeforeSignUp( notificationService.sendInstanceToMeBeforeSignUp(request); } + @Operation(summary = "예약 푸쉬알림 발송") + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("/reservation") + public void sendReservation(@RequestBody SendReservationRequest request) { + reservationService.sendReservation(request); + } + @Operation(summary = "예약 푸쉬알림 시간수정") @ResponseStatus(HttpStatus.OK) @PatchMapping("/reservation") public void changeSendAtReservation(@RequestBody ChangeSendAtReservationRequest request) { - notificationService.changeSendAtReservation(request); + reservationService.changeSendAtReservation(request); } @Operation(summary = "예약 푸쉬알림 삭제") @ResponseStatus(HttpStatus.OK) @DeleteMapping("/reservation/{reservation_id}") public void deleteReservation(@PathVariable("reservation_id") Long reservationId) { - notificationService.deleteReservation(reservationId); + reservationService.deleteReservation(reservationId); } } diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/dto/response/QueryNotificationListResponse.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/dto/response/QueryNotificationListResponse.java index e46f89b7..cacd8572 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/dto/response/QueryNotificationListResponse.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/dto/response/QueryNotificationListResponse.java @@ -1,7 +1,6 @@ package io.github.depromeet.knockknockbackend.domain.notification.presentation.dto.response; -import io.github.depromeet.knockknockbackend.domain.group.presentation.dto.response.GroupBriefInfoDto; import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; @@ -11,7 +10,6 @@ @AllArgsConstructor public class QueryNotificationListResponse { - private final GroupBriefInfoDto groups; private final List reservations; private final Slice notifications; } diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/dto/response/QueryNotificationListResponseElement.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/dto/response/QueryNotificationListResponseElement.java index 26919c0c..d14d5793 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/dto/response/QueryNotificationListResponseElement.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/presentation/dto/response/QueryNotificationListResponseElement.java @@ -1,6 +1,7 @@ package io.github.depromeet.knockknockbackend.domain.notification.presentation.dto.response; +import io.github.depromeet.knockknockbackend.domain.group.presentation.dto.response.GroupInfoForNotificationDto; import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; @@ -14,5 +15,6 @@ public class QueryNotificationListResponseElement { private String imageUrl; private LocalDateTime createdDate; private Long sendUserId; + private GroupInfoForNotificationDto groups; private QueryNotificationReactionResponseElement reactions; } diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/service/NotificationReservationScheduler.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/service/NotificationReservationScheduler.java new file mode 100644 index 00000000..96dd67dd --- /dev/null +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/service/NotificationReservationScheduler.java @@ -0,0 +1,18 @@ +package io.github.depromeet.knockknockbackend.domain.notification.service; + + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class NotificationReservationScheduler { + + private final ReservationService reservationService; + + @Scheduled(cron = "0 0/1 * * * *") + public void reservationNotification() { + reservationService.processScheduledReservation(); + } +} diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/service/NotificationService.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/service/NotificationService.java index 74dcbf81..944dc2f3 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/service/NotificationService.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/service/NotificationService.java @@ -1,9 +1,8 @@ package io.github.depromeet.knockknockbackend.domain.notification.service; -import com.google.firebase.messaging.*; import io.github.depromeet.knockknockbackend.domain.group.domain.Group; -import io.github.depromeet.knockknockbackend.domain.group.presentation.dto.response.GroupBriefInfoDto; +import io.github.depromeet.knockknockbackend.domain.group.presentation.dto.response.GroupInfoForNotificationDto; import io.github.depromeet.knockknockbackend.domain.notification.domain.*; import io.github.depromeet.knockknockbackend.domain.notification.domain.Notification; import io.github.depromeet.knockknockbackend.domain.notification.domain.repository.DeviceTokenRepository; @@ -18,6 +17,8 @@ import io.github.depromeet.knockknockbackend.domain.reaction.domain.repository.NotificationReactionRepository; import io.github.depromeet.knockknockbackend.domain.user.domain.User; import io.github.depromeet.knockknockbackend.global.utils.security.SecurityUtils; +import io.github.depromeet.knockknockbackend.infrastructor.fcm.FcmService; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -35,12 +36,13 @@ public class NotificationService implements NotificationUtils { private static final boolean CREATED_DELETED_STATUS = false; + private final EntityManager entityManager; + private final FcmService fcmService; private final NotificationRepository notificationRepository; - private final ReservationRepository reservationRepository; - private final NotificationExperienceRepository notificationExperienceRepository; private final DeviceTokenRepository deviceTokenRepository; private final NotificationReactionRepository notificationReactionRepository; - private final EntityManager entityManager; + private final ReservationRepository reservationRepository; + private final NotificationExperienceRepository notificationExperienceRepository; @Transactional(readOnly = true) public QueryNotificationListLatestResponse queryListLatest() { @@ -61,17 +63,13 @@ public QueryNotificationListLatestResponse queryListLatest() { @Transactional(readOnly = true) public QueryNotificationListResponse queryListByGroupId(Pageable pageable, Long groupId) { - Slice notifications = - notificationRepository.findAllByGroupIdAndDeleted( - groupId, CREATED_DELETED_STATUS, pageable); - Optional groupBriefInfoDto = - notifications.stream() - .findFirst() - .map( - notification -> - new GroupBriefInfoDto( - notification.getGroup().getGroupBaseInfoVo())); + Slice notifications = + notificationRepository.findSliceByGroupId( + SecurityUtils.getCurrentUserId(), + groupId, + CREATED_DELETED_STATUS, + pageable); List myNotificationReactions = retrieveMyReactions(notifications.getContent()); @@ -98,9 +96,7 @@ public QueryNotificationListResponse queryListByGroupId(Pageable pageable, Long .collect(Collectors.toList()); return new QueryNotificationListResponse( - groupBriefInfoDto.orElse(null), - queryReservationListResponseElements, - queryNotificationListResponseElements); + queryReservationListResponseElements, queryNotificationListResponseElements); } public QueryNotificationListResponseElement getQueryNotificationListResponseElements( @@ -138,6 +134,9 @@ public QueryNotificationListResponseElement getQueryNotificationListResponseElem .imageUrl(notification.getImageUrl()) .createdDate(notification.getCreatedDate()) .sendUserId(notification.getSendUser().getId()) + .groups( + new GroupInfoForNotificationDto( + notification.getGroup().getGroupBaseInfoVo())) .reactions(notificationReactionResponseElement) .build(); } @@ -171,63 +170,26 @@ public void registerFcmToken(RegisterFcmTokenRequest request) { public void sendInstance(SendInstanceRequest request) { Long sendUserId = SecurityUtils.getCurrentUserId(); - List deviceTokens = getDeviceTokens(request, sendUserId); - List tokens = getTokens(deviceTokens); - MulticastMessage multicastMessage = makeMulticastMessageForFcm(request, tokens); - - try { - BatchResponse batchResponse = - FirebaseMessaging.getInstance().sendMulticast(multicastMessage); - if (batchResponse.getFailureCount() >= 1) { - logFcmMessagingException(batchResponse); - } - } catch (FirebaseMessagingException e) { - log.error("[**FCM notification sending Error] {} ", e.getMessage()); - throw FcmResponseException.EXCEPTION; - } - - Notification notification = - Notification.of( - request.getTitle(), - request.getContent(), - request.getImageUrl(), - Group.of(request.getGroupId()), - User.of(sendUserId)); - notification.addReceivers( - deviceTokens.stream() - .map( - deviceToken -> - new NotificationReceiver( - notification, - User.of(deviceToken.getUserId()), - deviceToken.getToken())) - .collect(Collectors.toList())); - notificationRepository.save(notification); - } + List deviceTokens = getDeviceTokens(request.getGroupId(), sendUserId); + List tokens = getFcmTokens(deviceTokens); - public void sendReservation(SendReservationRequest request) { - Long currentUserId = SecurityUtils.getCurrentUserId(); + fcmService.sendGroupMessage( + tokens, request.getTitle(), request.getContent(), request.getImageUrl()); - reservationRepository.save( - Reservation.of( - request.getSendAt(), - request.getTitle(), - request.getContent(), - request.getImageUrl(), - Group.of(request.getGroupId()), - User.of(currentUserId))); + recordNotification( + deviceTokens, + request.getTitle(), + request.getContent(), + request.getImageUrl(), + Group.of(request.getGroupId()), + User.of(sendUserId), + null); } public void sendInstanceToMeBeforeSignUp(SendInstanceToMeBeforeSignUpRequest request) { - Message message = makeMessageForFcm(request, request.getToken()); - try { - FirebaseMessaging.getInstance().send(message); - notificationExperienceRepository.save( - NotificationExperience.of(request.getToken(), request.getContent())); - } catch (FirebaseMessagingException e) { - log.error("[**FCM notification Experience sending Error] {} ", e.getMessage()); - throw FcmResponseException.EXCEPTION; - } + fcmService.sendMessage(request.getToken(), request.getContent()); + notificationExperienceRepository.save( + NotificationExperience.of(request.getToken(), request.getContent())); } public void deleteByNotificationId(Long notificationId) { @@ -237,74 +199,37 @@ public void deleteByNotificationId(Long notificationId) { notificationRepository.save(notification); } - public void changeSendAtReservation(ChangeSendAtReservationRequest request) { - Reservation reservation = queryReservationById(request.getReservationId()); - reservation.changeSendAt(request.getSendAt()); - reservationRepository.save(reservation); - } - - @Transactional - public void deleteReservation(Long reservationId) { - Reservation reservation = queryReservationById(reservationId); - validateDeletePermissionReservation(reservation); - reservationRepository.delete(reservation); - } - - private void logFcmMessagingException(BatchResponse batchResponse) { - log.error( - "[**FCM notification sending Error] successCount : {}, failureCount : {} ", - batchResponse.getSuccessCount(), - batchResponse.getFailureCount()); - batchResponse.getResponses().stream() - .filter(sendResponse -> sendResponse.getException() != null) - .forEach( - sendResponse -> - log.error( - "[**FCM notification sending Error] errorCode: {}, errorMessage : {}", - sendResponse.getException().getErrorCode(), - sendResponse.getException().getMessage())); - } - - private MulticastMessage makeMulticastMessageForFcm( - SendInstanceRequest request, List tokens) { - return MulticastMessage.builder() - .setNotification( - com.google.firebase.messaging.Notification.builder() - .setTitle(request.getTitle()) - .setBody(request.getContent()) - .setImage(request.getImageUrl()) - .build()) - .addAllTokens(tokens) - .build(); - } - - private Message makeMessageForFcm(SendInstanceToMeBeforeSignUpRequest request, String token) { - return Message.builder() - .setNotification( - com.google.firebase.messaging.Notification.builder() - .setBody(request.getContent()) - .build()) - .setToken(token) - .build(); - } - - private List getDeviceTokens(SendInstanceRequest request, Long sendUserId) { + public List getDeviceTokens(Long groupId, Long sendUserId) { Boolean nightOption = null; if (NightCondition.isNight()) { nightOption = true; } return notificationRepository.findTokenByGroupAndOptionAndNonBlock( - sendUserId, request.getGroupId(), nightOption); + sendUserId, groupId, nightOption); } - private List getTokens(List deviceTokens) { + public List getFcmTokens(List deviceTokens) { return deviceTokens.stream().map(DeviceToken::getToken).collect(Collectors.toList()); } + public void recordNotification( + List deviceTokens, + String title, + String content, + String imageUrl, + Group group, + User sendUser, + LocalDateTime createdDate) { + Notification notification = + Notification.makeNotificationWithReceivers( + deviceTokens, title, content, imageUrl, group, sendUser, createdDate); + notificationRepository.save(notification); + } + public List retrieveMyReactions(List notifications) { - return notificationReactionRepository.findByUserIdAndNotificationIn( - SecurityUtils.getCurrentUserId(), notifications); + return notificationReactionRepository.findByUserAndNotificationIn( + User.of(SecurityUtils.getCurrentUserId()), notifications); } private void validateDeletePermission(Notification notification) { @@ -319,16 +244,4 @@ public Notification queryNotificationById(Long notificationId) { .findById(notificationId) .orElseThrow(() -> NotificationNotFoundException.EXCEPTION); } - - private Reservation queryReservationById(Long reservationId) { - return reservationRepository - .findById(reservationId) - .orElseThrow(() -> ReservationNotFoundException.EXCEPTION); - } - - private void validateDeletePermissionReservation(Reservation reservation) { - if (!SecurityUtils.getCurrentUserId().equals(reservation.getSendUser().getId())) { - throw ReservationForbiddenException.EXCEPTION; - } - } } diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/service/ReservationService.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/service/ReservationService.java new file mode 100644 index 00000000..4825fe0d --- /dev/null +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/notification/service/ReservationService.java @@ -0,0 +1,115 @@ +package io.github.depromeet.knockknockbackend.domain.notification.service; + + +import io.github.depromeet.knockknockbackend.domain.group.domain.Group; +import io.github.depromeet.knockknockbackend.domain.notification.domain.DeviceToken; +import io.github.depromeet.knockknockbackend.domain.notification.domain.Reservation; +import io.github.depromeet.knockknockbackend.domain.notification.domain.repository.ReservationRepository; +import io.github.depromeet.knockknockbackend.domain.notification.exception.ReservationForbiddenException; +import io.github.depromeet.knockknockbackend.domain.notification.exception.ReservationNotFoundException; +import io.github.depromeet.knockknockbackend.domain.notification.presentation.dto.request.ChangeSendAtReservationRequest; +import io.github.depromeet.knockknockbackend.domain.notification.presentation.dto.request.SendReservationRequest; +import io.github.depromeet.knockknockbackend.domain.user.domain.User; +import io.github.depromeet.knockknockbackend.global.utils.security.SecurityUtils; +import io.github.depromeet.knockknockbackend.infrastructor.fcm.FcmService; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Service +public class ReservationService { + + private final FcmService fcmService; + private final NotificationService notificationService; + + private final ReservationRepository reservationRepository; + + public void sendReservation(SendReservationRequest request) { + Long currentUserId = SecurityUtils.getCurrentUserId(); + + reservationRepository.save( + Reservation.of( + request.getSendAt(), + request.getTitle(), + request.getContent(), + request.getImageUrl(), + Group.of(request.getGroupId()), + User.of(currentUserId))); + } + + public void changeSendAtReservation(ChangeSendAtReservationRequest request) { + Reservation reservation = queryReservationById(request.getReservationId()); + reservation.changeSendAt(request.getSendAt()); + reservationRepository.save(reservation); + } + + @Transactional + public void deleteReservation(Long reservationId) { + Reservation reservation = queryReservationById(reservationId); + validateDeletePermissionReservation(reservation); + reservationRepository.delete(reservation); + } + + @Transactional + public void processScheduledReservation() { + log.info("**Reservation Scheduled process time:" + LocalDateTime.now()); + + List reservations = retrieveReservation(); + if (reservations.isEmpty()) { + log.info("**Reservation Scheduled data is nothing"); + return; + } + deleteReservation( + reservations.stream().map(Reservation::getId).collect(Collectors.toList())); + + reservations.forEach( + reservation -> { + List deviceTokens = + notificationService.getDeviceTokens( + reservation.getGroup().getId(), + reservation.getSendUser().getId()); + List tokens = notificationService.getFcmTokens(deviceTokens); + + fcmService.sendGroupMessage( + tokens, + reservation.getTitle(), + reservation.getContent(), + reservation.getImageUrl()); + + notificationService.recordNotification( + deviceTokens, + reservation.getTitle(), + reservation.getContent(), + reservation.getImageUrl(), + reservation.getGroup(), + reservation.getSendUser(), + reservation.getCreatedDate()); + }); + } + + private List retrieveReservation() { + return reservationRepository.findBySendAtLessThan(LocalDateTime.now()); + } + + private void deleteReservation(List reservationIds) { + reservationRepository.deleteByIdIn(reservationIds); + } + + private Reservation queryReservationById(Long reservationId) { + return reservationRepository + .findById(reservationId) + .orElseThrow(() -> ReservationNotFoundException.EXCEPTION); + } + + private void validateDeletePermissionReservation(Reservation reservation) { + if (!SecurityUtils.getCurrentUserId().equals(reservation.getSendUser().getId())) { + throw ReservationForbiddenException.EXCEPTION; + } + } +} diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/reaction/domain/repository/NotificationReactionRepository.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/reaction/domain/repository/NotificationReactionRepository.java index f3978e49..2e2dffc5 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/reaction/domain/repository/NotificationReactionRepository.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/reaction/domain/repository/NotificationReactionRepository.java @@ -4,6 +4,7 @@ import io.github.depromeet.knockknockbackend.domain.notification.domain.Notification; import io.github.depromeet.knockknockbackend.domain.notification.domain.vo.NotificationReactionCountInfoVo; import io.github.depromeet.knockknockbackend.domain.reaction.domain.NotificationReaction; +import io.github.depromeet.knockknockbackend.domain.user.domain.User; import java.util.List; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; @@ -17,6 +18,6 @@ public interface NotificationReactionRepository extends CrudRepository findAllCountByNotification(Notification notification); - List findByUserIdAndNotificationIn( - Long userId, List notifications); + List findByUserAndNotificationIn( + User user, List notifications); } diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/storage/presentation/StorageController.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/storage/presentation/StorageController.java index 76813562..4be862e6 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/storage/presentation/StorageController.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/storage/presentation/StorageController.java @@ -3,6 +3,7 @@ import io.github.depromeet.knockknockbackend.domain.notification.presentation.dto.response.QueryNotificationListInStorageResponse; import io.github.depromeet.knockknockbackend.domain.storage.presentation.dto.request.DeleteStorage; +import io.github.depromeet.knockknockbackend.domain.storage.presentation.dto.request.QueryStorageByGroupIds; import io.github.depromeet.knockknockbackend.domain.storage.service.StorageService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -26,11 +27,11 @@ public class StorageController { @Operation(summary = "보관함 푸쉬알림 리스트 조회") @GetMapping public QueryNotificationListInStorageResponse queryNotificationsInStorage( - @RequestParam(value = "groupId", required = false) Long groupId, + @RequestBody(required = false) QueryStorageByGroupIds request, @RequestParam(value = "periodOfMonth", required = false) Integer periodOfMonth, @PageableDefault(size = 20, sort = "createdDate", direction = Direction.DESC) Pageable pageable) { - return storageService.queryNotificationsInStorage(groupId, periodOfMonth, pageable); + return storageService.queryNotificationsInStorage(request, periodOfMonth, pageable); } @Operation(summary = "보관함에 푸쉬알림 보관") diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/storage/presentation/dto/request/QueryStorageByGroupIds.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/storage/presentation/dto/request/QueryStorageByGroupIds.java new file mode 100644 index 00000000..0d0e99a0 --- /dev/null +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/storage/presentation/dto/request/QueryStorageByGroupIds.java @@ -0,0 +1,10 @@ +package io.github.depromeet.knockknockbackend.domain.storage.presentation.dto.request; + + +import java.util.List; +import lombok.Getter; + +@Getter +public class QueryStorageByGroupIds { + List groupIds; +} diff --git a/src/main/java/io/github/depromeet/knockknockbackend/domain/storage/service/StorageService.java b/src/main/java/io/github/depromeet/knockknockbackend/domain/storage/service/StorageService.java index 7ac14e95..fcba301a 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/domain/storage/service/StorageService.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/domain/storage/service/StorageService.java @@ -1,6 +1,7 @@ package io.github.depromeet.knockknockbackend.domain.storage.service; +import io.github.depromeet.knockknockbackend.domain.group.domain.Group; import io.github.depromeet.knockknockbackend.domain.group.domain.repository.GroupUserRepository; import io.github.depromeet.knockknockbackend.domain.notification.domain.Notification; import io.github.depromeet.knockknockbackend.domain.notification.domain.repository.NotificationRepository; @@ -15,8 +16,10 @@ import io.github.depromeet.knockknockbackend.domain.storage.exception.StorageForbiddenException; import io.github.depromeet.knockknockbackend.domain.storage.exception.StorageNotFoundException; import io.github.depromeet.knockknockbackend.domain.storage.presentation.dto.request.DeleteStorage; +import io.github.depromeet.knockknockbackend.domain.storage.presentation.dto.request.QueryStorageByGroupIds; import io.github.depromeet.knockknockbackend.domain.user.domain.User; import io.github.depromeet.knockknockbackend.global.utils.security.SecurityUtils; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -52,10 +55,15 @@ public void deleteNotificationFromStorage(DeleteStorage request) { @Transactional(readOnly = true) public QueryNotificationListInStorageResponse queryNotificationsInStorage( - Long groupId, Integer periodOfMonth, Pageable pageable) { + QueryStorageByGroupIds request, Integer periodOfMonth, Pageable pageable) { + List groups = new ArrayList<>(); + if (request != null) { + groups = request.getGroupIds().stream().map(Group::of).collect(Collectors.toList()); + } + Slice notifications = notificationRepository.findSliceFromStorage( - SecurityUtils.getCurrentUserId(), groupId, periodOfMonth, pageable); + SecurityUtils.getCurrentUserId(), groups, periodOfMonth, pageable); List myNotificationReactions = notificationService.retrieveMyReactions(notifications.getContent()); diff --git a/src/main/java/io/github/depromeet/knockknockbackend/global/config/ScheduledConfig.java b/src/main/java/io/github/depromeet/knockknockbackend/global/config/ScheduledConfig.java new file mode 100644 index 00000000..74c4a0c5 --- /dev/null +++ b/src/main/java/io/github/depromeet/knockknockbackend/global/config/ScheduledConfig.java @@ -0,0 +1,9 @@ +package io.github.depromeet.knockknockbackend.global.config; + + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@Configuration +public class ScheduledConfig {} diff --git a/src/main/java/io/github/depromeet/knockknockbackend/infrastructor/fcm/FcmService.java b/src/main/java/io/github/depromeet/knockknockbackend/infrastructor/fcm/FcmService.java index 0aa8b619..ee6e097f 100644 --- a/src/main/java/io/github/depromeet/knockknockbackend/infrastructor/fcm/FcmService.java +++ b/src/main/java/io/github/depromeet/knockknockbackend/infrastructor/fcm/FcmService.java @@ -15,12 +15,17 @@ @Service public class FcmService { - private void sendGroupMessage(List tokenList, String title, String content) { + public void sendGroupMessage( + List tokenList, String title, String content, String imageUrl) { MulticastMessage multicast = MulticastMessage.builder() .addAllTokens(tokenList) .setNotification( - Notification.builder().setTitle(title).setBody(content).build()) + Notification.builder() + .setTitle(title) + .setBody(content) + .setImage(imageUrl) + .build()) .setApnsConfig( ApnsConfig.builder() .setAps(Aps.builder().setSound("default").build()) @@ -30,12 +35,11 @@ private void sendGroupMessage(List tokenList, String title, String conte FirebaseMessaging.getInstance().sendMulticastAsync(multicast); } - private void sendMessage(String token, String title, String content) { + public void sendMessage(String token, String content) { Message message = Message.builder() .setToken(token) - .setNotification( - Notification.builder().setTitle(title).setBody(content).build()) + .setNotification(Notification.builder().setBody(content).build()) .setApnsConfig( ApnsConfig.builder() .setAps(Aps.builder().setSound("default").build())