Skip to content

Commit

Permalink
feat: #70 회원 탈퇴 기능/API 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
Wo-ogie committed May 18, 2024
1 parent 831c334 commit af10e88
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public enum CustomExceptionType {
USER_KAKAO_UID_DUPLICATION(2204, "이미 가입한 계정입니다."),
USER_NOT_FOUND_BY_KAKAO_UID(2205, "일치하는 회원을 찾을 수 없습니다."),
USER_NOT_FOUND_BY_PHONE(2206, "일치하는 회원을 찾을 수 없습니다."),
USER_DELETION_PERMISSION_DENIED(2207, "회원 탈퇴 권한이 없습니다. 사용중인 계정과 탈퇴하려는 계정 정보가 다르지는 않은지 확인해주세요."),

KAKAO_CLIENT(10000, "카카오 서버와의 통신 중 오류가 발생했습니다."),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,8 @@ public void update(InstrumentUpdateRequest updateRequest) {
this.description = new InstrumentDescription(updateRequest.getDescription());
}
}

public void removeSeller() {
this.seller = null;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
package com.ajou.hertz.domain.instrument.service;

import java.util.Collection;
import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.ajou.hertz.domain.instrument.acoustic_and_classic_guitar.dto.AcousticAndClassicGuitarDto;
import com.ajou.hertz.domain.instrument.acoustic_and_classic_guitar.dto.request.AcousticAndClassicGuitarUpdateRequest;
import com.ajou.hertz.domain.instrument.acoustic_and_classic_guitar.dto.request.CreateNewAcousticAndClassicGuitarRequest;
Expand Down Expand Up @@ -47,8 +41,12 @@
import com.ajou.hertz.domain.instrument.strategy.InstrumentCreationStrategy;
import com.ajou.hertz.domain.user.entity.User;
import com.ajou.hertz.domain.user.service.UserQueryService;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collection;
import java.util.List;

@RequiredArgsConstructor
@Transactional
Expand Down Expand Up @@ -283,6 +281,16 @@ public EffectorDto updateEffector(
return (EffectorDto)updateInstrument(userId, effectorId, updateRequest);
}

/**
* 판매자가 <code>sellerId</code>와 일치하는 모든 악기 매물 데이터에서 판매자를 제거한다. (회원 탈퇴 시 사용)
*
* @param sellerId id of seller(user)
*/
public void removeSellerFromInstruments(Long sellerId) {
instrumentRepository.findAllBySellerId(sellerId)
.forEach(Instrument::removeSeller);
}

/**
* 악기 매물을 삭제한다.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,8 @@ public static PracticeRoom create(
description
);
}

public void removeSeller() {
this.seller = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import com.ajou.hertz.domain.practice_room.entity.PracticeRoom;

public interface PracticeRoomRepository extends JpaRepository<PracticeRoom, Long> {
import java.util.List;

public interface PracticeRoomRepository extends JpaRepository<PracticeRoom, Long> {
List<PracticeRoom> findAllBySeller_Id(Long sellerId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
@Service
public class PracticeRoomCommandService {

private final PracticeRoomQueryService practiceRoomQueryService;
private final PracticeRoomRepository practiceRoomRepository;
private final UserQueryService userQueryService;
private final PracticeRoomImageCommandService practiceRoomImageCommandService;
Expand All @@ -33,8 +34,7 @@ public class PracticeRoomCommandService {
* @param createNewPracticeRoomRequest 판매하고자 하는 합주실의 정보
* @return 생성된 합주실 entity
*/
public PracticeRoomDto createNewPracticeRoom(Long sellerId,
CreateNewPracticeRoomRequest createNewPracticeRoomRequest) {
public PracticeRoomDto createNewPracticeRoom(Long sellerId, CreateNewPracticeRoomRequest createNewPracticeRoomRequest) {
User seller = userQueryService.getById(sellerId);
PracticeRoom practiceRoom = practiceRoomRepository.save(createNewPracticeRoomRequest.toEntity(seller));

Expand All @@ -52,4 +52,13 @@ public PracticeRoomDto createNewPracticeRoom(Long sellerId,

return new PracticeRoomDto(practiceRoom);
}

/**
* <code>sellerId</code>에 해당하는 모든 연습실에서 판매자 정보를 제거한다. (회원 탈퇴 시 사용)
*
* @param sellerId id of seller(user)
*/
public void removeSellerFromPracticeRooms(Long sellerId) {
practiceRoomQueryService.findAllBySellerId(sellerId).forEach(PracticeRoom::removeSeller);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.ajou.hertz.domain.practice_room.service;

import com.ajou.hertz.domain.practice_room.entity.PracticeRoom;
import com.ajou.hertz.domain.practice_room.repository.PracticeRoomRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class PracticeRoomQueryService {

private final PracticeRoomRepository practiceRoomRepository;

public List<PracticeRoom> findAllBySellerId(Long sellerId) {
return practiceRoomRepository.findAllBySeller_Id(sellerId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import com.ajou.hertz.common.auth.UserPrincipal;
Expand Down Expand Up @@ -206,5 +198,21 @@ public SellerInfoResponse getSellerInfoV1(
return SellerInfoResponse.from(userDto, sellingCount, soldCount, createdInstruments);
}

@Operation(
summary = "회원 탈퇴",
description = """
<p>회원 데이터를 삭제합니다. 프로필 이미지 등 회원 관련 데이터도 함께 삭제됩니다.
<p>회원이 올렸던 매물(악기, 연습실, 공연장)은 유지되며, 다만 판매자 데이터가 제거됩니다(<code>null</code>).
""",
security = @SecurityRequirement(name = "access-token")
)
@DeleteMapping(value = "/{userId}", headers = API_VERSION_HEADER_NAME + "=" + 1)
public ResponseEntity<Void> deleteUserV1(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@PathVariable Long userId
) {
userCommandService.deleteUser(userPrincipal.getUserId(), userId);
return ResponseEntity.noContent().build();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ajou.hertz.domain.user.exception;

import com.ajou.hertz.common.exception.ForbiddenException;
import com.ajou.hertz.common.exception.constant.CustomExceptionType;

public class UserDeletionPermissionDeniedException extends ForbiddenException {
public UserDeletionPermissionDeniedException() {
super(CustomExceptionType.USER_DELETION_PERMISSION_DENIED);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ajou.hertz.domain.user.repository;

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -8,4 +9,5 @@

public interface UserProfileImageRepository extends JpaRepository<UserProfileImage, Long> {
Optional<UserProfileImage> findByUser_Id(Long userId);
List<UserProfileImage> findAllByUser_Id(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

import java.util.UUID;

import com.ajou.hertz.domain.instrument.service.InstrumentCommandService;
import com.ajou.hertz.domain.instrument.service.InstrumentQueryService;
import com.ajou.hertz.domain.practice_room.entity.PracticeRoom;
import com.ajou.hertz.domain.practice_room.service.PracticeRoomCommandService;
import com.ajou.hertz.domain.practice_room.service.PracticeRoomQueryService;
import com.ajou.hertz.domain.user.controller.UpdatePasswordWithoutAuthenticationRequest;
import com.ajou.hertz.domain.user.exception.UserDeletionPermissionDeniedException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -28,10 +34,12 @@
public class UserCommandService {

private final UserQueryService userQueryService;
private final UserProfileImageCommandService userProfileImageCommandService;
private final PracticeRoomCommandService practiceRoomCommandService;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final HertzProperties hertzProperties;
private final UserProfileImageCommandService userProfileImageCommandService;
private final InstrumentCommandService instrumentCommandService;

/**
* 새로운 회원을 등록한다.
Expand Down Expand Up @@ -187,4 +195,19 @@ public UserDto updatePassword(UpdatePasswordWithoutAuthenticationRequest updateP
user.changePassword(passwordEncoder.encode(updatePasswordRequest.getPassword()));
return UserDto.from(user);
}

public void deleteUser(Long requesterId, Long userId) {
if (!requesterId.equals(userId)) {
throw new UserDeletionPermissionDeniedException();
}

instrumentCommandService.removeSellerFromInstruments(userId);
practiceRoomCommandService.removeSellerFromPracticeRooms(userId);
// TODO: 공연장(ConcertHall)에서 유저 제거 (연관관계 끊기)

userRepository.deleteById(userId);
userProfileImageCommandService.deleteImagesByUserId(userId);

// TODO: 유저 탈퇴 이력 저장
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
package com.ajou.hertz.domain.user.service;

import java.util.Optional;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.ajou.hertz.common.file.dto.FileDto;
import com.ajou.hertz.common.file.service.FileService;
import com.ajou.hertz.domain.user.entity.User;
import com.ajou.hertz.domain.user.entity.UserProfileImage;
import com.ajou.hertz.domain.user.repository.UserProfileImageRepository;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.Optional;

@RequiredArgsConstructor
@Transactional
Expand All @@ -27,9 +26,8 @@ public class UserProfileImageCommandService {
/**
* 유저의 프로필 이미지를 업데이트한다.
*
* @param user 프로필 이미지를 업데이트할 유저
* @param user 프로필 이미지를 업데이트할 유저
* @param newProfileImage 새로운 프로필 이미지
*
* @return 새로운 프로필 이미지 URL
*/
public String updateProfileImage(User user, MultipartFile newProfileImage) {
Expand All @@ -39,6 +37,19 @@ public String updateProfileImage(User user, MultipartFile newProfileImage) {
return newUserProfileImage.getUrl();
}

/**
* 유저의 모든 프로필 이미지를 삭제한다.
*
* @param userId id of user
*/
public void deleteImagesByUserId(Long userId) {
List<UserProfileImage> profileImages = userProfileImageRepository.findAllByUser_Id(userId);
userProfileImageRepository.deleteAllInBatch(profileImages);
fileService.deleteAll(
profileImages.stream().map(UserProfileImage::getStoredName).toList()
);
}

/**
* 유저의 프로필 이미지를 삭제한다.
*
Expand All @@ -55,18 +66,18 @@ private void deleteOldProfileImage(Long userId) {

/**
* 새로운 프로필 이미지를 업로드한다.
* @param user 프로필 이미지를 업데이트할 유저
* @param newProfileImage 새로운 프로필 이미지
*
* @param user 프로필 이미지를 업데이트할 유저
* @param newProfileImage 새로운 프로필 이미지
* @return 새로운 프로필 이미지 entity
*/
private UserProfileImage uploadNewProfileImage(User user, MultipartFile newProfileImage) {
FileDto uploadedFile = fileService.uploadFile(newProfileImage, USER_PROFILE_IMAGE_UPLOAD_PATH);
UserProfileImage newUserProfileImage = UserProfileImage.create(
user,
uploadedFile.getOriginalName(),
uploadedFile.getStoredName(),
uploadedFile.getUrl());
user,
uploadedFile.getOriginalName(),
uploadedFile.getStoredName(),
uploadedFile.getUrl());
userProfileImageRepository.save(newUserProfileImage);
return newUserProfileImage;
}
Expand Down

0 comments on commit af10e88

Please sign in to comment.