Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

M3-286 FCM 토큰 관리기능 서버 구현 #60

Merged
merged 11 commits into from
Aug 8, 2024
Merged
13 changes: 13 additions & 0 deletions src/main/java/com/m3pro/groundflip/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -13,9 +14,11 @@
import org.springframework.web.multipart.MultipartFile;

import com.m3pro.groundflip.domain.dto.Response;
import com.m3pro.groundflip.domain.dto.user.FcmTokenRequest;
import com.m3pro.groundflip.domain.dto.user.UserDeleteRequest;
import com.m3pro.groundflip.domain.dto.user.UserInfoRequest;
import com.m3pro.groundflip.domain.dto.user.UserInfoResponse;
import com.m3pro.groundflip.service.FcmService;
import com.m3pro.groundflip.service.UserService;

import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -31,6 +34,7 @@
@SecurityRequirement(name = "Authorization")
public class UserController {
private final UserService userService;
private final FcmService fcmService;

@Operation(summary = "사용자 기본 정보 조회", description = "닉네임, id, 출생년도, 성별, 프로필 사진, 그룹이름, 그룹 id 를 조회 한다.")
@GetMapping("/{userId}")
Expand Down Expand Up @@ -62,4 +66,13 @@ public Response<?> putUserInfo(
userService.deleteUser(userId, userDeleteRequest);
return Response.createSuccessWithNoData();
}

@Operation(summary = "FCM 등록 토큰 등록", description = "푸시 알림을 위한 FCM 등록 토큰을 저장한다.")
@PostMapping("/fcm-token")
public Response<?> postFcmToken(
@RequestBody FcmTokenRequest fcmTokenRequest
) {
fcmService.registerFcmToken(fcmTokenRequest);
return Response.createSuccessWithNoData();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.m3pro.groundflip.domain.dto.user;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Schema(title = "FCM 등록 토큰 저장")
public class FcmTokenRequest {
@Schema(description = "사용자 Id", example = "125")
private Long userId;

@Schema(description = "사용자 fcm token", example = "sdfghweredasdvasdfq/weqwefs;dvsdghrthwdffevdrer")
private String fcmToken;
}
37 changes: 37 additions & 0 deletions src/main/java/com/m3pro/groundflip/domain/entity/FcmToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.m3pro.groundflip.domain.entity;

import com.m3pro.groundflip.domain.entity.global.BaseTimeEntity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(name = "fcm_token")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class FcmToken extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "fcm_token_id")
private Long id;

private String token;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ public class BaseTimeEntity {
@LastModifiedDate
private LocalDateTime modifiedAt;

public void updateModifiedAt() {
public void updateModifiedAtToNow() {
modifiedAt = LocalDateTime.now();
}

public void updateModifiedAt(LocalDateTime localDateTime) {
modifiedAt = localDateTime;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.m3pro.groundflip.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.m3pro.groundflip.domain.entity.FcmToken;
import com.m3pro.groundflip.domain.entity.User;

public interface FcmTokenRepository extends JpaRepository<FcmToken, Long> {
Optional<FcmToken> findByUser(User user);

void deleteByUser(User user);
}
43 changes: 43 additions & 0 deletions src/main/java/com/m3pro/groundflip/service/FcmService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.m3pro.groundflip.service;

import java.util.Optional;

import org.springframework.stereotype.Service;

import com.m3pro.groundflip.domain.dto.user.FcmTokenRequest;
import com.m3pro.groundflip.domain.entity.FcmToken;
import com.m3pro.groundflip.domain.entity.User;
import com.m3pro.groundflip.exception.AppException;
import com.m3pro.groundflip.exception.ErrorCode;
import com.m3pro.groundflip.repository.FcmTokenRepository;
import com.m3pro.groundflip.repository.UserRepository;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
public class FcmService {
private final UserRepository userRepository;
private final FcmTokenRepository fcmTokenRepository;

@Transactional
public void registerFcmToken(FcmTokenRequest fcmTokenRequest) {
Long userId = fcmTokenRequest.getUserId();
User user = userRepository.findById(userId).orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND));
Optional<FcmToken> fcmToken = fcmTokenRepository.findByUser(user);

if (fcmToken.isPresent()) {
fcmToken.get().updateModifiedAtToNow();
} else {
fcmTokenRepository.save(
FcmToken.builder()
.user(user)
.token(fcmTokenRequest.getFcmToken())
.build()
);
}
}
}
30 changes: 2 additions & 28 deletions src/main/java/com/m3pro/groundflip/service/PixelService.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ private void occupyPixel(PixelOccupyRequest pixelOccupyRequest) {

Pixel targetPixel = pixelRepository.findByXAndY(pixelOccupyRequest.getX(), pixelOccupyRequest.getY())
.orElseThrow(() -> new AppException(ErrorCode.PIXEL_NOT_FOUND));
updateRankingOnCache(targetPixel, occupyingUserId);
rankingService.updateRanking(targetPixel, occupyingUserId);
updatePixelOwner(targetPixel, occupyingUserId);

updatePixelAddress(targetPixel);
Expand All @@ -159,7 +159,7 @@ private void occupyPixel(PixelOccupyRequest pixelOccupyRequest) {

private void updatePixelOwner(Pixel targetPixel, Long occupyingUserId) {
if (Objects.equals(targetPixel.getUserId(), occupyingUserId)) {
targetPixel.updateModifiedAt();
targetPixel.updateModifiedAtToNow();
} else {
targetPixel.updateUserId(occupyingUserId);
}
Expand All @@ -178,32 +178,6 @@ private void updatePixelAddress(Pixel targetPixel) {
}
}

/**
* 레디스 상에서 랭킹을 조정한다.
* @param targetPixel 랭킹을 조정할 픽셀
* @param occupyingUserId 현재 픽셀을 방문한 유저
* @return
* @author 김민욱
*/
private void updateRankingOnCache(Pixel targetPixel, Long occupyingUserId) {
Long originalOwnerUserId = targetPixel.getUserId();
LocalDateTime thisWeekStart = DateUtils.getThisWeekStartDate().atTime(0, 0);
LocalDateTime modifiedAt = targetPixel.getModifiedAt();

if (Objects.equals(originalOwnerUserId, occupyingUserId)) {
if (modifiedAt.isAfter(thisWeekStart)) {
return;
}
rankingService.increaseCurrentPixelCount(occupyingUserId);
} else {
if (originalOwnerUserId == null || modifiedAt.isBefore(thisWeekStart)) {
rankingService.increaseCurrentPixelCount(occupyingUserId);
} else {
rankingService.updateRankingAfterOccupy(occupyingUserId, originalOwnerUserId);
}
}
}

/**
* 개인 기록 모드에서 픽셀 방문 기록을 가져온다
* @param pixelId 기록을 조회할 픽셀
Expand Down
36 changes: 21 additions & 15 deletions src/main/java/com/m3pro/groundflip/service/RankingService.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.m3pro.groundflip.service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -11,6 +13,7 @@

import com.m3pro.groundflip.domain.dto.ranking.Ranking;
import com.m3pro.groundflip.domain.dto.ranking.UserRankingResponse;
import com.m3pro.groundflip.domain.entity.Pixel;
import com.m3pro.groundflip.domain.entity.RankingHistory;
import com.m3pro.groundflip.domain.entity.User;
import com.m3pro.groundflip.exception.AppException;
Expand All @@ -31,20 +34,23 @@ public class RankingService {
private final UserRepository userRepository;
private final RankingHistoryRepository rankingHistoryRepository;

/**
* 현재 픽셀의 수를 1 증가 시킨다.
* @param userId 사용자 id
*/
public void increaseCurrentPixelCount(Long userId) {
rankingRedisRepository.increaseCurrentPixelCount(userId);
}
public void updateRanking(Pixel targetPixel, Long occupyingUserId) {
Long originalOwnerUserId = targetPixel.getUserId();
LocalDateTime thisWeekStart = DateUtils.getThisWeekStartDate().atTime(0, 0);
LocalDateTime modifiedAt = targetPixel.getModifiedAt();

/**
* 현재 픽셀의 수를 1 감소 시킨다.
* @param userId 사용자 id
*/
public void decreaseCurrentPixelCount(Long userId) {
rankingRedisRepository.decreaseCurrentPixelCount(userId);
if (Objects.equals(originalOwnerUserId, occupyingUserId)) {
if (modifiedAt.isAfter(thisWeekStart)) {
return;
}
rankingRedisRepository.increaseCurrentPixelCount(occupyingUserId);
} else {
if (originalOwnerUserId == null || modifiedAt.isBefore(thisWeekStart)) {
rankingRedisRepository.increaseCurrentPixelCount(occupyingUserId);
} else {
updateRankingAfterOccupy(occupyingUserId, originalOwnerUserId);
}
}
}

/**
Expand All @@ -53,8 +59,8 @@ public void decreaseCurrentPixelCount(Long userId) {
* @param deprivedUserId 픽셀을 뺴앗긴 유저
*/
public void updateRankingAfterOccupy(Long occupyingUserId, Long deprivedUserId) {
increaseCurrentPixelCount(occupyingUserId);
decreaseCurrentPixelCount(deprivedUserId);
rankingRedisRepository.increaseCurrentPixelCount(occupyingUserId);
rankingRedisRepository.decreaseCurrentPixelCount(deprivedUserId);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/m3pro/groundflip/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.m3pro.groundflip.exception.ErrorCode;
import com.m3pro.groundflip.jwt.JwtProvider;
import com.m3pro.groundflip.repository.AppleRefreshTokenRepository;
import com.m3pro.groundflip.repository.FcmTokenRepository;
import com.m3pro.groundflip.repository.RankingRedisRepository;
import com.m3pro.groundflip.repository.UserCommunityRepository;
import com.m3pro.groundflip.repository.UserRepository;
Expand All @@ -39,6 +40,7 @@ public class UserService {
private final UserRepository userRepository;
private final AppleRefreshTokenRepository appleRefreshTokenRepository;
private final UserCommunityRepository userCommunityRepository;
private final FcmTokenRepository fcmTokenRepository;
private final S3Uploader s3Uploader;
private final JwtProvider jwtProvider;
private final AppleApiClient appleApiClient;
Expand Down Expand Up @@ -122,6 +124,8 @@ public void deleteUser(Long userId, UserDeleteRequest userDeleteRequest) {
if (deletedUser.getProvider() == Provider.APPLE) {
revokeAppleToken(deletedUser.getId());
}
fcmTokenRepository.deleteByUser(deletedUser);

deletedUser.updateBirthYear(convertToDate(1900));
deletedUser.updateNickName(null);
deletedUser.updateProfileImage(null);
Expand Down
75 changes: 75 additions & 0 deletions src/test/java/com/m3pro/groundflip/service/FcmServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.m3pro.groundflip.service;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.util.Optional;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.m3pro.groundflip.domain.dto.user.FcmTokenRequest;
import com.m3pro.groundflip.domain.entity.FcmToken;
import com.m3pro.groundflip.domain.entity.User;
import com.m3pro.groundflip.exception.AppException;
import com.m3pro.groundflip.exception.ErrorCode;
import com.m3pro.groundflip.repository.FcmTokenRepository;
import com.m3pro.groundflip.repository.UserRepository;

@ExtendWith(MockitoExtension.class)
class FcmServiceTest {
private static final Long testUserId = 1L;
private static final String testFcmToken = "test token";
private static FcmTokenRequest fcmTokenRequest;
@Mock
private UserRepository userRepository;
@Mock
private FcmTokenRepository fcmTokenRepository;
@InjectMocks
private FcmService fcmService;

@BeforeAll
static void beforeAll() {
fcmTokenRequest = new FcmTokenRequest(testUserId, testFcmToken);
}

@Test
@DisplayName("[registerFcmToken] user 가 없는 경우 에러 발생")
void registerFcmToken_UserNotFound() {
when(userRepository.findById(testUserId)).thenReturn(Optional.empty());

AppException exception = assertThrows(AppException.class, () -> fcmService.registerFcmToken(fcmTokenRequest));
assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.USER_NOT_FOUND);
}

@Test
@DisplayName("[registerFcmToken] fcm 토큰 새로 등록")
void registerFcmToken_RegisterNewToken() {
User user = User.builder().id(1L).email("test@test.com").build();
when(userRepository.findById(testUserId)).thenReturn(Optional.of(user));
when(fcmTokenRepository.findByUser(user)).thenReturn(Optional.empty());

fcmService.registerFcmToken(fcmTokenRequest);

verify(fcmTokenRepository, times(1)).save(any());
}

@Test
@DisplayName("[registerFcmToken] fcm 토큰이 이미 등록된 경우 수정 날짜만 변경")
void registerFcmToken_UpdateModifiedDate() {
User user = User.builder().id(1L).email("test@test.com").build();
FcmToken fcmToken = FcmToken.builder().id(1L).token(testFcmToken).user(user).build();
when(userRepository.findById(testUserId)).thenReturn(Optional.of(user));
when(fcmTokenRepository.findByUser(user)).thenReturn(Optional.of(fcmToken));

fcmService.registerFcmToken(fcmTokenRequest);

assertThat(fcmToken.getModifiedAt()).isNotNull();
}
}
Loading
Loading