From cb161fd03e6d50c4f20076bb3b729e624ae4e9b4 Mon Sep 17 00:00:00 2001 From: chaewss Date: Mon, 22 Apr 2024 04:13:46 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20API=20=EC=B6=94=EA=B0=80=20#117?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/schema.sql | 1 + .../java/com/cvsgo/controller/UserController.java | 6 ++++++ src/main/java/com/cvsgo/entity/User.java | 6 +++++- .../cvsgo/repository/RefreshTokenRepository.java | 6 ++++-- src/main/java/com/cvsgo/service/UserService.java | 14 ++++++++++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/sql/schema.sql b/sql/schema.sql index 2f3f0e19..d0e7c86f 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -183,6 +183,7 @@ create table user ( profile_image_url varchar(255), created_at datetime, modified_at datetime, + is_deleted tinyint(1), primary key (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/main/java/com/cvsgo/controller/UserController.java b/src/main/java/com/cvsgo/controller/UserController.java index b66f1c14..e2424369 100644 --- a/src/main/java/com/cvsgo/controller/UserController.java +++ b/src/main/java/com/cvsgo/controller/UserController.java @@ -70,6 +70,12 @@ public SuccessResponse updateUser(@LoginUser User user, return SuccessResponse.create(); } + @DeleteMapping("/user") + public SuccessResponse deleteUser(@LoginUser User user) { + userService.deleteUser(user); + return SuccessResponse.create(); + } + @PostMapping("/users/{userId}/followers") @ResponseStatus(HttpStatus.CREATED) public SuccessResponse createUserFollow(@LoginUser User user, @PathVariable Long userId) { diff --git a/src/main/java/com/cvsgo/entity/User.java b/src/main/java/com/cvsgo/entity/User.java index db5f6a14..6e7d3f8a 100644 --- a/src/main/java/com/cvsgo/entity/User.java +++ b/src/main/java/com/cvsgo/entity/User.java @@ -18,9 +18,11 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; import org.springframework.security.crypto.password.PasswordEncoder; @Getter +@SQLDelete(sql = "UPDATE user SET is_deleted = true WHERE id = ?") @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class User extends BaseTimeEntity { @@ -46,7 +48,9 @@ public class User extends BaseTimeEntity { private String profileImageUrl; - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private Boolean isDeleted = Boolean.FALSE; + + @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST) private List userTags = new ArrayList<>(); @Builder diff --git a/src/main/java/com/cvsgo/repository/RefreshTokenRepository.java b/src/main/java/com/cvsgo/repository/RefreshTokenRepository.java index 1bba77a6..dbe3e615 100644 --- a/src/main/java/com/cvsgo/repository/RefreshTokenRepository.java +++ b/src/main/java/com/cvsgo/repository/RefreshTokenRepository.java @@ -1,11 +1,13 @@ package com.cvsgo.repository; import com.cvsgo.entity.RefreshToken; -import org.springframework.data.jpa.repository.JpaRepository; - +import com.cvsgo.entity.User; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; public interface RefreshTokenRepository extends JpaRepository { Optional findByToken(String token); + + void deleteAllByUser(User user); } diff --git a/src/main/java/com/cvsgo/service/UserService.java b/src/main/java/com/cvsgo/service/UserService.java index 068b6da5..cfaa54e3 100644 --- a/src/main/java/com/cvsgo/service/UserService.java +++ b/src/main/java/com/cvsgo/service/UserService.java @@ -19,6 +19,7 @@ import com.cvsgo.exception.BadRequestException; import com.cvsgo.exception.DuplicateException; import com.cvsgo.exception.NotFoundException; +import com.cvsgo.repository.RefreshTokenRepository; import com.cvsgo.repository.ReviewRepository; import com.cvsgo.repository.TagRepository; import com.cvsgo.repository.UserFollowRepository; @@ -46,6 +47,8 @@ public class UserService { private final UserFollowRepository userFollowRepository; + private final RefreshTokenRepository refreshTokenRepository; + private final ReviewRepository reviewRepository; private final PasswordEncoder passwordEncoder; @@ -139,6 +142,17 @@ public void updateUser(User user, UpdateUserRequestDto request) { user.updateProfileImageUrl(request.getProfileImageUrl()); } + /** + * 사용자를 논리 삭제한다. + * + * @param user 로그인한 사용자 + */ + @Transactional + public void deleteUser(User user) { + refreshTokenRepository.deleteAllByUser(user); + userRepository.delete(user); + } + /** * 회원 팔로우를 생성한다. * From fd41a6ae97339f7dfc43b74a3629a56f4a6ba974 Mon Sep 17 00:00:00 2001 From: chaewss Date: Mon, 22 Apr 2024 05:22:37 +0900 Subject: [PATCH 2/5] =?UTF-8?q?test:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20#117?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cvsgo/controller/UserControllerTest.java | 12 ++++++++++ .../cvsgo/repository/UserRepositoryTest.java | 24 +++++++++++++++++++ .../com/cvsgo/service/UserServiceTest.java | 14 +++++++++++ 3 files changed, 50 insertions(+) diff --git a/src/test/java/com/cvsgo/controller/UserControllerTest.java b/src/test/java/com/cvsgo/controller/UserControllerTest.java index df20f74f..0e831bca 100644 --- a/src/test/java/com/cvsgo/controller/UserControllerTest.java +++ b/src/test/java/com/cvsgo/controller/UserControllerTest.java @@ -398,6 +398,18 @@ void respond_409_when_update_user_but_nickname_conflicts() throws Exception { .andDo(print()); } + @Test + @DisplayName("회원 삭제에 성공하면 200을 응답한다") + void respond_200_when_delete_user_succeed() throws Exception { + mockMvc.perform(delete("/api/user") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(document(documentIdentifier, + getDocumentRequest(), + getDocumentResponse())); + } + @Test @DisplayName("회원 팔로우 생성에 성공하면 HTTP 201을 응답한다") void respond_201_when_create_user_follow_succeed() throws Exception { diff --git a/src/test/java/com/cvsgo/repository/UserRepositoryTest.java b/src/test/java/com/cvsgo/repository/UserRepositoryTest.java index 80999d57..0d8d5698 100644 --- a/src/test/java/com/cvsgo/repository/UserRepositoryTest.java +++ b/src/test/java/com/cvsgo/repository/UserRepositoryTest.java @@ -3,6 +3,7 @@ import com.cvsgo.config.TestConfig; import com.cvsgo.entity.Role; import com.cvsgo.entity.User; +import jakarta.persistence.EntityManager; import java.util.ArrayList; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -14,7 +15,9 @@ import java.util.Optional; import org.springframework.context.annotation.Import; +import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; @Import(TestConfig.class) @DataJpaTest @@ -24,6 +27,9 @@ class UserRepositoryTest { @Autowired private UserRepository userRepository; + @Autowired + private EntityManager em; + User user; @BeforeEach @@ -65,4 +71,22 @@ void succeed_to_find_user_by_email() { assertThat(foundUser).isPresent(); } + @Test + @DisplayName("사용자를 soft delete 한다") + void succeed_to_soft_delete_user() { + // given + User foundUser = userRepository.findById(user.getId()).orElseThrow(); + + // when + userRepository.delete(foundUser); + em.flush(); + em.clear(); + + User deletedUser = userRepository.findById(user.getId()).orElseThrow(); + + // then + assertThat(deletedUser).isNotNull(); + assertTrue(deletedUser.getIsDeleted()); + } + } diff --git a/src/test/java/com/cvsgo/service/UserServiceTest.java b/src/test/java/com/cvsgo/service/UserServiceTest.java index eb0c557d..bafba71f 100644 --- a/src/test/java/com/cvsgo/service/UserServiceTest.java +++ b/src/test/java/com/cvsgo/service/UserServiceTest.java @@ -9,6 +9,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import com.cvsgo.dto.user.SignUpRequestDto; import com.cvsgo.dto.user.UpdateUserRequestDto; @@ -21,6 +22,7 @@ import com.cvsgo.exception.BadRequestException; import com.cvsgo.exception.DuplicateException; import com.cvsgo.exception.NotFoundException; +import com.cvsgo.repository.RefreshTokenRepository; import com.cvsgo.repository.ReviewRepository; import com.cvsgo.repository.TagRepository; import com.cvsgo.repository.UserFollowRepository; @@ -57,6 +59,9 @@ class UserServiceTest { @Mock private UserFollowRepository userFollowRepository; + @Mock + private RefreshTokenRepository refreshTokenRepository; + @Mock private ReviewRepository reviewRepository; @@ -226,6 +231,15 @@ void should_throw_DuplicateException_when_update_user_but_nickname_is_duplicate( then(userRepository).should(times(1)).findByNickname(request.getNickname()); } + @Test + @DisplayName("회원을 정상적으로 논리 삭제한다") + void succeed_to_soft_delete_user() { + userService.deleteUser(user); + + then(refreshTokenRepository).should(times(1)).deleteAllByUser(user); + then(userRepository).should(times(1)).delete(any()); + } + @Test @DisplayName("회원 팔로우를 정상적으로 생성한다") void succeed_to_create_user_follow() { From 9afe15f71a8f7645c260514220fbb2d3cb5bc322 Mon Sep 17 00:00:00 2001 From: chaewss Date: Mon, 22 Apr 2024 05:25:25 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=C2=A0#117?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/cvsgo/service/AuthService.java | 1 + .../cvsgo/controller/AuthControllerTest.java | 17 +++++++++++++++ .../com/cvsgo/service/AuthServiceTest.java | 21 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/src/main/java/com/cvsgo/service/AuthService.java b/src/main/java/com/cvsgo/service/AuthService.java index 568aac0b..57c802f1 100644 --- a/src/main/java/com/cvsgo/service/AuthService.java +++ b/src/main/java/com/cvsgo/service/AuthService.java @@ -61,6 +61,7 @@ public AuthService(@Value("${jwt.secret-key}") final String secretKey, public LoginResponseDto login(LoginRequestDto request) { User user = userRepository.findByUserId(request.getEmail()) .orElseThrow(() -> NOT_FOUND_USER); + if (Boolean.TRUE.equals(user.getIsDeleted())) throw NOT_FOUND_USER; user.validatePassword(request.getPassword(), passwordEncoder); String accessToken = createAccessToken(user, key, ACCESS_TOKEN_TTL_MILLISECOND); diff --git a/src/test/java/com/cvsgo/controller/AuthControllerTest.java b/src/test/java/com/cvsgo/controller/AuthControllerTest.java index c8869399..7658a17f 100644 --- a/src/test/java/com/cvsgo/controller/AuthControllerTest.java +++ b/src/test/java/com/cvsgo/controller/AuthControllerTest.java @@ -133,6 +133,23 @@ void respond_400_when_login_but_user_does_not_exist() throws Exception { .andDo(print()); } + @Test + @DisplayName("탈퇴한 계정이면 로그인 API 호출시 HTTP 400를 응답한다") + void respond_400_when_login_but_user_is_deleted() throws Exception { + LoginRequestDto loginRequestDto = LoginRequestDto.builder() + .email("abc@naver.com") + .password("password1!") + .build(); + + given(authService.login(any())).willThrow(ExceptionConstants.NOT_FOUND_USER); + + mockMvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginRequestDto))) + .andExpect(status().isNotFound()) + .andDo(print()); + } + @Test @DisplayName("비밀번호가 일치하지 않으면 로그인 API 호출시 HTTP 401을 응답한다") void respond_401_when_password_is_not_correct() throws Exception { diff --git a/src/test/java/com/cvsgo/service/AuthServiceTest.java b/src/test/java/com/cvsgo/service/AuthServiceTest.java index bba23a71..eecafc71 100644 --- a/src/test/java/com/cvsgo/service/AuthServiceTest.java +++ b/src/test/java/com/cvsgo/service/AuthServiceTest.java @@ -94,6 +94,27 @@ void should_throw_NotFoundException_when_user_tries_to_login_but_user_does_not_e then(userRepository).should(times(1)).findByUserId(any()); } + @Test + @DisplayName("탈퇴한 사용자이면 NotFoundException이 발생한다") + void should_throw_NotFoundException_when_user_tries_to_login_but_user_is_deleted() { + LoginRequestDto loginRequestDto = LoginRequestDto.builder() + .email("abc@naver.com") + .password("password1!") + .build(); + User user = User.builder() + .userId("abc@naver.com") + .password(passwordEncoder.encode("password1!")) + .isDeleted(true) + .build(); + + given(userRepository.findByUserId(loginRequestDto.getEmail())) + .willReturn(Optional.of(user)); + + assertThrows(NotFoundException.class, () -> authService.login(loginRequestDto)); + + then(userRepository).should(times(1)).findByUserId(any()); + } + @Test @DisplayName("비밀번호가 일치하지 않으면 UnauthorizedException이 발생한다") void should_throw_UnauthorizedException_when_password_is_not_correct() { From ff7c42af4c6557ff592b77070f94e12c77f3b5b2 Mon Sep 17 00:00:00 2001 From: chaewss Date: Mon, 22 Apr 2024 05:40:12 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20=ED=83=88=ED=87=B4=ED=95=9C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EC=9D=98=20=EB=A6=AC=EB=B7=B0=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=C2=A0#117?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 탈퇴한 회원일 경우 - reviewerId - reviewerNickname - reviewerProfileImageUrl - isFollowing responseDto 필드값 null 처리 --- .../dto/review/ReadProductReviewQueryDto.java | 7 +++-- .../review/ReadProductReviewResponseDto.java | 14 ++++++---- .../cvsgo/dto/review/ReadReviewQueryDto.java | 9 ++++-- .../java/com/cvsgo/dto/review/ReviewDto.java | 12 ++++---- src/main/java/com/cvsgo/entity/User.java | 3 +- .../ReviewCustomRepositoryImpl.java | 2 ++ .../controller/ReviewControllerTest.java | 17 +++++------ .../com/cvsgo/service/ReviewServiceTest.java | 28 +++++++++---------- 8 files changed, 53 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/cvsgo/dto/review/ReadProductReviewQueryDto.java b/src/main/java/com/cvsgo/dto/review/ReadProductReviewQueryDto.java index 1a958d54..fd1b2a95 100644 --- a/src/main/java/com/cvsgo/dto/review/ReadProductReviewQueryDto.java +++ b/src/main/java/com/cvsgo/dto/review/ReadProductReviewQueryDto.java @@ -17,6 +17,8 @@ public class ReadProductReviewQueryDto { private final String reviewerProfileImageUrl; + private final Boolean reviewerIsDeleted; + private final boolean isFollowing; private final String content; @@ -31,12 +33,13 @@ public class ReadProductReviewQueryDto { @QueryProjection public ReadProductReviewQueryDto(long reviewerId, long reviewId, String reviewerNickname, - String reviewerProfileImageUrl, UserFollow userFollow, String content, int rating, - ReviewLike reviewLike, long likeCount, LocalDateTime createdAt) { + String reviewerProfileImageUrl, boolean reviewerIsDeleted, UserFollow userFollow, + String content, int rating, ReviewLike reviewLike, long likeCount, LocalDateTime createdAt) { this.reviewerId = reviewerId; this.reviewId = reviewId; this.reviewerNickname = reviewerNickname; this.reviewerProfileImageUrl = reviewerProfileImageUrl; + this.reviewerIsDeleted = reviewerIsDeleted; this.isFollowing = userFollow != null; this.content = content; this.rating = rating; diff --git a/src/main/java/com/cvsgo/dto/review/ReadProductReviewResponseDto.java b/src/main/java/com/cvsgo/dto/review/ReadProductReviewResponseDto.java index 2b596b41..c11de7b7 100644 --- a/src/main/java/com/cvsgo/dto/review/ReadProductReviewResponseDto.java +++ b/src/main/java/com/cvsgo/dto/review/ReadProductReviewResponseDto.java @@ -57,19 +57,21 @@ private ReadProductReviewResponseDto(Long reviewId, Long reviewerId, String revi this.createdAt = createdAt; } - public static ReadProductReviewResponseDto of(ReadProductReviewQueryDto queryDto, User loginUser, - List reviewImageUrls, List tags) { + public static ReadProductReviewResponseDto of(ReadProductReviewQueryDto queryDto, + User loginUser, List reviewImageUrls, List tags) { + boolean isReviewerDeleted = queryDto.getReviewerIsDeleted(); return ReadProductReviewResponseDto.builder() .reviewContent(queryDto.getContent()) .reviewId(queryDto.getReviewId()) - .reviewerId(queryDto.getReviewerId()) + .reviewerId(isReviewerDeleted ? null : queryDto.getReviewerId()) + .reviewerNickname(isReviewerDeleted ? null : queryDto.getReviewerNickname()) + .reviewerProfileImageUrl( + isReviewerDeleted ? null : queryDto.getReviewerProfileImageUrl()) + .isFollowingUser(isReviewerDeleted ? null : queryDto.isFollowing()) .reviewRating(queryDto.getRating()) .isReviewLiked(queryDto.isReviewLiked()) .reviewLikeCount(queryDto.getLikeCount()) - .isFollowingUser(queryDto.isFollowing()) .isMe(loginUser != null && loginUser.getId() == queryDto.getReviewerId()) - .reviewerNickname(queryDto.getReviewerNickname()) - .reviewerProfileImageUrl(queryDto.getReviewerProfileImageUrl()) .reviewImages(reviewImageUrls) .createdAt(queryDto.getCreatedAt()) .tags(tags) diff --git a/src/main/java/com/cvsgo/dto/review/ReadReviewQueryDto.java b/src/main/java/com/cvsgo/dto/review/ReadReviewQueryDto.java index 0a52cd22..015063c4 100644 --- a/src/main/java/com/cvsgo/dto/review/ReadReviewQueryDto.java +++ b/src/main/java/com/cvsgo/dto/review/ReadReviewQueryDto.java @@ -26,6 +26,8 @@ public class ReadReviewQueryDto { private final String reviewerProfileImageUrl; + private final Boolean reviewerIsDeleted; + private final boolean isFollowing; private final Long likeCount; @@ -43,9 +45,9 @@ public class ReadReviewQueryDto { @QueryProjection public ReadReviewQueryDto(Long reviewId, Long productId, String productName, String manufacturerName, String productImageUrl, Long reviewerId, String reviewerNickname, - String reviewerProfileImageUrl, UserFollow userFollow, Long likeCount, Integer rating, - String reviewContent, LocalDateTime createdAt, ReviewLike reviewLike, - ProductBookmark productBookmark) { + String reviewerProfileImageUrl, boolean reviewerIsDeleted, UserFollow userFollow, + Long likeCount, Integer rating, String reviewContent, LocalDateTime createdAt, + ReviewLike reviewLike, ProductBookmark productBookmark) { this.reviewId = reviewId; this.productId = productId; this.productName = productName; @@ -54,6 +56,7 @@ public ReadReviewQueryDto(Long reviewId, Long productId, String productName, this.reviewerId = reviewerId; this.reviewerNickname = reviewerNickname; this.reviewerProfileImageUrl = reviewerProfileImageUrl; + this.reviewerIsDeleted = reviewerIsDeleted; this.isFollowing = userFollow != null; this.likeCount = likeCount; this.rating = rating; diff --git a/src/main/java/com/cvsgo/dto/review/ReviewDto.java b/src/main/java/com/cvsgo/dto/review/ReviewDto.java index 6d246d22..76428476 100644 --- a/src/main/java/com/cvsgo/dto/review/ReviewDto.java +++ b/src/main/java/com/cvsgo/dto/review/ReviewDto.java @@ -47,7 +47,7 @@ public class ReviewDto { @Builder public ReviewDto(Long productId, String productName, String productManufacturer, String productImageUrl, Long reviewId, Long reviewerId, String reviewerNickname, - String reviewerProfileImageUrl, boolean isFollowing, List reviewerTags, + String reviewerProfileImageUrl, Boolean isFollowing, List reviewerTags, Long reviewLikeCount, Integer reviewRating, String reviewContent, LocalDateTime createdAt, Boolean isReviewLiked, Boolean isProductBookmarked, List reviewImageUrls) { this.productId = productId; @@ -71,17 +71,19 @@ public ReviewDto(Long productId, String productName, String productManufacturer, public static ReviewDto of(ReadReviewQueryDto readReviewQueryDto, List reviewImageUrls, List tags) { + boolean isReviewerDeleted = readReviewQueryDto.getReviewerIsDeleted(); return ReviewDto.builder() .productName(readReviewQueryDto.getProductName()) .productId(readReviewQueryDto.getProductId()) .reviewContent(readReviewQueryDto.getReviewContent()) .productImageUrl(readReviewQueryDto.getProductImageUrl()) - .isFollowing(readReviewQueryDto.isFollowing()) .productManufacturer(readReviewQueryDto.getManufacturerName()) .reviewId(readReviewQueryDto.getReviewId()) - .reviewerId(readReviewQueryDto.getReviewerId()) - .reviewerNickname(readReviewQueryDto.getReviewerNickname()) - .reviewerProfileImageUrl(readReviewQueryDto.getReviewerProfileImageUrl()) + .reviewerId(isReviewerDeleted ? null : readReviewQueryDto.getReviewerId()) + .reviewerNickname(isReviewerDeleted ? null : readReviewQueryDto.getReviewerNickname()) + .reviewerProfileImageUrl( + isReviewerDeleted ? null : readReviewQueryDto.getReviewerProfileImageUrl()) + .isFollowing(isReviewerDeleted ? null : readReviewQueryDto.isFollowing()) .reviewRating(readReviewQueryDto.getRating()) .reviewLikeCount(readReviewQueryDto.getLikeCount()) .createdAt(readReviewQueryDto.getCreatedAt()) diff --git a/src/main/java/com/cvsgo/entity/User.java b/src/main/java/com/cvsgo/entity/User.java index 6e7d3f8a..32fda485 100644 --- a/src/main/java/com/cvsgo/entity/User.java +++ b/src/main/java/com/cvsgo/entity/User.java @@ -54,12 +54,13 @@ public class User extends BaseTimeEntity { private List userTags = new ArrayList<>(); @Builder - public User(Long id, String userId, String password, String nickname, Role role) { + public User(Long id, String userId, String password, String nickname, Role role, Boolean isDeleted) { this.id = id; this.userId = userId; this.password = password; this.nickname = nickname; this.role = role; + this.isDeleted = isDeleted; } public static User create(String userId, String password, String nickname, List tags) { diff --git a/src/main/java/com/cvsgo/repository/ReviewCustomRepositoryImpl.java b/src/main/java/com/cvsgo/repository/ReviewCustomRepositoryImpl.java index c383ad0a..6bc72c81 100644 --- a/src/main/java/com/cvsgo/repository/ReviewCustomRepositoryImpl.java +++ b/src/main/java/com/cvsgo/repository/ReviewCustomRepositoryImpl.java @@ -45,6 +45,7 @@ public List findAllByFilter(User loginUser, review.user.id, review.user.nickname, review.user.profileImageUrl, + review.user.isDeleted, userFollow, review.likeCount, review.rating, @@ -85,6 +86,7 @@ public List findAllByProductIdAndFilter(User loginUse review.id, user.nickname, user.profileImageUrl, + user.isDeleted, userFollow, review.content, review.rating, diff --git a/src/test/java/com/cvsgo/controller/ReviewControllerTest.java b/src/test/java/com/cvsgo/controller/ReviewControllerTest.java index a6debf80..80800de6 100644 --- a/src/test/java/com/cvsgo/controller/ReviewControllerTest.java +++ b/src/test/java/com/cvsgo/controller/ReviewControllerTest.java @@ -185,9 +185,10 @@ void respond_200_when_success_to_read_reviews() throws Exception { fieldWithPath("data.reviews[].productManufacturer").type(JsonFieldType.STRING).description("상품 제조사"), fieldWithPath("data.reviews[].productImageUrl").type(JsonFieldType.STRING).description("상품 이미지 URL"), fieldWithPath("data.reviews[].reviewId").type(JsonFieldType.NUMBER).description("리뷰 ID"), - fieldWithPath("data.reviews[].reviewerId").type(JsonFieldType.NUMBER).description("리뷰 작성자 ID"), - fieldWithPath("data.reviews[].reviewerNickname").type(JsonFieldType.STRING).description("리뷰 작성자 닉네임"), - fieldWithPath("data.reviews[].reviewerProfileImageUrl").type(JsonFieldType.STRING).description("리뷰 작성자 프로필 이미지 URL"), + fieldWithPath("data.reviews[].reviewerId").type(JsonFieldType.NUMBER).description("리뷰 작성자 ID").optional(), + fieldWithPath("data.reviews[].reviewerNickname").type(JsonFieldType.STRING).description("리뷰 작성자 닉네임").optional(), + fieldWithPath("data.reviews[].reviewerProfileImageUrl").type(JsonFieldType.STRING).description("리뷰 작성자 프로필 이미지 URL").optional(), + fieldWithPath("data.content[].isFollowing").type(JsonFieldType.BOOLEAN).description("사용자가 리뷰 작성자를 팔로우하는지 여부").optional(), fieldWithPath("data.reviews[].reviewerTags").type(JsonFieldType.ARRAY).description("리뷰 작성자의 태그 목록"), fieldWithPath("data.reviews[].reviewLikeCount").type(JsonFieldType.NUMBER).description("리뷰 좋아요 개수"), fieldWithPath("data.reviews[].reviewRating").type(JsonFieldType.NUMBER).description("리뷰 별점"), @@ -210,7 +211,7 @@ void respond_200_when_success_to_read_product_reviews() throws Exception { Review review = Review.builder().id(1L).rating(4).content("맛있어요").user(reviewer) .imageUrls(List.of()).build(); ReadProductReviewQueryDto readProductReviewQueryDto = new ReadProductReviewQueryDto(reviewer.getId(), - review.getId(), reviewer.getNickname(), reviewer.getProfileImageUrl(), null, + review.getId(), reviewer.getNickname(), reviewer.getProfileImageUrl(), true, null, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewResponseDto responseDto = ReadProductReviewResponseDto.of(readProductReviewQueryDto, reviewer, @@ -233,15 +234,15 @@ void respond_200_when_success_to_read_product_reviews() throws Exception { ), relaxedResponseFields( fieldWithPath("data.content[].reviewId").type(JsonFieldType.NUMBER).description("리뷰 ID"), - fieldWithPath("data.content[].reviewerId").type(JsonFieldType.NUMBER).description("리뷰 작성자 ID"), - fieldWithPath("data.content[].reviewerNickname").type(JsonFieldType.STRING).description("리뷰 작성자 닉네임"), - fieldWithPath("data.content[].reviewerProfileImageUrl").type(JsonFieldType.STRING).description("리뷰 작성자 프로필 이미지 URL").optional(), + fieldWithPath("data.content[].reviewerId").type(JsonFieldType.NUMBER).description("리뷰 작성자 ID").optional(), + fieldWithPath("data.content[].reviewerNickname").type(JsonFieldType.STRING).description("리뷰 작성자 닉네임").optional(), + fieldWithPath("data.content[].reviewerProfileImageUrl").type(JsonFieldType.STRING).description("리뷰 작성자 프로필 이미지 URL").optional().optional(), + fieldWithPath("data.content[].isFollowingUser").type(JsonFieldType.BOOLEAN).description("사용자가 리뷰 작성자를 팔로우하는지 여부").optional(), fieldWithPath("data.content[].reviewerTags").type(JsonFieldType.ARRAY).description("리뷰 작성자의 태그 목록"), fieldWithPath("data.content[].reviewLikeCount").type(JsonFieldType.NUMBER).description("리뷰 좋아요 개수"), fieldWithPath("data.content[].reviewRating").type(JsonFieldType.NUMBER).description("리뷰 별점"), fieldWithPath("data.content[].reviewContent").type(JsonFieldType.STRING).description("리뷰 내용"), fieldWithPath("data.content[].isReviewLiked").type(JsonFieldType.BOOLEAN).description("사용자의 리뷰 좋아요 여부"), - fieldWithPath("data.content[].isFollowingUser").type(JsonFieldType.BOOLEAN).description("사용자가 리뷰 작성자를 팔로우하는지 여부"), fieldWithPath("data.content[].isMe").type(JsonFieldType.BOOLEAN).description("로그인한 사용자가 리뷰 작성자인지 여부"), fieldWithPath("data.content[].reviewImages").type(JsonFieldType.ARRAY).description("리뷰 이미지 URL 목록"), fieldWithPath("data.content[].createdAt").type(JsonFieldType.STRING).description("리뷰 생성 시간").optional() diff --git a/src/test/java/com/cvsgo/service/ReviewServiceTest.java b/src/test/java/com/cvsgo/service/ReviewServiceTest.java index ded34953..c00521fc 100644 --- a/src/test/java/com/cvsgo/service/ReviewServiceTest.java +++ b/src/test/java/com/cvsgo/service/ReviewServiceTest.java @@ -144,7 +144,7 @@ void succeed_to_read_review() { @DisplayName("특정 상품의 리뷰를 정상적으로 조회한다") void succeed_to_read_product_review() { ReadProductReviewQueryDto queryDto1 = new ReadProductReviewQueryDto(user1.getId(), review.getId(), - user1.getNickname(), user1.getProfileImageUrl(), userFollow, review.getContent(), + user1.getNickname(), user1.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewRequestDto requestDto = new ReadProductReviewRequestDto(List.of(1L, 2L, 3L), List.of(4, 5), ReviewSortBy.LATEST); @@ -173,22 +173,22 @@ void succeed_to_read_product_review() { @DisplayName("준회원인 사용자가 특정 상품의 리뷰 0페이지를 조회하면 5개만 조회된다") void should_get_only_five_reviews_when_associate_user_read_first_page_of_product_reviews() { ReadProductReviewQueryDto queryDto1 = new ReadProductReviewQueryDto(user1.getId(), review.getId(), - user1.getNickname(), user1.getProfileImageUrl(), userFollow, review.getContent(), + user1.getNickname(), user1.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewQueryDto queryDto2 = new ReadProductReviewQueryDto(user1.getId(), review.getId(), - user1.getNickname(), user1.getProfileImageUrl(), userFollow, review.getContent(), + user1.getNickname(), user1.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewQueryDto queryDto3 = new ReadProductReviewQueryDto(user1.getId(), review.getId(), - user1.getNickname(), user1.getProfileImageUrl(), userFollow, review.getContent(), + user1.getNickname(), user1.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewQueryDto queryDto4 = new ReadProductReviewQueryDto(user1.getId(), review.getId(), - user1.getNickname(), user1.getProfileImageUrl(), userFollow, review.getContent(), + user1.getNickname(), user1.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewQueryDto queryDto5 = new ReadProductReviewQueryDto(user1.getId(), review.getId(), - user1.getNickname(), user1.getProfileImageUrl(), userFollow, review.getContent(), + user1.getNickname(), user1.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewQueryDto queryDto6 = new ReadProductReviewQueryDto(user1.getId(), review.getId(), - user1.getNickname(), user1.getProfileImageUrl(), userFollow, review.getContent(), + user1.getNickname(), user1.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewRequestDto requestDto = new ReadProductReviewRequestDto(List.of(1L, 2L, 3L), List.of(4, 5), ReviewSortBy.LATEST); @@ -225,22 +225,22 @@ void should_get_only_five_reviews_when_associate_user_read_first_page_of_product @DisplayName("정회원인 사용자가 특정 상품의 리뷰 0페이지를 조회하면 정상적으로 조회된다") void should_success_to_read_product_reviews_when_regular_user_read_first_page_of_product_reviews() { ReadProductReviewQueryDto queryDto1 = new ReadProductReviewQueryDto(user2.getId(), review.getId(), - user2.getNickname(), user2.getProfileImageUrl(), userFollow, review.getContent(), + user2.getNickname(), user2.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewQueryDto queryDto2 = new ReadProductReviewQueryDto(user2.getId(), review.getId(), - user2.getNickname(), user2.getProfileImageUrl(), userFollow, review.getContent(), + user2.getNickname(), user2.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewQueryDto queryDto3 = new ReadProductReviewQueryDto(user2.getId(), review.getId(), - user2.getNickname(), user2.getProfileImageUrl(), userFollow, review.getContent(), + user2.getNickname(), user2.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewQueryDto queryDto4 = new ReadProductReviewQueryDto(user2.getId(), review.getId(), - user2.getNickname(), user2.getProfileImageUrl(), userFollow, review.getContent(), + user2.getNickname(), user2.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewQueryDto queryDto5 = new ReadProductReviewQueryDto(user2.getId(), review.getId(), - user2.getNickname(), user2.getProfileImageUrl(), userFollow, review.getContent(), + user2.getNickname(), user2.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewQueryDto queryDto6 = new ReadProductReviewQueryDto(user2.getId(), review.getId(), - user2.getNickname(), user2.getProfileImageUrl(), userFollow, review.getContent(), + user2.getNickname(), user2.getProfileImageUrl(), false, userFollow, review.getContent(), review.getRating(), null, review.getLikeCount(), LocalDateTime.now()); ReadProductReviewRequestDto requestDto = new ReadProductReviewRequestDto(List.of(1L, 2L, 3L), List.of(4, 5), ReviewSortBy.LATEST); @@ -497,7 +497,7 @@ void should_throw_NotFoundException_when_delete_review_but_review_does_not_exist ReadReviewQueryDto readReviewQueryDto = new ReadReviewQueryDto(1L, 2L, "불닭볶음면큰컵", "삼양", "https://어쩌구저쩌구/products/불닭볶음면.png", - user1.getId(), user1.getNickname(), user1.getProfileImageUrl(), null, 3L, + user1.getId(), user1.getNickname(), user1.getProfileImageUrl(), false, null, 3L, 4, "맛있어요", LocalDateTime.now(), null, null); Tag tag = Tag.builder() From bb11aed130b0621b2799827a20c6fee76dc21614 Mon Sep 17 00:00:00 2001 From: chaewss Date: Mon, 22 Apr 2024 05:40:38 +0900 Subject: [PATCH 5/5] =?UTF-8?q?docs:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1=20#117?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/api-doc.adoc | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/docs/asciidoc/api-doc.adoc b/src/docs/asciidoc/api-doc.adoc index 29405bfa..f8f2cc58 100644 --- a/src/docs/asciidoc/api-doc.adoc +++ b/src/docs/asciidoc/api-doc.adoc @@ -69,7 +69,13 @@ include::{snippets}/user-controller-test/respond_200_when_update_user_succeed/ht | `409 CONFLICT` | `DUPLICATE_NICKNAME` | 해당하는 닉네임을 가진 유저가 이미 있는 경우 |=== -=== 1-7. 회원 팔로우 생성 +=== 1-7. 회원 삭제 +==== Sample Request +include::{snippets}/user-controller-test/respond_200_when_delete_user_succeed/http-request.adoc[] +==== Sample Response +include::{snippets}/user-controller-test/respond_200_when_delete_user_succeed/http-response.adoc[] + +=== 1-8. 회원 팔로우 생성 ==== Path Parameters include::{snippets}/user-controller-test/respond_201_when_create_user_follow_succeed/path-parameters.adoc[] ==== Sample Request @@ -85,7 +91,7 @@ include::{snippets}/user-controller-test/respond_201_when_create_user_follow_suc | `409 CONFLICT` | `DUPLICATE_USER_FOLLOW` | 해당하는 유저 팔로우가 이미 있는 경우 |=== -=== 1-8. 회원 팔로우 삭제 +=== 1-9. 회원 팔로우 삭제 ==== Path Parameters include::{snippets}/user-controller-test/respond_200_when_delete_user_follow_succeed/path-parameters.adoc[] ==== Sample Request @@ -100,7 +106,7 @@ include::{snippets}/user-controller-test/respond_200_when_delete_user_follow_suc | `404 NOT FOUND` | `NOT_FOUND_USER_FOLLOW` | 해당하는 팔로우가 없는 경우 |=== -=== 1-9. 태그 매칭률 조회 +=== 1-10. 태그 매칭률 조회 ==== Path Parameters include::{snippets}/user-controller-test/respond_200_when_read_user_tag_match_percentage_successfully/path-parameters.adoc[] ==== Sample Request @@ -116,7 +122,7 @@ include::{snippets}/user-controller-test/respond_200_when_read_user_tag_match_pe | `404 NOT FOUND` | `NOT_FOUND_USER` | 해당하는 유저가 없는 경우 |=== -=== 1-10. 특정 회원의 좋아요 상품 목록 조회 +=== 1-11. 특정 회원의 좋아요 상품 목록 조회 ==== Path Parameters include::{snippets}/user-controller-test/respond_200_when_read_liked_product_list_successfully/path-parameters.adoc[] ==== Request Fields @@ -136,7 +142,7 @@ include::{snippets}/user-controller-test/respond_200_when_read_liked_product_lis ==== Sample Response include::{snippets}/user-controller-test/respond_200_when_read_liked_product_list_successfully/http-response.adoc[] -=== 1-11. 특정 회원의 북마크 상품 목록 조회 +=== 1-12. 특정 회원의 북마크 상품 목록 조회 ==== Path Parameters include::{snippets}/user-controller-test/respond_200_when_read_bookmarked_product_list_successfully/path-parameters.adoc[] ==== Request Fields @@ -156,9 +162,12 @@ include::{snippets}/user-controller-test/respond_200_when_read_bookmarked_produc ==== Sample Response include::{snippets}/user-controller-test/respond_200_when_read_bookmarked_product_list_successfully/http-response.adoc[] -=== 1-11. 특정 사용자의 리뷰 목록 조회 +=== 1-13. 특정 사용자의 리뷰 목록 조회 +==== Request Fields +==== Path Parameters +include::{snippets}/user-controller-test/respond_200_when_read_user_review_list_successfully/path-parameters.adoc[] ==== Request Fields -include::{snippets}/user-controller-test/respond_200_when_read_user_review_list_successfully/request-fields.adoc[] +include::{snippets}/user-controller-test/respond_200_when_read_user_review_list_successfully/query-parameters.adoc[] ==== 정렬 기준 항목 |=== | sortBy 값 | 정렬 기준 | 비고