diff --git a/backend/src/main/java/net/pengcook/user/controller/UserController.java b/backend/src/main/java/net/pengcook/user/controller/UserController.java index 3d477671..9c421514 100644 --- a/backend/src/main/java/net/pengcook/user/controller/UserController.java +++ b/backend/src/main/java/net/pengcook/user/controller/UserController.java @@ -27,28 +27,30 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor +@RequestMapping("/user") public class UserController { private final UserService userService; private final UserFollowService userFollowService; - @GetMapping("/user/me") + @GetMapping("/me") public ProfileResponse getUserProfile(@LoginUser UserInfo userInfo) { return userService.getProfile(userInfo.getId(), userInfo.getId()); } - @GetMapping("/user/{userId}") + @GetMapping("/{userId}") public ProfileResponse getUserProfile(@LoginUser UserInfo userInfo, @PathVariable long userId) { return userService.getProfile(userInfo.getId(), userId); } - @PatchMapping("/user/me") + @PatchMapping("/me") public UpdateProfileResponse updateUserProfile( @LoginUser UserInfo userInfo, @RequestBody @Valid UpdateProfileRequest updateProfileRequest @@ -56,18 +58,18 @@ public UpdateProfileResponse updateUserProfile( return userService.updateProfile(userInfo.getId(), updateProfileRequest); } - @DeleteMapping("/user/me") + @DeleteMapping("/me") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteUser(@LoginUser UserInfo userInfo) { userService.deleteUser(userInfo); } - @GetMapping("/user/username/check") + @GetMapping("/username/check") public UsernameCheckResponse checkUsername(@RequestParam @NotBlank String username) { return userService.checkUsername(username); } - @PostMapping("/user/report") + @PostMapping("/report") @ResponseStatus(HttpStatus.CREATED) public ReportResponse report( @LoginUser UserInfo userInfo, @@ -76,12 +78,12 @@ public ReportResponse report( return userService.report(userInfo.getId(), reportRequest); } - @GetMapping("/user/report/reason") + @GetMapping("/report/reason") public List getReportReasons() { return ReportReasonResponse.REASONS; } - @PostMapping("/user/block") + @PostMapping("/block") @ResponseStatus(HttpStatus.CREATED) public UserBlockResponse blockUser( @LoginUser UserInfo userInfo, @@ -90,7 +92,18 @@ public UserBlockResponse blockUser( return userService.blockUser(userInfo.getId(), userBlockRequest.blockeeId()); } - @PostMapping("/user/follow") + @DeleteMapping("/block") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteBlock(@LoginUser UserInfo userInfo, @RequestBody @Valid UserBlockRequest userBlockRequest) { + userService.deleteBlock(userInfo.getId(), userBlockRequest.blockeeId()); + } + + @GetMapping("/block/list") + public List getBlockees(@LoginUser UserInfo userInfo) { + return userService.getBlockeesOf(userInfo.getId()); + } + + @PostMapping("/follow") @ResponseStatus(HttpStatus.CREATED) public UserFollowResponse followUser( @LoginUser UserInfo userInfo, @@ -99,7 +112,7 @@ public UserFollowResponse followUser( return userFollowService.followUser(userInfo.getId(), userFollowRequest.targetId()); } - @DeleteMapping("/user/follow") + @DeleteMapping("/follow") @ResponseStatus(HttpStatus.NO_CONTENT) public void unfollowUser( @LoginUser UserInfo userInfo, @@ -108,7 +121,7 @@ public void unfollowUser( userFollowService.unfollowUser(userInfo.getId(), userFollowRequest.targetId()); } - @DeleteMapping("/user/follower") + @DeleteMapping("/follower") @ResponseStatus(HttpStatus.NO_CONTENT) public void removeFollower( @LoginUser UserInfo userInfo, @@ -117,12 +130,12 @@ public void removeFollower( userFollowService.unfollowUser(userFollowRequest.targetId(), userInfo.getId()); } - @GetMapping("/user/{userId}/follower") + @GetMapping("/{userId}/follower") public FollowInfoResponse getFollowerInfo(@PathVariable long userId) { return userFollowService.getFollowerInfo(userId); } - @GetMapping("/user/{userId}/following") + @GetMapping("/{userId}/following") public FollowInfoResponse getFollowingInfo(@PathVariable long userId) { return userFollowService.getFollowingInfo(userId); } diff --git a/backend/src/main/java/net/pengcook/user/dto/UserBlockResponse.java b/backend/src/main/java/net/pengcook/user/dto/UserBlockResponse.java index 5e15bcf2..60adb698 100644 --- a/backend/src/main/java/net/pengcook/user/dto/UserBlockResponse.java +++ b/backend/src/main/java/net/pengcook/user/dto/UserBlockResponse.java @@ -1,4 +1,10 @@ package net.pengcook.user.dto; +import net.pengcook.user.domain.UserBlock; + public record UserBlockResponse(UserResponse blocker, UserResponse blockee) { + + public UserBlockResponse(UserBlock userBlock) { + this(new UserResponse(userBlock.getBlocker()), new UserResponse(userBlock.getBlockee())); + } } diff --git a/backend/src/main/java/net/pengcook/user/repository/UserBlockRepository.java b/backend/src/main/java/net/pengcook/user/repository/UserBlockRepository.java index 7643fca8..b8691a1c 100644 --- a/backend/src/main/java/net/pengcook/user/repository/UserBlockRepository.java +++ b/backend/src/main/java/net/pengcook/user/repository/UserBlockRepository.java @@ -1,6 +1,7 @@ package net.pengcook.user.repository; import java.util.List; +import net.pengcook.user.domain.User; import net.pengcook.user.domain.UserBlock; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -14,4 +15,6 @@ public interface UserBlockRepository extends JpaRepository { void deleteByBlockerId(long blockerId); boolean existsByBlockerIdAndBlockeeId(long blockerId, long blockeeId); + + void deleteByBlockerAndBlockee(User blocker, User blockee); } diff --git a/backend/src/main/java/net/pengcook/user/service/UserService.java b/backend/src/main/java/net/pengcook/user/service/UserService.java index f570be98..1bd57082 100644 --- a/backend/src/main/java/net/pengcook/user/service/UserService.java +++ b/backend/src/main/java/net/pengcook/user/service/UserService.java @@ -20,7 +20,6 @@ import net.pengcook.user.dto.UpdateProfileRequest; import net.pengcook.user.dto.UpdateProfileResponse; import net.pengcook.user.dto.UserBlockResponse; -import net.pengcook.user.dto.UserResponse; import net.pengcook.user.dto.UsernameCheckResponse; import net.pengcook.user.exception.NotFoundException; import net.pengcook.user.exception.UserNotFoundException; @@ -91,8 +90,26 @@ public UserBlockResponse blockUser(long blockerId, long blockeeId) { userFollowService.blockUserFollow(blockerId, blockeeId); UserBlock userBlock = userBlockRepository.save(new UserBlock(blocker, blockee)); - return new UserBlockResponse(new UserResponse(userBlock.getBlocker()), - new UserResponse(userBlock.getBlockee())); + return new UserBlockResponse(userBlock); + } + + @Transactional + public void deleteBlock(long blockerId, long blockeeId) { + User blocker = userRepository.findById(blockerId) + .orElseThrow(() -> new UserNotFoundException("정상적으로 로그인되지 않았습니다.")); + User blockee = userRepository.findById(blockeeId) + .orElseThrow(() -> new UserNotFoundException("차단한 사용자를 찾을 수 없습니다.")); + + userBlockRepository.deleteByBlockerAndBlockee(blocker, blockee); + } + + @Transactional(readOnly = true) + public List getBlockeesOf(long blockerId) { + List userBlocks = userBlockRepository.findAllByBlockerId(blockerId); + + return userBlocks.stream() + .map(UserBlockResponse::new) + .toList(); } @Transactional diff --git a/backend/src/test/java/net/pengcook/user/controller/UserControllerTest.java b/backend/src/test/java/net/pengcook/user/controller/UserControllerTest.java index 80d41577..da927210 100644 --- a/backend/src/test/java/net/pengcook/user/controller/UserControllerTest.java +++ b/backend/src/test/java/net/pengcook/user/controller/UserControllerTest.java @@ -27,6 +27,7 @@ import net.pengcook.user.dto.UpdateProfileRequest; import net.pengcook.user.dto.UpdateProfileResponse; import net.pengcook.user.dto.UserBlockRequest; +import net.pengcook.user.repository.UserBlockRepository; import net.pengcook.user.dto.UserFollowRequest; import net.pengcook.user.repository.UserRepository; import org.junit.jupiter.api.DisplayName; @@ -42,7 +43,8 @@ class UserControllerTest extends RestDocsSetting { @Autowired UserRepository userRepository; - + @Autowired + UserBlockRepository userBlockRepository; @Autowired ImageClientService imageClientService; @@ -348,6 +350,85 @@ void blockUser() { .body("blockee.id", is(2)); } + @Test + @WithLoginUser(email = "loki@pengcook.net") + @DisplayName("사용자 차단을 해제한다.") + void deleteBlock() { + UserBlockRequest userBlockRequest = new UserBlockRequest(3L); + + RestAssured.given(spec).log().all() + .filter(document(DEFAULT_RESTDOCS_PATH, + "사용자 차단을 해제합니다.", + "사용자 차단 해제 API", + requestFields( + fieldWithPath("blockeeId").description("차단한 사용자 ID") + ) + )) + .contentType(ContentType.JSON) + .body(userBlockRequest) + .when().delete("/user/block") + .then().log().all() + .statusCode(204); + + boolean exists = userBlockRepository.existsByBlockerIdAndBlockeeId(1L, 3L); + + assertThat(exists).isFalse(); + } + + @Test + @WithLoginUser(email = "loki@pengcook.net") + @DisplayName("사용자의 차단 목록을 불러온다.") + void getBlockeesOf() { + RestAssured.given(spec).log().all() + .filter(document(DEFAULT_RESTDOCS_PATH, + "로그인한 사용자의 차단 목록을 조회합니다.", + "차단 목록 조회 API", + responseFields( + fieldWithPath("[]").description("차단 목록"), + fieldWithPath("[].blocker.id").description("차단자 ID"), + fieldWithPath("[].blocker.email").description("차단자 이메일"), + fieldWithPath("[].blocker.username").description("차단자 아이디"), + fieldWithPath("[].blocker.nickname").description("차단자 닉네임"), + fieldWithPath("[].blocker.image").description("차단자 프로필 이미지"), + fieldWithPath("[].blocker.region").description("차단자 국가"), + fieldWithPath("[].blockee.id").description("차단대상 ID"), + fieldWithPath("[].blockee.email").description("차단대상 이메일"), + fieldWithPath("[].blockee.username").description("차단대상 아이디"), + fieldWithPath("[].blockee.nickname").description("차단대상 닉네임"), + fieldWithPath("[].blockee.image").description("차단대상 프로필 이미지"), + fieldWithPath("[].blockee.region").description("차단대상 국가") + ) + )) + .contentType(ContentType.JSON) + .when().get("/user/block/list") + .then().log().all() + .statusCode(200) + .body("[0].blocker.id", equalTo(1)) + .body("[0].blocker.email", equalTo("loki@pengcook.net")) + .body("[0].blocker.username", equalTo("loki")) + .body("[0].blocker.nickname", equalTo("로키")) + .body("[0].blocker.image", equalTo("loki.jpg")) + .body("[0].blocker.region", equalTo("KOREA")) + .body("[0].blockee.id", equalTo(3)) + .body("[0].blockee.email", equalTo("crocodile@pengcook.net")) + .body("[0].blockee.username", equalTo("crocodile")) + .body("[0].blockee.nickname", equalTo("악어")) + .body("[0].blockee.image", equalTo("crocodile.jpg")) + .body("[0].blockee.region", equalTo("KOREA")) + .body("[1].blocker.id", equalTo(1)) + .body("[1].blocker.email", equalTo("loki@pengcook.net")) + .body("[1].blocker.username", equalTo("loki")) + .body("[1].blocker.nickname", equalTo("로키")) + .body("[1].blocker.image", equalTo("loki.jpg")) + .body("[1].blocker.region", equalTo("KOREA")) + .body("[1].blockee.id", equalTo(4)) + .body("[1].blockee.email", equalTo("birdsheep@pengcook.net")) + .body("[1].blockee.username", equalTo("birdsheep")) + .body("[1].blockee.nickname", equalTo("새양")) + .body("[1].blockee.image", equalTo("birdsheep.jpg")) + .body("[1].blockee.region", equalTo("KOREA")); + } + @Test @WithLoginUser @DisplayName("사용자를 삭제한다.") diff --git a/backend/src/test/java/net/pengcook/user/service/UserFollowServiceTest.java b/backend/src/test/java/net/pengcook/user/service/UserFollowServiceTest.java index 744c39e8..0db3afc7 100644 --- a/backend/src/test/java/net/pengcook/user/service/UserFollowServiceTest.java +++ b/backend/src/test/java/net/pengcook/user/service/UserFollowServiceTest.java @@ -46,7 +46,7 @@ void follow() { @DisplayName("차단 관계에 있는 사용자를 팔로우하면 예외가 발생한다.") void followUserWhenBlockingOrBlocked() { long followerId = 1L; - long blockingFolloweeId = 2L; + long blockingFolloweeId = 3L; long blockedFolloweeId = 5L; assertThatThrownBy(() -> userFollowService.followUser(followerId, blockingFolloweeId)) diff --git a/backend/src/test/java/net/pengcook/user/service/UserServiceTest.java b/backend/src/test/java/net/pengcook/user/service/UserServiceTest.java index 79ec5469..88ad8c42 100644 --- a/backend/src/test/java/net/pengcook/user/service/UserServiceTest.java +++ b/backend/src/test/java/net/pengcook/user/service/UserServiceTest.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import java.util.List; import net.pengcook.authentication.domain.UserInfo; import net.pengcook.comment.repository.CommentRepository; import net.pengcook.image.service.ImageClientService; @@ -195,7 +196,11 @@ void blockUser() { UserBlockResponse actual = userService.blockUser(blockerId, blockeeId); - assertThat(actual).isEqualTo(expected); + assertAll( + () -> assertThat(actual).isEqualTo(expected), + () -> assertThat(userBlockRepository.existsByBlockerIdAndBlockeeId(1L, 2L)).isTrue() + + ); } @Test @@ -249,6 +254,58 @@ void blockUserWhenNotExistBlockee() { .hasMessage("차단할 사용자를 찾을 수 없습니다."); } + @Test + @DisplayName("사용자 차단을 해제한다.") + void deleteBlock() { + long blockerId = 1L; + long blockeeId = 3L; + + userService.deleteBlock(blockerId, blockeeId); + + assertThat(userBlockRepository.existsByBlockerIdAndBlockeeId(1L, 3L)).isFalse(); + } + + @Test + @DisplayName("차단을 해제하는 사용자가 존재하지 않으면 예외가 발생한다.") + void deleteBlockWhenNotExistBlocker() { + long blockerId = 2000L; + long blockeeId = 3L; + + assertThatThrownBy(() -> userService.deleteBlock(blockerId, blockeeId)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("정상적으로 로그인되지 않았습니다."); + } + + @Test + @DisplayName("차단당한 사용자가 존재하지 않으면 예외가 발생한다.") + void deleteBlockWhenNotExistBlockee() { + long blockerId = 1L; + long blockeeId = 2000L; + + assertThatThrownBy(() -> userService.deleteBlock(blockerId, blockeeId)) + .isInstanceOf(UserNotFoundException.class) + .hasMessage("차단한 사용자를 찾을 수 없습니다."); + } + + @Test + @DisplayName("차단 목록을 불러온다.") + void getBlockeesOf() { + long blockerId = 1L; + List expected = List.of( + new UserBlockResponse( + new UserResponse(blockerId, "loki@pengcook.net", "loki", "로키", "loki.jpg", "KOREA"), + new UserResponse(3L, "crocodile@pengcook.net", "crocodile", "악어", "crocodile.jpg", "KOREA") + ), + new UserBlockResponse( + new UserResponse(blockerId, "loki@pengcook.net", "loki", "로키", "loki.jpg", "KOREA"), + new UserResponse(4L, "birdsheep@pengcook.net", "birdsheep", "새양", "birdsheep.jpg", "KOREA") + )); + + List userBlockResponses = userService.getBlockeesOf(blockerId); + + assertThat(userBlockResponses).containsExactlyElementsOf(expected); + } + @Test @DisplayName("차단한 사용자들의 목록을 불러올 수 있다.") void getBlockedUserGroup() { @@ -257,9 +314,9 @@ void getBlockedUserGroup() { BlockedUserGroup blockedUserGroup = userService.getBlockedUserGroup(blockerId); assertAll( - () -> assertThat(blockedUserGroup.isBlocked(2L)).isTrue(), + () -> assertThat(blockedUserGroup.isBlocked(2L)).isFalse(), () -> assertThat(blockedUserGroup.isBlocked(3L)).isTrue(), - () -> assertThat(blockedUserGroup.isBlocked(4L)).isFalse() + () -> assertThat(blockedUserGroup.isBlocked(4L)).isTrue() ); } diff --git a/backend/src/test/resources/data/users.sql b/backend/src/test/resources/data/users.sql index c00db2e5..4173aad9 100644 --- a/backend/src/test/resources/data/users.sql +++ b/backend/src/test/resources/data/users.sql @@ -191,8 +191,8 @@ VALUES (1, '레시피1 설명1 이미지', '레시피1 설명1', 1), (1, '레시피1 설명2 이미지', '레시피1 설명2', 2); INSERT INTO user_block (blocker_id, blockee_id) -VALUES (1, 2), - (1, 3), +VALUES (1, 3), + (1, 4), (5, 1); INSERT INTO user_follow (follower_id, followee_id)