diff --git a/src/main/java/com/ecolink/core/bookmark/repository/BookmarkRepository.java b/src/main/java/com/ecolink/core/bookmark/repository/BookmarkRepository.java index 5b8c8e1f..358cc943 100644 --- a/src/main/java/com/ecolink/core/bookmark/repository/BookmarkRepository.java +++ b/src/main/java/com/ecolink/core/bookmark/repository/BookmarkRepository.java @@ -1,5 +1,6 @@ package com.ecolink.core.bookmark.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,7 +14,10 @@ public interface BookmarkRepository extends JpaRepository { + "where b.avatar.id = :avatarId " + "and b.store.id = :storeId") boolean existsByAvatarAndStore(@Param("avatarId") Long avatarId, @Param("storeId") Long storeId); + Optional findBookmarkByAvatarIdAndStoreId(Long avatarId, Long storeId); + + List findAllByAvatarId(Long avatarId); } diff --git a/src/main/java/com/ecolink/core/common/config/scheduler/SchedulerConfig.java b/src/main/java/com/ecolink/core/common/config/scheduler/SchedulerConfig.java new file mode 100644 index 00000000..f97eaed4 --- /dev/null +++ b/src/main/java/com/ecolink/core/common/config/scheduler/SchedulerConfig.java @@ -0,0 +1,9 @@ +package com.ecolink.core.common.config.scheduler; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@Configuration +public class SchedulerConfig { +} diff --git a/src/main/java/com/ecolink/core/common/error/ErrorCode.java b/src/main/java/com/ecolink/core/common/error/ErrorCode.java index bc50f204..676c1eb1 100644 --- a/src/main/java/com/ecolink/core/common/error/ErrorCode.java +++ b/src/main/java/com/ecolink/core/common/error/ErrorCode.java @@ -30,6 +30,7 @@ public enum ErrorCode { APPLICATION_IS_PENDING(HttpStatus.BAD_REQUEST, "M-001", "해당 유저는 대표 신청 후 승인 대기중 상태입니다."), MANAGER_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "M-002", "주어진 유저로 매니저를 찾을 수 없습니다."), NOT_MANAGER_OF_STORE(HttpStatus.FORBIDDEN, "M-003", "해당 매장을 관리할 수 있는 권한이 없습니다."), + MANAGER_WITHDRAWAL_DENIED(HttpStatus.FORBIDDEN, "M-004", "매니저는 회원 탈퇴 기능을 쓸 수 없습니다."), /** * 상점 관련 오류 diff --git a/src/main/java/com/ecolink/core/like/repository/ReviewLikeRepository.java b/src/main/java/com/ecolink/core/like/repository/ReviewLikeRepository.java index 804993e9..9d3d21c1 100644 --- a/src/main/java/com/ecolink/core/like/repository/ReviewLikeRepository.java +++ b/src/main/java/com/ecolink/core/like/repository/ReviewLikeRepository.java @@ -18,4 +18,7 @@ public interface ReviewLikeRepository extends JpaRepository { List findAllByReviewList(@Param("reviewIds") List reviewIds, @Param("avatarId") Long avatarId); + @Query("select l from ReviewLike l " + + "where l.avatar.id = :avatarId") + List findAllByAvatarId(@Param("avatarId") Long avatarId); } diff --git a/src/main/java/com/ecolink/core/review/repository/ReviewRepository.java b/src/main/java/com/ecolink/core/review/repository/ReviewRepository.java index ad8a6d8e..81e18a56 100644 --- a/src/main/java/com/ecolink/core/review/repository/ReviewRepository.java +++ b/src/main/java/com/ecolink/core/review/repository/ReviewRepository.java @@ -1,5 +1,7 @@ package com.ecolink.core.review.repository; +import java.util.List; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -26,4 +28,8 @@ public interface ReviewRepository extends JpaRepository { Page findByStoreAndAvatar(@Param("storeId") Long storeId, @Param("avatarId") Long avatarId, Pageable pageable); + @Query("select r from Review r " + + "where r.writer.id = :writerId") + List findAllByAvatarId(@Param("writerId") Long writerId); + } diff --git a/src/main/java/com/ecolink/core/review/repository/ReviewTagRepository.java b/src/main/java/com/ecolink/core/review/repository/ReviewTagRepository.java index 022630a4..2ea39bd6 100644 --- a/src/main/java/com/ecolink/core/review/repository/ReviewTagRepository.java +++ b/src/main/java/com/ecolink/core/review/repository/ReviewTagRepository.java @@ -1,8 +1,16 @@ package com.ecolink.core.review.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.ecolink.core.review.domain.ReviewTag; public interface ReviewTagRepository extends JpaRepository { + @Query("select rt " + + "from ReviewTag rt " + + "where rt.review.writer.id = :writerId") + List findAllByAvatarId(@Param("writerId") Long writerId); } diff --git a/src/main/java/com/ecolink/core/user/constant/RoleType.java b/src/main/java/com/ecolink/core/user/constant/RoleType.java index 86ceaa96..bbeaf1fb 100644 --- a/src/main/java/com/ecolink/core/user/constant/RoleType.java +++ b/src/main/java/com/ecolink/core/user/constant/RoleType.java @@ -8,7 +8,8 @@ public enum RoleType { GUEST("ROLE_GUEST"), USER("ROLE_USER"), MANAGER("ROLE_MANAGER"), - ADMIN("ROLE_ADMIN"); + ADMIN("ROLE_ADMIN"), + WITHDRAWN_USER("ROLE_WITHDRAWN_USER"); private final String authority; diff --git a/src/main/java/com/ecolink/core/user/controller/UserController.java b/src/main/java/com/ecolink/core/user/controller/UserController.java new file mode 100644 index 00000000..fbc580e4 --- /dev/null +++ b/src/main/java/com/ecolink/core/user/controller/UserController.java @@ -0,0 +1,36 @@ +package com.ecolink.core.user.controller; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.ecolink.core.auth.token.UserPrincipal; +import com.ecolink.core.common.response.ApiResponse; +import com.ecolink.core.user.service.WithdrawService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +@RequestMapping("${api.prefix}/users") +public class UserController { + + private final WithdrawService withdrawService; + + @Tag(name = "${swagger.tag.account}") + @Operation(summary = "회원 탈퇴 API - 인증 필요", + description = "회원 탈퇴 - 인증 필요", + security = {@SecurityRequirement(name = "session-token")}) + @PreAuthorize("hasRole('USER')") + @DeleteMapping("/withdraw") + public ApiResponse withdraw( + @AuthenticationPrincipal UserPrincipal principal) { + withdrawService.withdraw(principal.getUserId()); + return ApiResponse.ok(); + } +} diff --git a/src/main/java/com/ecolink/core/user/domain/User.java b/src/main/java/com/ecolink/core/user/domain/User.java index 76f29335..8c6ca81d 100644 --- a/src/main/java/com/ecolink/core/user/domain/User.java +++ b/src/main/java/com/ecolink/core/user/domain/User.java @@ -138,4 +138,9 @@ public void addAvatar(Avatar avatar) { this.avatars.add(avatar); } + public void withdraw(Role role) { + this.withdrawn = true; + this.withdrawnDate = LocalDateTime.now(); + this.role = role; + } } diff --git a/src/main/java/com/ecolink/core/user/repository/UserRepository.java b/src/main/java/com/ecolink/core/user/repository/UserRepository.java index e541ee8b..d42fc56a 100644 --- a/src/main/java/com/ecolink/core/user/repository/UserRepository.java +++ b/src/main/java/com/ecolink/core/user/repository/UserRepository.java @@ -1,5 +1,6 @@ package com.ecolink.core.user.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -23,4 +24,8 @@ public interface UserRepository extends JpaRepository { + "where u.id = :id") Optional findUserGraphById(@Param("id") Long id); + @Query("select u from User u " + + "where u.withdrawn = true") + List findAllByWithdrawn(); + } diff --git a/src/main/java/com/ecolink/core/user/service/RoleService.java b/src/main/java/com/ecolink/core/user/service/RoleService.java index a375cd9d..8cd78895 100644 --- a/src/main/java/com/ecolink/core/user/service/RoleService.java +++ b/src/main/java/com/ecolink/core/user/service/RoleService.java @@ -21,4 +21,10 @@ public Role getUserRole() { return roleRepository.findByType(RoleType.USER) .orElseGet(() -> roleRepository.save(new Role(RoleType.USER))); } + + @Transactional + public Role getWithdrawnUserRole() { + return roleRepository.findByType(RoleType.WITHDRAWN_USER) + .orElseGet(() -> roleRepository.save(new Role(RoleType.WITHDRAWN_USER))); + } } diff --git a/src/main/java/com/ecolink/core/user/service/UserScheduleService.java b/src/main/java/com/ecolink/core/user/service/UserScheduleService.java new file mode 100644 index 00000000..daa6ee29 --- /dev/null +++ b/src/main/java/com/ecolink/core/user/service/UserScheduleService.java @@ -0,0 +1,32 @@ +package com.ecolink.core.user.service; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.ecolink.core.user.domain.User; +import com.ecolink.core.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class UserScheduleService { + + private final UserRepository userRepository; + private final WithdrawService withdrawService; + + @Transactional + @Scheduled(cron = "0 0 5 * * *") + public void delete() { + List withdrawnUsers = userRepository.findAllByWithdrawn(); + withdrawnUsers.stream() + .filter(user -> Duration.between(user.getWithdrawnDate(), LocalDateTime.now()).getSeconds() >= 604800) + .forEach(withdrawService::delete); + } +} diff --git a/src/main/java/com/ecolink/core/user/service/WithdrawService.java b/src/main/java/com/ecolink/core/user/service/WithdrawService.java new file mode 100644 index 00000000..0d7b660a --- /dev/null +++ b/src/main/java/com/ecolink/core/user/service/WithdrawService.java @@ -0,0 +1,57 @@ +package com.ecolink.core.user.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.ecolink.core.avatar.repository.AvatarRepository; +import com.ecolink.core.bookmark.repository.BookmarkRepository; +import com.ecolink.core.common.error.ErrorCode; +import com.ecolink.core.common.error.exception.ManagerForbiddenException; +import com.ecolink.core.like.repository.ReviewLikeRepository; +import com.ecolink.core.review.repository.ReviewRepository; +import com.ecolink.core.review.repository.ReviewTagRepository; +import com.ecolink.core.store.repository.SearchHistoryRepository; +import com.ecolink.core.user.constant.RoleType; +import com.ecolink.core.user.domain.User; +import com.ecolink.core.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class WithdrawService { + + private final UserRepository userRepository; + private final AvatarRepository avatarRepository; + private final ReviewRepository reviewRepository; + private final ReviewLikeRepository reviewLikeRepository; + private final ReviewTagRepository reviewTagRepository; + private final BookmarkRepository bookmarkRepository; + private final SearchHistoryRepository searchHistoryRepository; + private final UserService userService; + private final RoleService roleService; + + @Transactional + public void withdraw(Long userId) { + User user = userService.getUserGraphById(userId); + if (user.getRole().getType().equals(RoleType.MANAGER)) { + throw new ManagerForbiddenException(ErrorCode.MANAGER_WITHDRAWAL_DENIED); + } + user.withdraw(roleService.getWithdrawnUserRole()); + } + + @Transactional + public void delete(User user) { + user.getAvatars().forEach(avatar -> { + reviewTagRepository.deleteAllInBatch(reviewTagRepository.findAllByAvatarId(avatar.getId())); + reviewLikeRepository.deleteAllInBatch(reviewLikeRepository.findAllByAvatarId(avatar.getId())); + reviewRepository.deleteAllInBatch(reviewRepository.findAllByAvatarId(avatar.getId())); + bookmarkRepository.deleteAllInBatch(bookmarkRepository.findAllByAvatarId(avatar.getId())); + searchHistoryRepository.deleteAllInBatch(searchHistoryRepository.findAllByAvatarId(avatar.getId())); + }); + + avatarRepository.deleteAllInBatch(user.getAvatars()); + userRepository.delete(user); + } +}