diff --git a/src/main/java/backend/airo/api/global/swagger/UserControllerSwagger.java b/src/main/java/backend/airo/api/global/swagger/UserControllerSwagger.java index 336d838..6bb12de 100644 --- a/src/main/java/backend/airo/api/global/swagger/UserControllerSwagger.java +++ b/src/main/java/backend/airo/api/global/swagger/UserControllerSwagger.java @@ -40,4 +40,15 @@ Response updateMyPage( @UserPrincipal User user, @RequestBody UpdateUserInfoRequest updateUserInfoRequest ); + + @Operation(summary = "사용자 탈퇴", description = "사용자 탈퇴 API - 계정과 관련된 모든 데이터가 삭제됩니다") + @ApiResponse( + responseCode = "200", + description = "사용자 탈퇴 성공" + ) + @ApiResponse( + responseCode = "401", + description = "인증되지 않은 사용자" + ) + Response deleteUser(@UserPrincipal User user); } diff --git a/src/main/java/backend/airo/api/user/UserController.java b/src/main/java/backend/airo/api/user/UserController.java index e637da9..31827f6 100644 --- a/src/main/java/backend/airo/api/user/UserController.java +++ b/src/main/java/backend/airo/api/user/UserController.java @@ -41,4 +41,11 @@ public Response updateMyPage( ); return Response.success(UserResponse.create(updateUser)); } + + @DeleteMapping() + @PreAuthorize("isAuthenticated()") + public Response deleteUser(@UserPrincipal User user) { + userUseCase.deleteUser(user.getId()); + return Response.success(null); + } } diff --git a/src/main/java/backend/airo/application/user/usecase/UserUseCase.java b/src/main/java/backend/airo/application/user/usecase/UserUseCase.java index fa86f53..1895d42 100644 --- a/src/main/java/backend/airo/application/user/usecase/UserUseCase.java +++ b/src/main/java/backend/airo/application/user/usecase/UserUseCase.java @@ -3,17 +3,21 @@ import backend.airo.domain.user.User; import backend.airo.domain.user.command.UpdateExistingUserCommand; import backend.airo.domain.user.query.GetUserQuery; +import backend.airo.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; -@Component +@Service +@Transactional @RequiredArgsConstructor public class UserUseCase { private final GetUserQuery getUserCommand; private final UpdateExistingUserCommand updateExistingUserCommand; + private final UserRepository userRepository; public User getUserById(Long userId) { return getUserCommand.handle(userId); @@ -23,4 +27,12 @@ public User updateUser(Long userId, String name, String nickname, String phoneNu return updateExistingUserCommand.handle(userId, name, nickname, phoneNumber, birthDate); } + public void deleteUser(Long userId) { + // User 존재 여부 확인 + User user = userRepository.findById(userId); + + // User와 연관된 모든 데이터 삭제 + userRepository.deleteUserWithRelatedData(userId); + } + } diff --git a/src/main/java/backend/airo/domain/user/repository/UserRepository.java b/src/main/java/backend/airo/domain/user/repository/UserRepository.java index be0a843..35d9fa0 100644 --- a/src/main/java/backend/airo/domain/user/repository/UserRepository.java +++ b/src/main/java/backend/airo/domain/user/repository/UserRepository.java @@ -13,4 +13,6 @@ public interface UserRepository extends AggregateSupport { Optional findByEmail(String email); + // User와 연관된 모든 데이터를 함께 삭제 + void deleteUserWithRelatedData(Long userId); } diff --git a/src/main/java/backend/airo/persistence/comment/repository/CommentJpaRepository.java b/src/main/java/backend/airo/persistence/comment/repository/CommentJpaRepository.java index e7c7e55..3c2b399 100644 --- a/src/main/java/backend/airo/persistence/comment/repository/CommentJpaRepository.java +++ b/src/main/java/backend/airo/persistence/comment/repository/CommentJpaRepository.java @@ -11,4 +11,6 @@ public interface CommentJpaRepository extends JpaRepository Long countByPostId(Long postId); + void deleteByUserId(Long userId); + } diff --git a/src/main/java/backend/airo/persistence/image/repository/ImageJpaRepository.java b/src/main/java/backend/airo/persistence/image/repository/ImageJpaRepository.java index b887eea..001ffa5 100644 --- a/src/main/java/backend/airo/persistence/image/repository/ImageJpaRepository.java +++ b/src/main/java/backend/airo/persistence/image/repository/ImageJpaRepository.java @@ -28,6 +28,7 @@ public interface ImageJpaRepository extends JpaRepository { // Delete void deleteByPostId(Long postId); + void deleteByUserId(Long userId); boolean existsByPostId(Long postId); diff --git a/src/main/java/backend/airo/persistence/point/repository/PointJpaRepository.java b/src/main/java/backend/airo/persistence/point/repository/PointJpaRepository.java index 91184d1..d6a6407 100644 --- a/src/main/java/backend/airo/persistence/point/repository/PointJpaRepository.java +++ b/src/main/java/backend/airo/persistence/point/repository/PointJpaRepository.java @@ -17,4 +17,6 @@ INSERT INTO user_point (user_id, point_score) void upsertIncrement(@Param("userId") Long userId, @Param("delta") long delta); PointEntity findByUserId(Long userId); + + void deleteByUserId(Long userId); } diff --git a/src/main/java/backend/airo/persistence/point_history/repository/PointHistoryJpaRepository.java b/src/main/java/backend/airo/persistence/point_history/repository/PointHistoryJpaRepository.java index 7fa271c..5b72b9f 100644 --- a/src/main/java/backend/airo/persistence/point_history/repository/PointHistoryJpaRepository.java +++ b/src/main/java/backend/airo/persistence/point_history/repository/PointHistoryJpaRepository.java @@ -12,4 +12,6 @@ public interface PointHistoryJpaRepository extends JpaRepository findByStatusAndIdLessThanOrderByIdDesc( @Query("SELECT CASE WHEN COUNT(p) > 0 THEN true ELSE false END FROM PostEntity p WHERE p.id < :postId AND p.status = 'PUBLISHED'") boolean existsByIdLessThan(@Param("postId") Long postId); + + void deleteByUserId(Long userId); } diff --git a/src/main/java/backend/airo/persistence/post/repository/PostLikeJpaRepository.java b/src/main/java/backend/airo/persistence/post/repository/PostLikeJpaRepository.java index 2ef7760..aea0835 100644 --- a/src/main/java/backend/airo/persistence/post/repository/PostLikeJpaRepository.java +++ b/src/main/java/backend/airo/persistence/post/repository/PostLikeJpaRepository.java @@ -17,4 +17,6 @@ INSERT IGNORE INTO post_like(post_id, user_id) int deleteByPostIdAndUserId(Long postId, Long userId); + void deleteByUserId(Long userId); + } diff --git a/src/main/java/backend/airo/persistence/user/adapter/UserAdapter.java b/src/main/java/backend/airo/persistence/user/adapter/UserAdapter.java index 0762b58..6c128a5 100644 --- a/src/main/java/backend/airo/persistence/user/adapter/UserAdapter.java +++ b/src/main/java/backend/airo/persistence/user/adapter/UserAdapter.java @@ -3,8 +3,15 @@ import backend.airo.domain.user.User; import backend.airo.domain.user.enums.ProviderType; import backend.airo.domain.user.repository.UserRepository; +import backend.airo.persistence.post.repository.PostLikeJpaRepository; import backend.airo.persistence.user.entity.UserEntity; import backend.airo.persistence.user.repository.UserJpaRepository; +import backend.airo.persistence.post.repository.PostJpaRepository; + +import backend.airo.persistence.comment.repository.CommentJpaRepository; +import backend.airo.persistence.image.repository.ImageJpaRepository; +import backend.airo.persistence.point.repository.PointJpaRepository; +import backend.airo.persistence.point_history.repository.PointHistoryJpaRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -20,6 +27,14 @@ public class UserAdapter implements UserRepository { private final UserJpaRepository userJpaRepository; + + // 연관 엔티티들의 JpaRepository 주입 + private final PostJpaRepository postJpaRepository; + private final PostLikeJpaRepository postLikeJpaRepository; + private final CommentJpaRepository commentJpaRepository; + private final ImageJpaRepository imageJpaRepository; + private final PointJpaRepository pointJpaRepository; + private final PointHistoryJpaRepository pointHistoryJpaRepository; @Override public User save(User aggregate) { @@ -65,4 +80,27 @@ public Optional findByEmail(String email) { } + @Override + public void deleteUserWithRelatedData(Long userId) { + // 1. 연관된 이미지 삭제 + imageJpaRepository.deleteByUserId(userId); + + // 2. 연관된 댓글 삭제 + commentJpaRepository.deleteByUserId(userId); + + // 3. 연관된 게시글 좋아요 삭제 + postLikeJpaRepository.deleteByUserId(userId); + + // 4. 연관된 포인트 히스토리 삭제 + pointHistoryJpaRepository.deleteByUserId(userId); + + // 5. 연관된 포인트 삭제 + pointJpaRepository.deleteByUserId(userId); + + // 6. 연관된 게시글 삭제 + postJpaRepository.deleteByUserId(userId); + + // 7. 마지막으로 User 삭제 + userJpaRepository.deleteById(userId); + } }