Skip to content

Commit

Permalink
Merge pull request #120 from cvs-go/feature#117
Browse files Browse the repository at this point in the history
회원 탈퇴 기능 추가
  • Loading branch information
chaewss authored Apr 22, 2024
2 parents 27b9f4d + bb11aed commit 0a88e7c
Show file tree
Hide file tree
Showing 19 changed files with 188 additions and 49 deletions.
1 change: 1 addition & 0 deletions sql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
23 changes: 16 additions & 7 deletions src/docs/asciidoc/api-doc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 값 | 정렬 기준 | 비고
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/cvsgo/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ public SuccessResponse<Void> updateUser(@LoginUser User user,
return SuccessResponse.create();
}

@DeleteMapping("/user")
public SuccessResponse<Void> deleteUser(@LoginUser User user) {
userService.deleteUser(user);
return SuccessResponse.create();
}

@PostMapping("/users/{userId}/followers")
@ResponseStatus(HttpStatus.CREATED)
public SuccessResponse<Void> createUserFollow(@LoginUser User user, @PathVariable Long userId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class ReadProductReviewQueryDto {

private final String reviewerProfileImageUrl;

private final Boolean reviewerIsDeleted;

private final boolean isFollowing;

private final String content;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,21 @@ private ReadProductReviewResponseDto(Long reviewId, Long reviewerId, String revi
this.createdAt = createdAt;
}

public static ReadProductReviewResponseDto of(ReadProductReviewQueryDto queryDto, User loginUser,
List<String> reviewImageUrls, List<String> tags) {
public static ReadProductReviewResponseDto of(ReadProductReviewQueryDto queryDto,
User loginUser, List<String> reviewImageUrls, List<String> 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)
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/com/cvsgo/dto/review/ReadReviewQueryDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class ReadReviewQueryDto {

private final String reviewerProfileImageUrl;

private final Boolean reviewerIsDeleted;

private final boolean isFollowing;

private final Long likeCount;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
12 changes: 7 additions & 5 deletions src/main/java/com/cvsgo/dto/review/ReviewDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> reviewerTags,
String reviewerProfileImageUrl, Boolean isFollowing, List<String> reviewerTags,
Long reviewLikeCount, Integer reviewRating, String reviewContent, LocalDateTime createdAt,
Boolean isReviewLiked, Boolean isProductBookmarked, List<String> reviewImageUrls) {
this.productId = productId;
Expand All @@ -71,17 +71,19 @@ public ReviewDto(Long productId, String productName, String productManufacturer,

public static ReviewDto of(ReadReviewQueryDto readReviewQueryDto,
List<String> reviewImageUrls, List<String> 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())
Expand Down
9 changes: 7 additions & 2 deletions src/main/java/com/cvsgo/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -46,16 +48,19 @@ 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<UserTag> 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<Tag> tags) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RefreshToken, Long> {

Optional<RefreshToken> findByToken(String token);

void deleteAllByUser(User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public List<ReadReviewQueryDto> findAllByFilter(User loginUser,
review.user.id,
review.user.nickname,
review.user.profileImageUrl,
review.user.isDeleted,
userFollow,
review.likeCount,
review.rating,
Expand Down Expand Up @@ -85,6 +86,7 @@ public List<ReadProductReviewQueryDto> findAllByProductIdAndFilter(User loginUse
review.id,
user.nickname,
user.profileImageUrl,
user.isDeleted,
userFollow,
review.content,
review.rating,
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/cvsgo/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/cvsgo/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,6 +47,8 @@ public class UserService {

private final UserFollowRepository userFollowRepository;

private final RefreshTokenRepository refreshTokenRepository;

private final ReviewRepository reviewRepository;

private final PasswordEncoder passwordEncoder;
Expand Down Expand Up @@ -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);
}

/**
* 회원 팔로우를 생성한다.
*
Expand Down
17 changes: 17 additions & 0 deletions src/test/java/com/cvsgo/controller/AuthControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("[email protected]")
.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 {
Expand Down
Loading

0 comments on commit 0a88e7c

Please sign in to comment.