Skip to content

Commit

Permalink
feat: 인증 타임에 따른 알림 기능 구현 (#50)
Browse files Browse the repository at this point in the history
* feat: 인증 타임에 따른 주기적 알림 기능 도입

* test: 인증타임에 따른 주기적 알림 기능 테스트

* test: Restdoc 파일

* refactor: 코드 리뷰 반영

* refactor: 코드 리뷰 반영

* fix: checkstyle 수정

* refactor: 코드 리뷰 반영

* refactor: 리뷰 반영
  • Loading branch information
hongdosan authored Nov 7, 2023
1 parent 316dafd commit 35bac08
Show file tree
Hide file tree
Showing 19 changed files with 227 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 33 additions & 5 deletions src/main/java/com/moabam/api/application/NotificationService.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Participant> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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와 상의 후 삭제예정
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,12 @@ public List<Participant> findParticipants(Long roomId) {
)
.fetch();
}

public List<Participant> findAllByRoomCertifyTime(int certifyTime) {
return jpaQueryFactory
.selectFrom(participant)
.join(participant.room, room).fetchJoin()
.where(participant.room.certifyTime.eq(certifyTime))
.fetch();
}
}
12 changes: 10 additions & 2 deletions src/main/java/com/moabam/api/dto/NotificationMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.moabam.global.common.constant;
package com.moabam.global.common.util;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
Expand All @@ -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";
}
2 changes: 1 addition & 1 deletion src/main/java/com/moabam/global/config/FcmConfig.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/config
2 changes: 1 addition & 1 deletion src/main/resources/static/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ <h4 id="_상태코드httpstatus"><a class="link" href="#_상태코드httpstatus"
<div id="footer">
<div id="footer-text">
Version 0.0.1-SNAPSHOT<br>
Last updated 2023-11-07 17:14:47 +0900
Last updated 2023-11-06 23:30:09 +0900
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/highlight.min.js"></script>
Expand Down
4 changes: 2 additions & 2 deletions src/main/resources/static/docs/notification.html
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ <h4 id="_응답" class="discrete">응답</h4>
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Content-Length: 66
Content-Length: 64

{
"message" : "이미 콕 알림을 보낸 대상입니다."
Expand All @@ -487,7 +487,7 @@ <h4 id="_응답" class="discrete">응답</h4>
<div id="footer">
<div id="footer-text">
Version 0.0.1-SNAPSHOT<br>
Last updated 2023-11-07 17:14:47 +0900
Last updated 2023-11-06 23:30:09 +0900
</div>
</div>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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");
Expand All @@ -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);

Expand All @@ -71,11 +96,42 @@ 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
assertThatThrownBy(() -> notificationService.sendKnockNotification(memberTest, 1L, 1L))
.isInstanceOf(ConflictException.class)
.hasMessage(ErrorMessage.CONFLICT_KNOCK.getMessage());
}

@DisplayName("특정 인증 시간에 해당하는 방 사용자들에게 알림을 성공적으로 보낼 때, - Void")
@MethodSource("com.moabam.support.fixture.ParticipantFixture#provideParticipants")
@ParameterizedTest
void notificationService_sendCertificationTimeNotification(List<Participant> 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<Participant> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Participant>")
@MethodSource("com.moabam.support.fixture.ParticipantFixture#provideRoomAndParticipants")
@ParameterizedTest
void participantSearchRepository_findAllByRoomCertifyTime(Room room, List<Participant> participants) {
// Given
roomRepository.save(room);
participantRepository.saveAll(participants);

// When
List<Participant> actual = participantSearchRepository.findAllByRoomCertifyTime(10);

// Then
assertThat(actual).hasSize(3);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 35bac08

Please sign in to comment.