diff --git a/src/main/java/com/moabam/api/application/AuthenticationService.java b/src/main/java/com/moabam/api/application/AuthenticationService.java index c53b10f1..384d4caa 100644 --- a/src/main/java/com/moabam/api/application/AuthenticationService.java +++ b/src/main/java/com/moabam/api/application/AuthenticationService.java @@ -14,8 +14,8 @@ import com.moabam.api.dto.AuthorizationTokenResponse; import com.moabam.api.dto.LoginResponse; import com.moabam.api.dto.OAuthMapper; -import com.moabam.global.common.constant.GlobalConstant; import com.moabam.global.common.util.CookieUtils; +import com.moabam.global.common.util.GlobalConstant; import com.moabam.global.config.OAuthConfig; import com.moabam.global.error.exception.BadRequestException; import com.moabam.global.error.model.ErrorMessage; diff --git a/src/main/java/com/moabam/api/application/NotificationService.java b/src/main/java/com/moabam/api/application/NotificationService.java index 4bf18136..d70cd57a 100644 --- a/src/main/java/com/moabam/api/application/NotificationService.java +++ b/src/main/java/com/moabam/api/application/NotificationService.java @@ -1,14 +1,20 @@ package com.moabam.api.application; -import static com.moabam.global.common.constant.FcmConstant.*; +import static com.moabam.global.common.util.GlobalConstant.*; +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; +import com.moabam.api.domain.entity.Participant; import com.moabam.api.domain.repository.NotificationRepository; +import com.moabam.api.domain.repository.ParticipantSearchRepository; import com.moabam.api.dto.NotificationMapper; import com.moabam.global.common.annotation.MemberTest; import com.moabam.global.error.exception.ConflictException; @@ -22,21 +28,43 @@ @Transactional(readOnly = true) public class NotificationService { + private final RoomService roomService; private final FirebaseMessaging firebaseMessaging; private final NotificationRepository notificationRepository; + private final ParticipantSearchRepository participantSearchRepository; @Transactional public void sendKnockNotification(MemberTest member, Long targetId, Long roomId) { + roomService.validateRoomById(roomId); + String knockKey = generateKnockKey(member.memberId(), targetId, roomId); validateConflictKnockNotification(knockKey); validateFcmToken(targetId); - String fcmToken = notificationRepository.findFcmTokenByMemberId(targetId); Notification notification = NotificationMapper.toKnockNotificationEntity(member.nickname()); - Message message = NotificationMapper.toMessageEntity(notification, fcmToken); - + sendAsyncFcm(targetId, notification); notificationRepository.saveKnockNotification(knockKey); - firebaseMessaging.sendAsync(message); + } + + @Scheduled(cron = "0 50 * * * *") + public void sendCertificationTimeNotification() { + int certificationTime = (LocalDateTime.now().getHour() + ONE_HOUR) % HOURS_IN_A_DAY; + List participants = participantSearchRepository.findAllByRoomCertifyTime(certificationTime); + + participants.parallelStream().forEach(participant -> { + String roomTitle = participant.getRoom().getTitle(); + Notification notification = NotificationMapper.toCertifyAuthNotificationEntity(roomTitle); + sendAsyncFcm(participant.getMemberId(), notification); + }); + } + + private void sendAsyncFcm(Long fcmTokenKey, Notification notification) { + String fcmToken = notificationRepository.findFcmTokenByMemberId(fcmTokenKey); + + if (fcmToken != null) { + Message message = NotificationMapper.toMessageEntity(notification, fcmToken); + firebaseMessaging.sendAsync(message); + } } private void validateConflictKnockNotification(String knockKey) { diff --git a/src/main/java/com/moabam/api/application/OAuth2AuthorizationServerRequestService.java b/src/main/java/com/moabam/api/application/OAuth2AuthorizationServerRequestService.java index 66076649..78c3c278 100644 --- a/src/main/java/com/moabam/api/application/OAuth2AuthorizationServerRequestService.java +++ b/src/main/java/com/moabam/api/application/OAuth2AuthorizationServerRequestService.java @@ -14,7 +14,7 @@ import com.moabam.api.dto.AuthorizationTokenInfoResponse; import com.moabam.api.dto.AuthorizationTokenResponse; -import com.moabam.global.common.constant.GlobalConstant; +import com.moabam.global.common.util.GlobalConstant; import com.moabam.global.error.exception.BadRequestException; import com.moabam.global.error.handler.RestTemplateResponseHandler; import com.moabam.global.error.model.ErrorMessage; diff --git a/src/main/java/com/moabam/api/domain/repository/NotificationRepository.java b/src/main/java/com/moabam/api/domain/repository/NotificationRepository.java index b280c8be..f93f9ac2 100644 --- a/src/main/java/com/moabam/api/domain/repository/NotificationRepository.java +++ b/src/main/java/com/moabam/api/domain/repository/NotificationRepository.java @@ -1,7 +1,6 @@ package com.moabam.api.domain.repository; -import static com.moabam.global.common.constant.FcmConstant.*; -import static com.moabam.global.common.constant.GlobalConstant.*; +import static com.moabam.global.common.util.GlobalConstant.*; import static java.util.Objects.*; import java.time.Duration; @@ -16,6 +15,9 @@ @RequiredArgsConstructor public class NotificationRepository { + private static final long EXPIRE_KNOCK = 12; + private static final long EXPIRE_FCM_TOKEN = 60; + private final StringRedisRepository stringRedisRepository; // TODO : 세연님 로그인 시, 해당 메서드 사용해서 해당 유저의 FCM TOKEN 저장하면 됩니다. Front와 상의 후 삭제예정 diff --git a/src/main/java/com/moabam/api/domain/repository/ParticipantSearchRepository.java b/src/main/java/com/moabam/api/domain/repository/ParticipantSearchRepository.java index 91830965..768e6946 100644 --- a/src/main/java/com/moabam/api/domain/repository/ParticipantSearchRepository.java +++ b/src/main/java/com/moabam/api/domain/repository/ParticipantSearchRepository.java @@ -41,4 +41,12 @@ public List findParticipants(Long roomId) { ) .fetch(); } + + public List findAllByRoomCertifyTime(int certifyTime) { + return jpaQueryFactory + .selectFrom(participant) + .join(participant.room, room).fetchJoin() + .where(participant.room.certifyTime.eq(certifyTime)) + .fetch(); + } } diff --git a/src/main/java/com/moabam/api/dto/NotificationMapper.java b/src/main/java/com/moabam/api/dto/NotificationMapper.java index 79795e15..b5283825 100644 --- a/src/main/java/com/moabam/api/dto/NotificationMapper.java +++ b/src/main/java/com/moabam/api/dto/NotificationMapper.java @@ -9,16 +9,24 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class NotificationMapper { - private static final String TITLE = "모아밤"; + private static final String NOTIFICATION_TITLE = "모아밤"; private static final String KNOCK_BODY = "님이 콕 찔렀습니다."; + private static final String CERTIFY_TIME_BODY = "방 인증 시간입니다."; public static Notification toKnockNotificationEntity(String nickname) { return Notification.builder() - .setTitle(TITLE) + .setTitle(NOTIFICATION_TITLE) .setBody(nickname + KNOCK_BODY) .build(); } + public static Notification toCertifyAuthNotificationEntity(String title) { + return Notification.builder() + .setTitle(NOTIFICATION_TITLE) + .setBody(title + CERTIFY_TIME_BODY) + .build(); + } + public static Message toMessageEntity(Notification notification, String fcmToken) { return Message.builder() .setNotification(notification) diff --git a/src/main/java/com/moabam/global/common/constant/FcmConstant.java b/src/main/java/com/moabam/global/common/constant/FcmConstant.java deleted file mode 100644 index 7a0cf6d2..00000000 --- a/src/main/java/com/moabam/global/common/constant/FcmConstant.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.moabam.global.common.constant; - -public class FcmConstant { - - public static final long EXPIRE_KNOCK = 12; - public static final long EXPIRE_FCM_TOKEN = 60; - public static final String KNOCK_KEY = "room_%s_member_%s_knocks_%s"; - public static final String FIREBASE_PATH = "config/moabam-firebase.json"; -} diff --git a/src/main/java/com/moabam/global/common/constant/GlobalConstant.java b/src/main/java/com/moabam/global/common/util/GlobalConstant.java similarity index 51% rename from src/main/java/com/moabam/global/common/constant/GlobalConstant.java rename to src/main/java/com/moabam/global/common/util/GlobalConstant.java index 436df263..f1714cc1 100644 --- a/src/main/java/com/moabam/global/common/constant/GlobalConstant.java +++ b/src/main/java/com/moabam/global/common/util/GlobalConstant.java @@ -1,4 +1,4 @@ -package com.moabam.global.common.constant; +package com.moabam.global.common.util; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -9,4 +9,8 @@ public class GlobalConstant { public static final String BLANK = ""; public static final String CHARSET_UTF_8 = ";charset=UTF-8"; public static final String SPACE = " "; + public static final int ONE_HOUR = 1; + public static final int HOURS_IN_A_DAY = 24; + public static final String KNOCK_KEY = "room_%s_member_%s_knocks_%s"; + public static final String FIREBASE_PATH = "config/moabam-firebase.json"; } diff --git a/src/main/java/com/moabam/global/config/FcmConfig.java b/src/main/java/com/moabam/global/config/FcmConfig.java index 0c5a7cbc..678248b5 100644 --- a/src/main/java/com/moabam/global/config/FcmConfig.java +++ b/src/main/java/com/moabam/global/config/FcmConfig.java @@ -1,6 +1,6 @@ package com.moabam.global.config; -import static com.moabam.global.common.constant.FcmConstant.*; +import static com.moabam.global.common.util.GlobalConstant.*; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/resources/config b/src/main/resources/config index 8ba1e5fb..7026a658 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit 8ba1e5fbd724fc621b1f082c98356754244ad355 +Subproject commit 7026a65853d700a4f25a700fd327e926b562eabf diff --git a/src/main/resources/static/docs/index.html b/src/main/resources/static/docs/index.html index 06e3fd55..62f80fd6 100644 --- a/src/main/resources/static/docs/index.html +++ b/src/main/resources/static/docs/index.html @@ -616,7 +616,7 @@

diff --git a/src/main/resources/static/docs/notification.html b/src/main/resources/static/docs/notification.html index 1c3d4e18..b644496c 100644 --- a/src/main/resources/static/docs/notification.html +++ b/src/main/resources/static/docs/notification.html @@ -473,7 +473,7 @@

응답

Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Content-Type: application/json -Content-Length: 66 +Content-Length: 64 { "message" : "이미 콕 알림을 보낸 대상입니다." @@ -487,7 +487,7 @@

응답

diff --git a/src/test/java/com/moabam/api/application/NotificationServiceTest.java b/src/test/java/com/moabam/api/application/NotificationServiceTest.java index d733cd5b..ec72a657 100644 --- a/src/test/java/com/moabam/api/application/NotificationServiceTest.java +++ b/src/test/java/com/moabam/api/application/NotificationServiceTest.java @@ -3,17 +3,23 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; +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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.Message; +import com.moabam.api.domain.entity.Participant; import com.moabam.api.domain.repository.NotificationRepository; +import com.moabam.api.domain.repository.ParticipantSearchRepository; import com.moabam.global.common.annotation.MemberTest; import com.moabam.global.error.exception.ConflictException; import com.moabam.global.error.exception.NotFoundException; @@ -26,11 +32,17 @@ class NotificationServiceTest { private NotificationService notificationService; @Mock - private NotificationRepository notificationRepository; + private RoomService roomService; @Mock private FirebaseMessaging firebaseMessaging; + @Mock + private NotificationRepository notificationRepository; + + @Mock + private ParticipantSearchRepository participantSearchRepository; + private MemberTest memberTest; @BeforeEach @@ -42,6 +54,7 @@ void setUp() { @Test void notificationService_sendKnockNotification() { // Given + willDoNothing().given(roomService).validateRoomById(any(Long.class)); given(notificationRepository.existsFcmTokenByMemberId(any(Long.class))).willReturn(true); given(notificationRepository.existsByKey(any(String.class))).willReturn(false); given(notificationRepository.findFcmTokenByMemberId(any(Long.class))).willReturn("FCM-TOKEN"); @@ -54,10 +67,22 @@ void notificationService_sendKnockNotification() { verify(notificationRepository).saveKnockNotification(any(String.class)); } + @DisplayName("콕 찌를 상대의 방이 존재하지 않을 때, - NotFoundException") + @Test + void notificationService_sendKnockNotification_Room_NotFoundException() { + // Given + willThrow(NotFoundException.class).given(roomService).validateRoomById(any(Long.class)); + + // When & Then + assertThatThrownBy(() -> notificationService.sendKnockNotification(memberTest, 1L, 1L)) + .isInstanceOf(NotFoundException.class); + } + @DisplayName("콕 찌를 상대의 FCM 토큰이 존재하지 않을 때, - NotFoundException") @Test - void notificationService_sendKnockNotification_NotFoundException() { + void notificationService_sendKnockNotification_FcmToken_NotFoundException() { // Given + willDoNothing().given(roomService).validateRoomById(any(Long.class)); given(notificationRepository.existsByKey(any(String.class))).willReturn(false); given(notificationRepository.existsFcmTokenByMemberId(any(Long.class))).willReturn(false); @@ -71,6 +96,7 @@ void notificationService_sendKnockNotification_NotFoundException() { @Test void notificationService_sendKnockNotification_ConflictException() { // Given + willDoNothing().given(roomService).validateRoomById(any(Long.class)); given(notificationRepository.existsByKey(any(String.class))).willReturn(true); // When & Then @@ -78,4 +104,34 @@ void notificationService_sendKnockNotification_ConflictException() { .isInstanceOf(ConflictException.class) .hasMessage(ErrorMessage.CONFLICT_KNOCK.getMessage()); } + + @DisplayName("특정 인증 시간에 해당하는 방 사용자들에게 알림을 성공적으로 보낼 때, - Void") + @MethodSource("com.moabam.support.fixture.ParticipantFixture#provideParticipants") + @ParameterizedTest + void notificationService_sendCertificationTimeNotification(List participants) { + // Given + given(participantSearchRepository.findAllByRoomCertifyTime(any(Integer.class))).willReturn(participants); + given(notificationRepository.findFcmTokenByMemberId(any(Long.class))).willReturn("FCM-TOKEN"); + + // When + notificationService.sendCertificationTimeNotification(); + + // Then + verify(firebaseMessaging, times(3)).sendAsync(any(Message.class)); + } + + @DisplayName("특정 인증 시간에 해당하는 방 사용자들의 토큰값이 없을 때, - Void") + @MethodSource("com.moabam.support.fixture.ParticipantFixture#provideParticipants") + @ParameterizedTest + void notificationService_sendCertificationTimeNotification_NoFirebaseMessaging(List participants) { + // Given + given(participantSearchRepository.findAllByRoomCertifyTime(any(Integer.class))).willReturn(participants); + given(notificationRepository.findFcmTokenByMemberId(any(Long.class))).willReturn(null); + + // When + notificationService.sendCertificationTimeNotification(); + + // Then + verify(firebaseMessaging, times(0)).sendAsync(any(Message.class)); + } } diff --git a/src/test/java/com/moabam/api/application/OAuth2AuthorizationServerRequestServiceTest.java b/src/test/java/com/moabam/api/application/OAuth2AuthorizationServerRequestServiceTest.java index fc011ffa..c54b93f5 100644 --- a/src/test/java/com/moabam/api/application/OAuth2AuthorizationServerRequestServiceTest.java +++ b/src/test/java/com/moabam/api/application/OAuth2AuthorizationServerRequestServiceTest.java @@ -31,7 +31,7 @@ import com.moabam.api.dto.AuthorizationTokenInfoResponse; import com.moabam.api.dto.AuthorizationTokenResponse; -import com.moabam.global.common.constant.GlobalConstant; +import com.moabam.global.common.util.GlobalConstant; import com.moabam.global.error.exception.BadRequestException; import com.moabam.global.error.model.ErrorMessage; diff --git a/src/test/java/com/moabam/api/domain/repository/ParticipantSearchRepositoryTest.java b/src/test/java/com/moabam/api/domain/repository/ParticipantSearchRepositoryTest.java new file mode 100644 index 00000000..5ac95f4a --- /dev/null +++ b/src/test/java/com/moabam/api/domain/repository/ParticipantSearchRepositoryTest.java @@ -0,0 +1,45 @@ +package com.moabam.api.domain.repository; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import com.moabam.api.domain.entity.Participant; +import com.moabam.api.domain.entity.Room; +import com.moabam.global.config.JpaConfig; + +@DataJpaTest +@Import({JpaConfig.class, ParticipantSearchRepository.class}) +class ParticipantSearchRepositoryTest { + + @Autowired + private ParticipantSearchRepository participantSearchRepository; + + @Autowired + private ParticipantRepository participantRepository; + + @Autowired + private RoomRepository roomRepository; + + @DisplayName("인증 시간에 따른 참여자 조회를 성공적으로 했을 때, - List") + @MethodSource("com.moabam.support.fixture.ParticipantFixture#provideRoomAndParticipants") + @ParameterizedTest + void participantSearchRepository_findAllByRoomCertifyTime(Room room, List participants) { + // Given + roomRepository.save(room); + participantRepository.saveAll(participants); + + // When + List actual = participantSearchRepository.findAllByRoomCertifyTime(10); + + // Then + assertThat(actual).hasSize(3); + } +} diff --git a/src/test/java/com/moabam/api/presentation/MemberControllerTest.java b/src/test/java/com/moabam/api/presentation/MemberControllerTest.java index 32cef2d0..1c92c1a5 100644 --- a/src/test/java/com/moabam/api/presentation/MemberControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/MemberControllerTest.java @@ -37,7 +37,7 @@ import com.moabam.api.dto.AuthorizationCodeResponse; import com.moabam.api.dto.AuthorizationTokenInfoResponse; import com.moabam.api.dto.AuthorizationTokenResponse; -import com.moabam.global.common.constant.GlobalConstant; +import com.moabam.global.common.util.GlobalConstant; import com.moabam.global.config.OAuthConfig; import com.moabam.global.error.handler.RestTemplateResponseHandler; import com.moabam.support.fixture.AuthorizationResponseFixture; diff --git a/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java b/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java index 7083f1c2..3edbab12 100644 --- a/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java +++ b/src/test/java/com/moabam/api/presentation/NotificationControllerTest.java @@ -1,6 +1,6 @@ package com.moabam.api.presentation; -import static com.moabam.global.common.constant.FcmConstant.*; +import static com.moabam.global.common.util.GlobalConstant.*; import static org.mockito.BDDMockito.*; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; @@ -27,9 +27,9 @@ import com.moabam.api.domain.repository.MemberRepository; import com.moabam.api.domain.repository.NotificationRepository; import com.moabam.api.domain.repository.RoomRepository; -import com.moabam.fixture.RoomFixture; import com.moabam.global.common.repository.StringRedisRepository; import com.moabam.support.fixture.MemberFixture; +import com.moabam.support.fixture.RoomFixture; @Transactional @SpringBootTest diff --git a/src/test/java/com/moabam/support/fixture/ParticipantFixture.java b/src/test/java/com/moabam/support/fixture/ParticipantFixture.java new file mode 100644 index 00000000..c4b2c5c3 --- /dev/null +++ b/src/test/java/com/moabam/support/fixture/ParticipantFixture.java @@ -0,0 +1,42 @@ +package com.moabam.support.fixture; + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.params.provider.Arguments; + +import com.moabam.api.domain.entity.Participant; +import com.moabam.api.domain.entity.Room; + +public final class ParticipantFixture { + + public static Participant participant(Room room, Long memberId) { + return Participant.builder() + .room(room) + .memberId(memberId) + .build(); + } + + public static Stream provideParticipants() { + Room room = RoomFixture.room(10); + + return Stream.of(Arguments.of(List.of( + ParticipantFixture.participant(room, 1L), + ParticipantFixture.participant(room, 2L), + ParticipantFixture.participant(room, 3L) + ))); + } + + public static Stream provideRoomAndParticipants() { + Room room = RoomFixture.room(10); + + return Stream.of(Arguments.of( + room, + List.of( + ParticipantFixture.participant(room, 1L), + ParticipantFixture.participant(room, 2L), + ParticipantFixture.participant(room, 3L) + )) + ); + } +} diff --git a/src/test/java/com/moabam/fixture/RoomFixture.java b/src/test/java/com/moabam/support/fixture/RoomFixture.java similarity index 50% rename from src/test/java/com/moabam/fixture/RoomFixture.java rename to src/test/java/com/moabam/support/fixture/RoomFixture.java index 7a7af4a3..b65518b4 100644 --- a/src/test/java/com/moabam/fixture/RoomFixture.java +++ b/src/test/java/com/moabam/support/fixture/RoomFixture.java @@ -1,9 +1,9 @@ -package com.moabam.fixture; +package com.moabam.support.fixture; import com.moabam.api.domain.entity.Room; import com.moabam.api.domain.entity.enums.RoomType; -public class RoomFixture { +public final class RoomFixture { public static Room room() { return Room.builder() @@ -13,4 +13,13 @@ public static Room room() { .maxUserCount(8) .build(); } + + public static Room room(int certifyTime) { + return Room.builder() + .title("testTitle") + .roomType(RoomType.MORNING) + .certifyTime(certifyTime) + .maxUserCount(8) + .build(); + } }