From 4ff971863cc4526b44058711902012e50733930d Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Mon, 7 Jul 2025 18:00:18 +0900 Subject: [PATCH 01/15] :sparkles: feat : add update notification status method --- .../java/org/runimo/runimo/user/domain/UserDeviceToken.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java b/src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java index fbdc19b..f4a94f5 100644 --- a/src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java +++ b/src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java @@ -57,4 +57,8 @@ public static UserDeviceToken from(String deviceToken, DevicePlatform platform, .build(); } + public void updateNotificationAllowed(Boolean allowed) { + this.notificationAllowed = allowed; + } + } From ce47f17d23b5592a77277b93e424685f95770482 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Mon, 7 Jul 2025 18:00:27 +0900 Subject: [PATCH 02/15] :sparkles: feat : add response codes --- .../org/runimo/runimo/user/enums/UserHttpResponseCode.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java index 157ac11..facff44 100644 --- a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java +++ b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java @@ -14,6 +14,7 @@ public enum UserHttpResponseCode implements CustomResponseCode { REGISTER_EGG_SUCCESS(HttpStatus.CREATED, "부화기 등록 성공", "부화기 등록 성공"), USE_LOVE_POINT_SUCCESS(HttpStatus.OK, "애정 사용 성공", "애정 사용 성공"), MY_INCUBATING_EGG_FETCHED(HttpStatus.OK, "부화기중인 알 조회 성공", "부화중인 알 조회 성공"), + NOTIFICATION_ALLOW_UPDATED(HttpStatus.OK, "알림 허용 업데이트 성공", "알림 허용 업데이트 성공"), LOGIN_FAIL_NOT_SIGN_IN(HttpStatus.NOT_FOUND , "로그인 실패 - 회원가입하지 않은 사용자", "로그인 실패 - 회원가입하지 않은 사용자"), @@ -27,7 +28,8 @@ public enum UserHttpResponseCode implements CustomResponseCode { "사용자가 유효하지 않습니다. Refresh 토큰 삭제에 실패했습니다"), LOG_OUT_SUCCESS(HttpStatus.OK, "로그아웃 성공", "로그아웃 성공"), ALREADY_LOG_OUT_SUCCESS(HttpStatus.OK, "로그아웃 성공 (이미 로그아웃된 사용자)", "로그아웃 성공 (이미 로그아웃된 사용자)"), - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없음", "사용자를 찾을 수 없음"); + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없음", "사용자를 찾을 수 없음"), + DEVICE_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "디바이스 토큰이 없음", "디바이스 토큰이 없음"); private final HttpStatus code; private final String clientMessage; From 74f51e673820c4bb3e6cd46c0a4c39ba226ddfce Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Mon, 7 Jul 2025 18:01:02 +0900 Subject: [PATCH 03/15] :sparkles: feat : implement updating notification allowed logic --- .../user/controller/MyPageController.java | 15 ++++++++++++ .../UpdateNotificationAllowedRequst.java | 7 ++++++ .../repository/UserDeviceTokenRepository.java | 2 ++ .../usecases/UpdateUserDetailUsecase.java | 6 +++++ .../usecases/UpdateUserDetailUsecaseImpl.java | 23 +++++++++++++++++++ 5 files changed, 53 insertions(+) create mode 100644 src/main/java/org/runimo/runimo/user/controller/request/UpdateNotificationAllowedRequst.java create mode 100644 src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecase.java create mode 100644 src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java diff --git a/src/main/java/org/runimo/runimo/user/controller/MyPageController.java b/src/main/java/org/runimo/runimo/user/controller/MyPageController.java index ddc878a..21525dd 100644 --- a/src/main/java/org/runimo/runimo/user/controller/MyPageController.java +++ b/src/main/java/org/runimo/runimo/user/controller/MyPageController.java @@ -2,11 +2,15 @@ import lombok.RequiredArgsConstructor; import org.runimo.runimo.common.response.SuccessResponse; +import org.runimo.runimo.user.controller.request.UpdateNotificationAllowedRequst; import org.runimo.runimo.user.enums.UserHttpResponseCode; import org.runimo.runimo.user.service.dto.response.MyPageViewResponse; +import org.runimo.runimo.user.service.usecases.UpdateUserDetailUsecase; import org.runimo.runimo.user.service.usecases.query.MyPageQueryUsecase; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -16,6 +20,7 @@ public class MyPageController { private final MyPageQueryUsecase myPageQueryUsecase; + private final UpdateUserDetailUsecase updateUserDetailUsecase; @GetMapping public ResponseEntity> queryMyPageView( @@ -25,4 +30,14 @@ public ResponseEntity> queryMyPageView( SuccessResponse.of(UserHttpResponseCode.MY_PAGE_DATA_FETCHED, response)); } + @PatchMapping("/notifications/permission") + public ResponseEntity> updateNotificationPermission( + @UserId Long userId, + @RequestBody UpdateNotificationAllowedRequst request + ) { + updateUserDetailUsecase.updateUserNotificationAllowed(userId, request.allowed()); + return ResponseEntity.ok().body( + SuccessResponse.messageOnly(UserHttpResponseCode.NOTIFICATION_ALLOW_UPDATED)); + } + } diff --git a/src/main/java/org/runimo/runimo/user/controller/request/UpdateNotificationAllowedRequst.java b/src/main/java/org/runimo/runimo/user/controller/request/UpdateNotificationAllowedRequst.java new file mode 100644 index 0000000..a92f9c5 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/controller/request/UpdateNotificationAllowedRequst.java @@ -0,0 +1,7 @@ +package org.runimo.runimo.user.controller.request; + +public record UpdateNotificationAllowedRequst( + boolean allowed +) { + +} diff --git a/src/main/java/org/runimo/runimo/user/repository/UserDeviceTokenRepository.java b/src/main/java/org/runimo/runimo/user/repository/UserDeviceTokenRepository.java index e10ff44..cb75746 100644 --- a/src/main/java/org/runimo/runimo/user/repository/UserDeviceTokenRepository.java +++ b/src/main/java/org/runimo/runimo/user/repository/UserDeviceTokenRepository.java @@ -1,5 +1,6 @@ package org.runimo.runimo.user.repository; +import java.util.Optional; import org.runimo.runimo.user.domain.UserDeviceToken; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -7,4 +8,5 @@ @Repository public interface UserDeviceTokenRepository extends JpaRepository { + Optional findByUserId(Long userId); } \ No newline at end of file diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecase.java new file mode 100644 index 0000000..5a7b067 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecase.java @@ -0,0 +1,6 @@ +package org.runimo.runimo.user.service.usecases; + +public interface UpdateUserDetailUsecase { + + void updateUserNotificationAllowed(Long userId, boolean allowed); +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java new file mode 100644 index 0000000..bcf1cd3 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java @@ -0,0 +1,23 @@ +package org.runimo.runimo.user.service.usecases; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.domain.UserDeviceToken; +import org.runimo.runimo.user.enums.UserHttpResponseCode; +import org.runimo.runimo.user.exception.UserException; +import org.runimo.runimo.user.repository.UserDeviceTokenRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UpdateUserDetailUsecaseImpl implements UpdateUserDetailUsecase { + + private final UserDeviceTokenRepository userDeviceTokenRepository; + + @Override + public void updateUserNotificationAllowed(Long userId, boolean allowed) { + UserDeviceToken userDeviceToken = userDeviceTokenRepository.findByUserId(userId) + .orElseThrow(() -> UserException.of(UserHttpResponseCode.USER_NOT_FOUND)); + userDeviceToken.updateNotificationAllowed(allowed); + userDeviceTokenRepository.save(userDeviceToken); + } +} From a4c309c9090bd26fa23d0b3746c7a17fd4193201 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Mon, 7 Jul 2025 18:16:00 +0900 Subject: [PATCH 04/15] :sparkles: feat : add fields to update request --- .../request/UpdateNotificationAllowedRequst.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/runimo/runimo/user/controller/request/UpdateNotificationAllowedRequst.java b/src/main/java/org/runimo/runimo/user/controller/request/UpdateNotificationAllowedRequst.java index a92f9c5..9d882b8 100644 --- a/src/main/java/org/runimo/runimo/user/controller/request/UpdateNotificationAllowedRequst.java +++ b/src/main/java/org/runimo/runimo/user/controller/request/UpdateNotificationAllowedRequst.java @@ -1,7 +1,13 @@ package org.runimo.runimo.user.controller.request; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "알림 허용 여부 업데이트 요청") public record UpdateNotificationAllowedRequst( - boolean allowed + @Schema(description = "알림 허용 여부", example = "true") + boolean allowed, + String deviceToken, + String platform ) { } From 7505f7075ee0799cfdae138d2f32026743e86be1 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Mon, 7 Jul 2025 18:26:38 +0900 Subject: [PATCH 05/15] :sparkles: feat : implement upsert logic for device token --- .../usecases/UpdateUserDetailUsecase.java | 4 ++- .../usecases/UpdateUserDetailUsecaseImpl.java | 27 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecase.java index 5a7b067..879ce6d 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecase.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecase.java @@ -1,6 +1,8 @@ package org.runimo.runimo.user.service.usecases; +import org.runimo.runimo.user.service.dto.command.UpdateNotificationAllowedCommand; + public interface UpdateUserDetailUsecase { - void updateUserNotificationAllowed(Long userId, boolean allowed); + void updateUserNotificationAllowed(UpdateNotificationAllowedCommand command); } diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java index bcf1cd3..4d800fe 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java @@ -1,23 +1,42 @@ package org.runimo.runimo.user.service.usecases; import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.domain.User; import org.runimo.runimo.user.domain.UserDeviceToken; import org.runimo.runimo.user.enums.UserHttpResponseCode; import org.runimo.runimo.user.exception.UserException; import org.runimo.runimo.user.repository.UserDeviceTokenRepository; +import org.runimo.runimo.user.service.UserFinder; +import org.runimo.runimo.user.service.dto.command.UpdateNotificationAllowedCommand; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class UpdateUserDetailUsecaseImpl implements UpdateUserDetailUsecase { + private final UserFinder userFinder; private final UserDeviceTokenRepository userDeviceTokenRepository; @Override - public void updateUserNotificationAllowed(Long userId, boolean allowed) { - UserDeviceToken userDeviceToken = userDeviceTokenRepository.findByUserId(userId) + @Transactional + public void updateUserNotificationAllowed(UpdateNotificationAllowedCommand command) { + UserDeviceToken userDeviceToken = userFinder.findUserDeviceTokenByUserId(command.userId()) + .orElse(createUserDeviceTokenIfNotExist(command)); + userDeviceToken.updateNotificationAllowed(command.allowed()); + } + + private UserDeviceToken createUserDeviceTokenIfNotExist( + UpdateNotificationAllowedCommand command) { + User user = userFinder.findUserById(command.userId()) .orElseThrow(() -> UserException.of(UserHttpResponseCode.USER_NOT_FOUND)); - userDeviceToken.updateNotificationAllowed(allowed); - userDeviceTokenRepository.save(userDeviceToken); + + UserDeviceToken token = UserDeviceToken.builder() + .user(user) + .deviceToken(command.deviceToken()) + .platform(command.devicePlatform()) + .notificationAllowed(command.allowed()) + .build(); + return userDeviceTokenRepository.save(token); } } From b0e0a4dae79d9cacbe63cac4217502596395ff6c Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Mon, 7 Jul 2025 18:26:57 +0900 Subject: [PATCH 06/15] :sparkles: feat : add querying user notification status --- .../runimo/user/service/UserFinder.java | 8 ++++++ .../usecases/query/UserInfoQueryUsecase.java | 9 +++++++ .../query/UserInfoQueryUsecaseImpl.java | 25 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/main/java/org/runimo/runimo/user/service/usecases/query/UserInfoQueryUsecase.java create mode 100644 src/main/java/org/runimo/runimo/user/service/usecases/query/UserInfoQueryUsecaseImpl.java diff --git a/src/main/java/org/runimo/runimo/user/service/UserFinder.java b/src/main/java/org/runimo/runimo/user/service/UserFinder.java index b3e6a9a..ca49cf0 100644 --- a/src/main/java/org/runimo/runimo/user/service/UserFinder.java +++ b/src/main/java/org/runimo/runimo/user/service/UserFinder.java @@ -4,7 +4,9 @@ import lombok.RequiredArgsConstructor; import org.runimo.runimo.user.domain.LovePoint; import org.runimo.runimo.user.domain.User; +import org.runimo.runimo.user.domain.UserDeviceToken; import org.runimo.runimo.user.repository.LovePointRepository; +import org.runimo.runimo.user.repository.UserDeviceTokenRepository; import org.runimo.runimo.user.repository.UserRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -15,6 +17,7 @@ public class UserFinder { private final UserRepository userRepository; private final LovePointRepository lovePointRepository; + private final UserDeviceTokenRepository userDeviceTokenRepository; @Transactional(readOnly = true) public Optional findUserByPublicId(final String publicId) { @@ -30,4 +33,9 @@ public Optional findUserById(final Long userId) { public Optional findLovePointByUserId(Long userId) { return lovePointRepository.findLovePointByUserId(userId); } + + @Transactional(readOnly = true) + public Optional findUserDeviceTokenByUserId(Long userId) { + return userDeviceTokenRepository.findByUserId(userId); + } } diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/query/UserInfoQueryUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/query/UserInfoQueryUsecase.java new file mode 100644 index 0000000..21ad4c3 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/query/UserInfoQueryUsecase.java @@ -0,0 +1,9 @@ +package org.runimo.runimo.user.service.usecases.query; + +import org.runimo.runimo.user.service.dto.response.NotificationAllowedResponse; + +public interface UserInfoQueryUsecase { + + NotificationAllowedResponse getUserNotificationAllowed(Long userId); + +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/query/UserInfoQueryUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/query/UserInfoQueryUsecaseImpl.java new file mode 100644 index 0000000..082a55c --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/query/UserInfoQueryUsecaseImpl.java @@ -0,0 +1,25 @@ +package org.runimo.runimo.user.service.usecases.query; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.user.enums.UserHttpResponseCode; +import org.runimo.runimo.user.exception.UserException; +import org.runimo.runimo.user.service.UserFinder; +import org.runimo.runimo.user.service.dto.response.NotificationAllowedResponse; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserInfoQueryUsecaseImpl implements UserInfoQueryUsecase { + + private final UserFinder userFinder; + + @Override + public NotificationAllowedResponse getUserNotificationAllowed(Long userId) { + if (userFinder.findUserById(userId).isEmpty()) { + throw UserException.of(UserHttpResponseCode.USER_NOT_FOUND); + } + var userDeviceToken = userFinder.findUserDeviceTokenByUserId(userId) + .orElseThrow(() -> UserException.of(UserHttpResponseCode.DEVICE_TOKEN_NOT_FOUND)); + return new NotificationAllowedResponse(userId, userDeviceToken.getNotificationAllowed()); + } +} From ca72c0a9fc2a0b28f800d593334185c876058823 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Mon, 7 Jul 2025 18:27:14 +0900 Subject: [PATCH 07/15] :sparkles: feat : add update-notification-allowed command --- .../UpdateNotificationAllowedCommand.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/org/runimo/runimo/user/service/dto/command/UpdateNotificationAllowedCommand.java diff --git a/src/main/java/org/runimo/runimo/user/service/dto/command/UpdateNotificationAllowedCommand.java b/src/main/java/org/runimo/runimo/user/service/dto/command/UpdateNotificationAllowedCommand.java new file mode 100644 index 0000000..a9c48a2 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dto/command/UpdateNotificationAllowedCommand.java @@ -0,0 +1,20 @@ +package org.runimo.runimo.user.service.dto.command; + +import org.runimo.runimo.user.controller.request.UpdateNotificationAllowedRequst; +import org.runimo.runimo.user.domain.DevicePlatform; + +public record UpdateNotificationAllowedCommand( + Long userId, + Boolean allowed, + String deviceToken, + DevicePlatform devicePlatform +) { + + public static UpdateNotificationAllowedCommand of(Long userId, + UpdateNotificationAllowedRequst request) { + return new UpdateNotificationAllowedCommand(userId, request.allowed(), + request.deviceToken(), DevicePlatform.fromString(request.platform())); + } + + +} From 57e15914125d00d317329575de607d69aa125d39 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 9 Jul 2025 21:01:13 +0900 Subject: [PATCH 08/15] :sparkles: feat : implement notification-permission update --- .../user/controller/MyPageController.java | 29 ++++++++++++++++++- .../runimo/user/domain/UserDeviceToken.java | 12 ++++++++ .../user/enums/UserHttpResponseCode.java | 3 +- .../response/NotificationAllowedResponse.java | 13 +++++++++ .../usecases/UpdateUserDetailUsecaseImpl.java | 8 +++-- 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/runimo/runimo/user/service/dto/response/NotificationAllowedResponse.java diff --git a/src/main/java/org/runimo/runimo/user/controller/MyPageController.java b/src/main/java/org/runimo/runimo/user/controller/MyPageController.java index 21525dd..7d5d476 100644 --- a/src/main/java/org/runimo/runimo/user/controller/MyPageController.java +++ b/src/main/java/org/runimo/runimo/user/controller/MyPageController.java @@ -1,12 +1,18 @@ package org.runimo.runimo.user.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.RequiredArgsConstructor; import org.runimo.runimo.common.response.SuccessResponse; import org.runimo.runimo.user.controller.request.UpdateNotificationAllowedRequst; import org.runimo.runimo.user.enums.UserHttpResponseCode; +import org.runimo.runimo.user.service.dto.command.UpdateNotificationAllowedCommand; import org.runimo.runimo.user.service.dto.response.MyPageViewResponse; +import org.runimo.runimo.user.service.dto.response.NotificationAllowedResponse; import org.runimo.runimo.user.service.usecases.UpdateUserDetailUsecase; import org.runimo.runimo.user.service.usecases.query.MyPageQueryUsecase; +import org.runimo.runimo.user.service.usecases.query.UserInfoQueryUsecase; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -20,6 +26,7 @@ public class MyPageController { private final MyPageQueryUsecase myPageQueryUsecase; + private final UserInfoQueryUsecase userInfoQueryUsecase; private final UpdateUserDetailUsecase updateUserDetailUsecase; @GetMapping @@ -30,14 +37,34 @@ public ResponseEntity> queryMyPageView( SuccessResponse.of(UserHttpResponseCode.MY_PAGE_DATA_FETCHED, response)); } + @Operation(summary = "회원 알림 수신 허용 여부 수정", description = "알림을 수신 여부 변경") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "수신 여부 수정 성공"), + @ApiResponse(responseCode = "404", description = "없는 사용자") + }) @PatchMapping("/notifications/permission") public ResponseEntity> updateNotificationPermission( @UserId Long userId, @RequestBody UpdateNotificationAllowedRequst request ) { - updateUserDetailUsecase.updateUserNotificationAllowed(userId, request.allowed()); + updateUserDetailUsecase.updateUserNotificationAllowed( + UpdateNotificationAllowedCommand.of(userId, request)); return ResponseEntity.ok().body( SuccessResponse.messageOnly(UserHttpResponseCode.NOTIFICATION_ALLOW_UPDATED)); } + @Operation(summary = "회원 알림 수신 허용 여부 확인", description = "알림을 수신하는지 확인") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "수신 여부 확인"), + @ApiResponse(responseCode = "404", description = "없는 사용자 또는 토큰 미등록") + }) + @GetMapping("/notifications/permission") + public ResponseEntity> queryNotificationPermission( + @UserId Long userId + ) { + return ResponseEntity.ok( + SuccessResponse.of(UserHttpResponseCode.NOTIFICATION_ALLOW_FETCHED, + userInfoQueryUsecase.getUserNotificationAllowed(userId))); + } + } diff --git a/src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java b/src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java index f4a94f5..e2bb871 100644 --- a/src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java +++ b/src/main/java/org/runimo/runimo/user/domain/UserDeviceToken.java @@ -61,4 +61,16 @@ public void updateNotificationAllowed(Boolean allowed) { this.notificationAllowed = allowed; } + public void updateDeviceToken(String deviceToken) { + validateDeviceToken(deviceToken, this.platform); + this.deviceToken = deviceToken; + this.lastUsedAt = LocalDateTime.now(); + } + + private void validateDeviceToken(String deviceToken, DevicePlatform platform) { + if (deviceToken == null || deviceToken.trim().isEmpty()) { + throw new IllegalArgumentException("디바이스 토큰은 필수입니다"); + } + } + } diff --git a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java index facff44..f5fe713 100644 --- a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java +++ b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java @@ -15,6 +15,7 @@ public enum UserHttpResponseCode implements CustomResponseCode { USE_LOVE_POINT_SUCCESS(HttpStatus.OK, "애정 사용 성공", "애정 사용 성공"), MY_INCUBATING_EGG_FETCHED(HttpStatus.OK, "부화기중인 알 조회 성공", "부화중인 알 조회 성공"), NOTIFICATION_ALLOW_UPDATED(HttpStatus.OK, "알림 허용 업데이트 성공", "알림 허용 업데이트 성공"), + NOTIFICATION_ALLOW_FETCHED(HttpStatus.OK, "알림 허용 조회 성공", "알림 허용 조회 성공"), LOGIN_FAIL_NOT_SIGN_IN(HttpStatus.NOT_FOUND , "로그인 실패 - 회원가입하지 않은 사용자", "로그인 실패 - 회원가입하지 않은 사용자"), @@ -29,7 +30,7 @@ public enum UserHttpResponseCode implements CustomResponseCode { LOG_OUT_SUCCESS(HttpStatus.OK, "로그아웃 성공", "로그아웃 성공"), ALREADY_LOG_OUT_SUCCESS(HttpStatus.OK, "로그아웃 성공 (이미 로그아웃된 사용자)", "로그아웃 성공 (이미 로그아웃된 사용자)"), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없음", "사용자를 찾을 수 없음"), - DEVICE_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "디바이스 토큰이 없음", "디바이스 토큰이 없음"); + DEVICE_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "디바이스 토큰이 없음", "디바이스 토큰이 없음"); private final HttpStatus code; private final String clientMessage; diff --git a/src/main/java/org/runimo/runimo/user/service/dto/response/NotificationAllowedResponse.java b/src/main/java/org/runimo/runimo/user/service/dto/response/NotificationAllowedResponse.java new file mode 100644 index 0000000..f5ae67b --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/dto/response/NotificationAllowedResponse.java @@ -0,0 +1,13 @@ +package org.runimo.runimo.user.service.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "알림 허용 여부 응답") +public record NotificationAllowedResponse( + @Schema(description = "사용자 ID", example = "1") + Long userId, + @Schema(description = "알림 허용 여부", example = "true") + boolean allowed +) { + +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java index 4d800fe..6438e85 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImpl.java @@ -21,9 +21,11 @@ public class UpdateUserDetailUsecaseImpl implements UpdateUserDetailUsecase { @Override @Transactional public void updateUserNotificationAllowed(UpdateNotificationAllowedCommand command) { - UserDeviceToken userDeviceToken = userFinder.findUserDeviceTokenByUserId(command.userId()) - .orElse(createUserDeviceTokenIfNotExist(command)); - userDeviceToken.updateNotificationAllowed(command.allowed()); + UserDeviceToken token = userDeviceTokenRepository + .findByUserId(command.userId()) + .orElseGet(() -> createUserDeviceTokenIfNotExist(command)); + token.updateDeviceToken(command.deviceToken()); + token.updateNotificationAllowed(command.allowed()); } private UserDeviceToken createUserDeviceTokenIfNotExist( From af2e5406403ab1a88c43f05ee0873756bc40a443 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 9 Jul 2025 21:10:59 +0900 Subject: [PATCH 09/15] :hammer: chore : add db migration file --- .../V202050709__add_unique_constraint_to_device_token.sql | 2 ++ .../h2/V202050709__add_unique_constraint_to_device_token.sql | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 src/main/resources/db/migration/V202050709__add_unique_constraint_to_device_token.sql create mode 100644 src/test/resources/db/migration/h2/V202050709__add_unique_constraint_to_device_token.sql diff --git a/src/main/resources/db/migration/V202050709__add_unique_constraint_to_device_token.sql b/src/main/resources/db/migration/V202050709__add_unique_constraint_to_device_token.sql new file mode 100644 index 0000000..a6b42aa --- /dev/null +++ b/src/main/resources/db/migration/V202050709__add_unique_constraint_to_device_token.sql @@ -0,0 +1,2 @@ +ALTER TABLE user_token + ADD CONSTRAINT uq_user_token_device_token UNIQUE (device_token); \ No newline at end of file diff --git a/src/test/resources/db/migration/h2/V202050709__add_unique_constraint_to_device_token.sql b/src/test/resources/db/migration/h2/V202050709__add_unique_constraint_to_device_token.sql new file mode 100644 index 0000000..a6b42aa --- /dev/null +++ b/src/test/resources/db/migration/h2/V202050709__add_unique_constraint_to_device_token.sql @@ -0,0 +1,2 @@ +ALTER TABLE user_token + ADD CONSTRAINT uq_user_token_device_token UNIQUE (device_token); \ No newline at end of file From 06032c18a05b8ff537d56c0d533d329203c8a4c2 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 9 Jul 2025 21:11:41 +0900 Subject: [PATCH 10/15] :white_check_mark: test : add test-codes --- .../java/org/runimo/runimo/CleanUpUtil.java | 1 + .../runimo/user/api/MyPageAcceptanceTest.java | 153 ++++++++++++++++++ .../user/controller/MyPageControllerTest.java | 6 + 3 files changed, 160 insertions(+) diff --git a/src/test/java/org/runimo/runimo/CleanUpUtil.java b/src/test/java/org/runimo/runimo/CleanUpUtil.java index 65cab99..78d0e25 100644 --- a/src/test/java/org/runimo/runimo/CleanUpUtil.java +++ b/src/test/java/org/runimo/runimo/CleanUpUtil.java @@ -15,6 +15,7 @@ public class CleanUpUtil { "user_love_point", "incubating_egg", "runimo", + "user_token" }; @Autowired diff --git a/src/test/java/org/runimo/runimo/user/api/MyPageAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/MyPageAcceptanceTest.java index c6bfc37..a1e8641 100644 --- a/src/test/java/org/runimo/runimo/user/api/MyPageAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/MyPageAcceptanceTest.java @@ -73,4 +73,157 @@ void tearDown() { equalTo(6700)); } + @Test + @Sql(scripts = "/sql/user_device_token_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 유저_토큰이_저장된_유저가_알림_허용_상태를_변경하는_경우() { + // given + String requestBody = """ + { + "allowed": false, + "device_token": "existing_device_token_12345", + "platform": "FCM" + } + """; + + // when & then + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + .body(requestBody) + .when() + .patch("/api/v1/users/me/notifications/permission") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .body("code", equalTo("NOTIFICATION_ALLOW_UPDATED")); + + // 변경된 상태 확인 + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + .when() + .get("/api/v1/users/me/notifications/permission") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .body("code", equalTo("NOTIFICATION_ALLOW_FETCHED")) + .body("payload.allowed", equalTo(false)); + } + + @Test + @Sql(scripts = "/sql/user_device_token_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 유저_토큰이_없는_사용자가_알림_허용_요청을_보내는_경우_새로_저장된다() { + // given + String requestBody = """ + { + "allowed": true, + "device_token": "new_device_token_67890", + "platform": "APNS" + } + """; + + // when & then + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + .body(requestBody) + .when() + .patch("/api/v1/users/me/notifications/permission") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .body("code", equalTo("NOTIFICATION_ALLOW_UPDATED")); + + // 생성된 토큰 정보 확인 + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + .when() + .get("/api/v1/users/me/notifications/permission") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .body("code", equalTo("NOTIFICATION_ALLOW_FETCHED")) + .body("payload.allowed", equalTo(true)); + } + + @Test + @Sql(scripts = "/sql/user_device_token_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 유저_토큰이_저장된_유저의_토큰정보가_바뀐_경우_새로운_토큰으로_업데이트된다() { + // given - 기존 토큰: "existing_device_token_12345", FCM, true + // 새로운 토큰으로 변경 요청 + String requestBody = """ + { + "allowed": true, + "device_token": "updated_device_token_99999", + "platform": "APNS" + } + """; + + // when & then + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + .body(requestBody) + .when() + .patch("/api/v1/users/me/notifications/permission") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .body("code", equalTo("NOTIFICATION_ALLOW_UPDATED")); + + // 업데이트된 정보 확인 + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + .when() + .get("/api/v1/users/me/notifications/permission") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .body("code", equalTo("NOTIFICATION_ALLOW_FETCHED")) + .body("payload.allowed", equalTo(true)); + } + + @Test + @Sql(scripts = "/sql/user_device_token_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 잘못된_플랫폼으로_요청하는_경우_400_에러가_발생한다() { + // given + String requestBody = """ + { + "allowed": true, + "device_token": "some_device_token", + "platform": "INVALID_PLATFORM" + } + """; + + // when & then - 현재 DevicePlatform.fromString에서 IllegalArgumentException 발생 + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + .body(requestBody) + .when() + .patch("/api/v1/users/me/notifications/permission") + .then() + .log().all() + .statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @Test + @Sql(scripts = "/sql/user_mypage_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 디바이스_토큰이_없는_사용자가_알림_허용_조회시_404_에러가_발생한다() { + // given - 토큰이 저장되지 않은 상태 + + // when & then + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + .when() + .get("/api/v1/users/me/notifications/permission") + .then() + .log().all() + .statusCode(HttpStatus.NOT_FOUND.value()) + .body("code", equalTo("DEVICE_TOKEN_NOT_FOUND")); + } + } diff --git a/src/test/java/org/runimo/runimo/user/controller/MyPageControllerTest.java b/src/test/java/org/runimo/runimo/user/controller/MyPageControllerTest.java index 6918b50..65ba91b 100644 --- a/src/test/java/org/runimo/runimo/user/controller/MyPageControllerTest.java +++ b/src/test/java/org/runimo/runimo/user/controller/MyPageControllerTest.java @@ -18,7 +18,9 @@ import org.runimo.runimo.configs.ControllerTest; import org.runimo.runimo.user.service.dto.LatestRunningRecord; import org.runimo.runimo.user.service.dto.response.MyPageViewResponse; +import org.runimo.runimo.user.service.usecases.UpdateUserDetailUsecase; import org.runimo.runimo.user.service.usecases.query.MyPageQueryUsecase; +import org.runimo.runimo.user.service.usecases.query.UserInfoQueryUsecase; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithAnonymousUser; @@ -36,6 +38,10 @@ class MyPageControllerTest { @MockitoBean private MyPageQueryUsecase myPageQueryUsecase; @MockitoBean + private UserInfoQueryUsecase userInfoQueryUsecase; + @MockitoBean + private UpdateUserDetailUsecase updateUserDetailUsecase; + @MockitoBean private JwtResolver jwtResolver; @MockitoBean private UserIdResolver userIdResolver; From ed95476419482d112ae76544da5562b51b03b721 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 9 Jul 2025 21:13:48 +0900 Subject: [PATCH 11/15] :hammer: chore : update dev-profile application.yml --- src/main/resources/application.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fa6c959..3ce932a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -117,6 +117,11 @@ spring: dialect: org.hibernate.dialect.MySQLDialect show-sql: true + flyway: + enabled: true + locations: classpath:db/migration/mysql + baseline-on-migrate: true + clean-disabled: false jwt: expiration: ${JWT_EXPIRATION:180000} refresh: From 44360ab4e15bc513390c9375975797dd5cb0e2a8 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 9 Jul 2025 21:26:08 +0900 Subject: [PATCH 12/15] :white_check_mark: test : add test-setup-sql --- .../resources/sql/all_user_notification.sql | 36 +++++++++++++++++++ .../sql/inactive_user_notification.sql | 29 +++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/test/resources/sql/all_user_notification.sql create mode 100644 src/test/resources/sql/inactive_user_notification.sql diff --git a/src/test/resources/sql/all_user_notification.sql b/src/test/resources/sql/all_user_notification.sql new file mode 100644 index 0000000..1e18704 --- /dev/null +++ b/src/test/resources/sql/all_user_notification.sql @@ -0,0 +1,36 @@ +-- notification_all_users_test_data.sql +-- ALL_USERS 조건 테스트를 위한 데이터 +-- 알림 허용된 모든 활성 사용자 대상 + +-- 사용자 데이터 (7명 - 활성 5명, 삭제된 사용자 2명) +INSERT INTO users (id, public_id, nickname, created_at, updated_at, deleted_at) +VALUES (1, 'active-user-1', '활성사용자1', '2025-01-01 00:00:00', '2025-01-01 00:00:00', NULL), + (2, 'active-user-2', '활성사용자2', '2025-01-01 00:00:00', '2025-01-01 00:00:00', NULL), + (3, 'active-user-3', '활성사용자3', '2025-01-01 00:00:00', '2025-01-01 00:00:00', NULL), + (4, 'active-user-4', '활성사용자4', '2025-01-01 00:00:00', '2025-01-01 00:00:00', NULL), + (5, 'active-user-5', '활성사용자5', '2025-01-01 00:00:00', '2025-01-01 00:00:00', NULL), +-- 삭제된 사용자들 (deleted_at이 NULL이 아님) + (6, 'deleted-user-1', '삭제된사용자1', '2025-01-01 00:00:00', '2025-01-01 00:00:00', + '2025-01-02 00:00:00'), + (7, 'deleted-user-2', '삭제된사용자2', '2025-01-01 00:00:00', '2025-01-01 00:00:00', + '2025-01-03 00:00:00'); + +INSERT INTO users (id, public_id, nickname, img_url, total_distance_in_meters, + total_time_in_seconds, created_at, + updated_at, role) +VALUES (11, 'admin-user-uuid-1', 'Daniel', 'https://example.com/images/user1.png', 10000, 3600, + NOW(), + NOW(), 'ADMIN'); + +-- 사용자별 device token +-- 활성 사용자 5명은 모두 알림 허용, 삭제된 사용자는 제외 +INSERT INTO user_token (user_id, device_token, platform, notification_allowed, created_at, + updated_at) +VALUES (1, 'active-device-token-1', 'FCM', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (2, 'active-device-token-2', 'APNS', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (3, 'active-device-token-3', 'FCM', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (4, 'active-device-token-4', 'APNS', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (5, 'active-device-token-5', 'FCM', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'), +-- 삭제된 사용자들의 토큰 (대상에서 제외되어야 함) + (6, 'deleted-device-token-1', 'FCM', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (7, 'deleted-device-token-2', 'APNS', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'); diff --git a/src/test/resources/sql/inactive_user_notification.sql b/src/test/resources/sql/inactive_user_notification.sql new file mode 100644 index 0000000..01d0ff4 --- /dev/null +++ b/src/test/resources/sql/inactive_user_notification.sql @@ -0,0 +1,29 @@ +-- INACTIVE_USERS 조건 테스트를 위한 데이터 +-- 1주일 내 달리기 기록이 없는 사용자 = 비활성 사용자 + +-- 사용자 데이터 (5명) +INSERT INTO users (id, public_id, nickname, created_at, updated_at) +VALUES (1, 'active-user-1', '활성사용자1', '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (2, 'active-user-2', '활성사용자2', '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (3, 'active-user-3', '활성사용자3', '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (4, 'inactive-user-1', '비활성사용자1', '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (5, 'inactive-user-2', '비활성사용자2', '2025-01-01 00:00:00', '2025-01-01 00:00:00'); + +INSERT INTO users (id, public_id, nickname, img_url, total_distance_in_meters, + total_time_in_seconds, created_at, + updated_at, role) +VALUES (11, 'admin-user-uuid-1', 'Daniel', 'https://example.com/images/user1.png', 10000, 3600, + NOW(), + NOW(), 'ADMIN'); + + +-- 사용자별 device token (알림 허용된 사용자들만) +INSERT INTO user_token (user_id, device_token, platform, notification_allowed, created_at, + updated_at) +VALUES (1, 'active-device-token-1', 'FCM', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (2, 'active-device-token-2', 'APNS', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (3, 'active-device-token-3', 'FCM', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (4, 'inactive-device-token-1', 'FCM', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'), + (5, 'inactive-device-token-2', 'APNS', true, '2025-01-01 00:00:00', '2025-01-01 00:00:00'); + + From e84ad0470b290be6ed71de9e7366c59feab96cf1 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 9 Jul 2025 21:33:08 +0900 Subject: [PATCH 13/15] :white_check_mark: test : add device-token test-setup-sql --- .../sql/user_device_token_test_data.sql | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/test/resources/sql/user_device_token_test_data.sql diff --git a/src/test/resources/sql/user_device_token_test_data.sql b/src/test/resources/sql/user_device_token_test_data.sql new file mode 100644 index 0000000..d63963e --- /dev/null +++ b/src/test/resources/sql/user_device_token_test_data.sql @@ -0,0 +1,18 @@ +-- 테스트용 기본 유저 (기존에 있다고 가정) +SET FOREIGN_KEY_CHECKS = 0; +TRUNCATE TABLE users; +INSERT INTO users (id, public_id, nickname, img_url, total_distance_in_meters, + total_time_in_seconds, created_at, + updated_at) +VALUES (1, 'test-user-uuid-1', 'Daniel', 'https://example.com/images/user1.png', 10000, 3600, NOW(), + NOW()); +SET FOREIGN_KEY_CHECKS = 1; + +TRUNCATE TABLE oauth_account; +INSERT INTO oauth_account (id, created_at, deleted_at, updated_at, provider, provider_id, user_id) +VALUES (1, NOW(), null, NOW(), 'KAKAO', 1234, 1); + +-- 기존 디바이스 토큰 데이터 +INSERT INTO user_token (id, user_id, device_token, platform, notification_allowed, created_at, + updated_at) +VALUES (1, 1, 'existing_device_token_12345', 'FCM', true, NOW(), NOW()); From cdb22d6f29e87935181e762be46bdd42bfc8c445 Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 9 Jul 2025 21:56:06 +0900 Subject: [PATCH 14/15] :white_check_mark: test : add user-device-update-usecase --- .../UpdateUserDetailUsecaseImplTest.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/test/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImplTest.java diff --git a/src/test/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImplTest.java b/src/test/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImplTest.java new file mode 100644 index 0000000..d08324f --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImplTest.java @@ -0,0 +1,78 @@ +package org.runimo.runimo.user.service.usecases; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.runimo.runimo.user.UserFixtures; +import org.runimo.runimo.user.controller.request.UpdateNotificationAllowedRequst; +import org.runimo.runimo.user.domain.DevicePlatform; +import org.runimo.runimo.user.domain.UserDeviceToken; +import org.runimo.runimo.user.repository.UserDeviceTokenRepository; +import org.runimo.runimo.user.service.UserFinder; +import org.runimo.runimo.user.service.dto.command.UpdateNotificationAllowedCommand; + +class UpdateUserDetailUsecaseImplTest { + + private UpdateUserDetailUsecase updateUserDetailUsecase; + @Mock + private UserFinder userFinder; + @Mock + private UserDeviceTokenRepository userDeviceTokenRepository; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + updateUserDetailUsecase = new UpdateUserDetailUsecaseImpl(userFinder, + userDeviceTokenRepository); + } + + @Test + void 토큰이_없으면_새로_생성() { + // given + given(userDeviceTokenRepository.findByUserId(any())) + .willReturn(Optional.empty()); + given(userFinder.findUserById(any())) + .willReturn(Optional.ofNullable(UserFixtures.getDefaultUser())); + given(userDeviceTokenRepository.save(any())) + .willReturn(UserDeviceToken.from("token", DevicePlatform.APNS, true)); + var command = UpdateNotificationAllowedCommand.of( + 1L, + new UpdateNotificationAllowedRequst(true, "token", "APNS") + ); + + // when + updateUserDetailUsecase.updateUserNotificationAllowed(command); + + // then + verify(userDeviceTokenRepository, times(1)).save(any()); + } + + @Test + void 토큰_있으면_생성안함() { + given(userDeviceTokenRepository.findByUserId(any())) + .willReturn( + Optional.ofNullable(UserDeviceToken.from("token", DevicePlatform.APNS, true))); + given(userFinder.findUserById(any())) + .willReturn(Optional.ofNullable(UserFixtures.getDefaultUser())); + given(userDeviceTokenRepository.save(any())) + .willReturn(UserDeviceToken.from("token", DevicePlatform.APNS, true)); + var command = UpdateNotificationAllowedCommand.of( + 1L, + new UpdateNotificationAllowedRequst(true, "token", "APNS") + ); + + // when + updateUserDetailUsecase.updateUserNotificationAllowed(command); + + // then + verify(userDeviceTokenRepository, times(0)).save(any()); + } + +} \ No newline at end of file From 97f5646062d5bb65c9992f0e05f15e5a871ba86b Mon Sep 17 00:00:00 2001 From: ekgns33 Date: Wed, 9 Jul 2025 22:00:22 +0900 Subject: [PATCH 15/15] :white_check_mark: test : remove unnecessary mock setup --- .../user/service/usecases/UpdateUserDetailUsecaseImplTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImplTest.java b/src/test/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImplTest.java index d08324f..6aa046a 100644 --- a/src/test/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImplTest.java +++ b/src/test/java/org/runimo/runimo/user/service/usecases/UpdateUserDetailUsecaseImplTest.java @@ -61,8 +61,6 @@ void setUp() { Optional.ofNullable(UserDeviceToken.from("token", DevicePlatform.APNS, true))); given(userFinder.findUserById(any())) .willReturn(Optional.ofNullable(UserFixtures.getDefaultUser())); - given(userDeviceTokenRepository.save(any())) - .willReturn(UserDeviceToken.from("token", DevicePlatform.APNS, true)); var command = UpdateNotificationAllowedCommand.of( 1L, new UpdateNotificationAllowedRequst(true, "token", "APNS")