From 6bc266f366747db80c649d28f45c47af42623a39 Mon Sep 17 00:00:00 2001 From: myqewr Date: Wed, 25 Jun 2025 21:36:06 +0900 Subject: [PATCH] =?UTF-8?q?feature=20:=20=ED=83=88=ED=87=B4=ED=95=9C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EA=B3=84=EC=A0=95=20=EB=B3=B5=EA=B5=AC=20?= =?UTF-8?q?API(#139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/docs/asciidoc/auth-api.adoc | 1 - src/docs/asciidoc/user-api.adoc | 44 ++++-- .../RecoverUserAccountController.java | 31 +++++ .../request/RecoverUserAccountRequest.java | 19 +++ .../response/RecoverUserAccountResponse.java | 15 ++ .../post/PostDomainPersistenceAdapter.java | 7 + .../repository/PostRepository.java | 3 + .../user/RecoverUserAccountCommand.java | 14 ++ .../in/user/RecoverUserAccountUseCase.java | 11 ++ .../out/persistence/post/LoadPostPort.java | 3 + .../application/query/FindByUserIdsQuery.java | 16 +++ .../user/GeneralUserSignupService.java | 3 +- .../user/RecoverUserAccountService.java | 59 ++++++++ .../service/user/UserExitService.java | 19 --- .../user/UserHardDeleteByEmailService.java | 32 ++++- .../service/user/UserHardDeleteService.java | 24 +++- .../vo/user/RecoverUserAccountVo.java | 14 ++ .../response/enums/ErrorResponseCode.java | 1 + .../security/SecurityConfig.java | 3 +- .../server/user/RecoverUserAccountTest.java | 131 ++++++++++++++++++ 21 files changed, 412 insertions(+), 39 deletions(-) create mode 100644 src/main/java/com/ftm/server/adapter/in/web/user/controller/RecoverUserAccountController.java create mode 100644 src/main/java/com/ftm/server/adapter/in/web/user/dto/request/RecoverUserAccountRequest.java create mode 100644 src/main/java/com/ftm/server/adapter/in/web/user/dto/response/RecoverUserAccountResponse.java create mode 100644 src/main/java/com/ftm/server/application/command/user/RecoverUserAccountCommand.java create mode 100644 src/main/java/com/ftm/server/application/port/in/user/RecoverUserAccountUseCase.java create mode 100644 src/main/java/com/ftm/server/application/query/FindByUserIdsQuery.java create mode 100644 src/main/java/com/ftm/server/application/service/user/RecoverUserAccountService.java create mode 100644 src/main/java/com/ftm/server/application/vo/user/RecoverUserAccountVo.java create mode 100644 src/test/java/com/ftm/server/user/RecoverUserAccountTest.java diff --git a/.gitignore b/.gitignore index 78e875a..1be5283 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ HELP.md .gradle +.history build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ diff --git a/src/docs/asciidoc/auth-api.adoc b/src/docs/asciidoc/auth-api.adoc index 744c062..fd7634b 100644 --- a/src/docs/asciidoc/auth-api.adoc +++ b/src/docs/asciidoc/auth-api.adoc @@ -86,4 +86,3 @@ include::{snippetsDir}/sessionValidityCheck/2/http-response.adoc[] ==== Response Body Fields include::{snippetsDir}/sessionValidityCheck/1/response-fields.adoc[] - diff --git a/src/docs/asciidoc/user-api.adoc b/src/docs/asciidoc/user-api.adoc index 381dc33..c868e4b 100644 --- a/src/docs/asciidoc/user-api.adoc +++ b/src/docs/asciidoc/user-api.adoc @@ -40,6 +40,7 @@ include::{snippetsDir}/emailAuthentication/3/http-response.adoc[] --- +[[email-code-verification]] === **3. 이메일 인증 코드 검증 api** 이메일 인증용 코드를 검증하는 api입니다. @@ -58,7 +59,30 @@ include::{snippetsDir}/emailCodeVerification/1/response-fields.adoc[] --- -=== **4. 회원가입시 필요한 정보 목록 조회 api** + +=== **4. 회원 계정 복구 API** + +탈퇴한 회원의 계정 복구 api 입니다. 별도의 인증 없이 요청이 가능합니다. +<>에서 isRecoverable 필드가 true인 경우 요청 가능합니다. + +==== Request +include::{snippetsDir}/recoverUserAccount/1/http-request.adoc[] + +==== Request Body Fields +include::{snippetsDir}/recoverUserAccount/1/request-fields.adoc[] + +==== 성공 Response +성공 1. 유효한 경우 +include::{snippetsDir}/recoverUserAccount/1/http-response.adoc[] + +성공 2. 유효하지 않은 경우 +include::{snippetsDir}/recoverUserAccount/2/http-response.adoc[] + +==== Response Body Fields +include::{snippetsDir}/recoverUserAccount/1/response-fields.adoc[] + + +=== **5. 회원가입시 필요한 정보 목록 조회 api** 회원가입시 사용자에게 입력 받는 연령대 정보와 관심 해시태그 정보 옵션들을 조회합니다. @@ -74,7 +98,7 @@ include::{snippetsDir}/userSignupOptions/1/response-fields.adoc[] --- -=== **5. 일반 회원가입 api** +=== **6. 일반 회원가입 api** 일반 회원가입 api입니다. @@ -97,7 +121,7 @@ include::{snippetsDir}/generalUserSignUp/2/http-response.adoc[] include::{snippetsDir}/generalUserSignUp/3/http-response.adoc[] -=== **6. 소셜 회원가입 api** +=== **7. 소셜 회원가입 api** 소셜 회원가입 api입니다. @@ -124,7 +148,7 @@ include::{snippetsDir}/socialUserSignUp/2/http-response.adoc[] include::{snippetsDir}/socialUserSignUp/3/http-response.adoc[] -=== **7. 사용자 정보 간단 조회 api** +=== **8. 사용자 정보 간단 조회 api** 사용자 정보 수정 시 노출되는 정보를 제공 @@ -138,7 +162,7 @@ include::{snippetsDir}/userSimpleInfo/1/http-response.adoc[] include::{snippetsDir}/userSimpleInfo/1/response-fields.adoc[] -=== **8. 사용자 정보 수정 api** +=== **9. 사용자 정보 수정 api** 사용자 정보를 수정합니다. 수정이 필요한 항목만 수정을 요청해 주세요. @@ -164,7 +188,7 @@ include::{snippetsDir}/userInfoUpdate/2/http-response.adoc[] include::{snippetsDir}/userInfoUpdate/3/http-response.adoc[] -=== **9. 회원 탈퇴 api** +=== **10. 회원 탈퇴 api** 회원 탈퇴를 진행 @@ -178,7 +202,7 @@ include::{snippetsDir}/userExit/1/http-response.adoc[] include::{snippetsDir}/userExit/1/response-fields.adoc[] -=== **10. 북마크 생성 api** +=== **11. 북마크 생성 api** 게시글 북마크를 생성 @@ -203,7 +227,7 @@ include::{snippetsDir}/createBookmark/1/response-fields.adoc[] include::{snippetsDir}/createBookmark/3/http-response.adoc[] -=== **11. 내가 작성한 유저픽 게시글 목록 조회** +=== **12. 내가 작성한 유저픽 게시글 목록 조회** 마이페이지 > 작성한 게시글 목록 > 더보기 버튼 클릭 시 내가 작성한 유저픽 게시글 목록을 조회하는 api 입니다. + 무한 스크롤, 더보기 형식으로 조회하여 모든 데이터 개수와 번호를 부여하는 Page 방식이 아닌, + @@ -236,7 +260,7 @@ include::{snippetsDir}/loadMyPosts/3/http-response.adoc[] include::{snippetsDir}/loadMyPosts/4/http-response.adoc[] -=== **12. 내가 북마크한 유저픽 게시글 목록 조회** +=== **13. 내가 북마크한 유저픽 게시글 목록 조회** 마이페이지 > 북마크한 게시글 목록 > 더보기 버튼 클릭 시 내가 작성한 유저픽 게시글 목록을 조회하는 api 입니다. + 무한 스크롤, 더보기 형식으로 조회하여 모든 데이터 개수와 번호를 부여하는 Page 방식이 아닌, + @@ -268,7 +292,7 @@ include::{snippetsDir}/loadMyBookmarkPosts/3/http-response.adoc[] include::{snippetsDir}/loadMyBookmarkPosts/4/http-response.adoc[] -=== **13. 북마크 삭제 api** +=== **14. 북마크 삭제 api** 사용자가 등록한 북마크를 삭제합니다. diff --git a/src/main/java/com/ftm/server/adapter/in/web/user/controller/RecoverUserAccountController.java b/src/main/java/com/ftm/server/adapter/in/web/user/controller/RecoverUserAccountController.java new file mode 100644 index 0000000..a208d9f --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/user/controller/RecoverUserAccountController.java @@ -0,0 +1,31 @@ +package com.ftm.server.adapter.in.web.user.controller; + +import com.ftm.server.adapter.in.web.user.dto.request.RecoverUserAccountRequest; +import com.ftm.server.adapter.in.web.user.dto.response.RecoverUserAccountResponse; +import com.ftm.server.application.command.user.RecoverUserAccountCommand; +import com.ftm.server.application.port.in.user.RecoverUserAccountUseCase; +import com.ftm.server.common.response.ApiResponse; +import com.ftm.server.common.response.enums.SuccessResponseCode; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class RecoverUserAccountController { + + private final RecoverUserAccountUseCase recoverUserAccountUseCase; + + @PostMapping("/api/users/me/recover") + public ResponseEntity recoverUserAccount( + @Valid @RequestBody RecoverUserAccountRequest request) { + RecoverUserAccountResponse response = + RecoverUserAccountResponse.from( + recoverUserAccountUseCase.execute( + RecoverUserAccountCommand.of(request.getEmail()))); + return ResponseEntity.ok(ApiResponse.success(SuccessResponseCode.OK, response)); + } +} diff --git a/src/main/java/com/ftm/server/adapter/in/web/user/dto/request/RecoverUserAccountRequest.java b/src/main/java/com/ftm/server/adapter/in/web/user/dto/request/RecoverUserAccountRequest.java new file mode 100644 index 0000000..475d19b --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/user/dto/request/RecoverUserAccountRequest.java @@ -0,0 +1,19 @@ +package com.ftm.server.adapter.in.web.user.dto.request; + +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class RecoverUserAccountRequest { + + @Pattern( + regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", + message = "이메일 형식이 올바르지 않습니다.") + private final String email; + + public static RecoverUserAccountRequest of(String email) { + return new RecoverUserAccountRequest(email); + } +} diff --git a/src/main/java/com/ftm/server/adapter/in/web/user/dto/response/RecoverUserAccountResponse.java b/src/main/java/com/ftm/server/adapter/in/web/user/dto/response/RecoverUserAccountResponse.java new file mode 100644 index 0000000..701aa2a --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/user/dto/response/RecoverUserAccountResponse.java @@ -0,0 +1,15 @@ +package com.ftm.server.adapter.in.web.user.dto.response; + +import com.ftm.server.application.vo.user.RecoverUserAccountVo; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class RecoverUserAccountResponse { + private final Long userId; + + public static RecoverUserAccountResponse from(RecoverUserAccountVo vo) { + return new RecoverUserAccountResponse(vo.getUserId()); + } +} diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java b/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java index b3320ee..dd57aaf 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java @@ -307,4 +307,11 @@ public List loadRepresentativeImagesByPostIds(FindByIdsQuery query) { .map(postImageMapper::toDomainEntity) .toList(); } + + @Override + public List loadPostListByUsers(FindByUserIdsQuery query) { + return postRepository.findAllByUserIdIn(query.getUserIds()).stream() + .map(postMapper::toDomainEntity) + .toList(); + } } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostRepository.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostRepository.java index ca6d23b..b188a15 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostRepository.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostRepository.java @@ -19,4 +19,7 @@ public interface PostRepository void deleteAllByIdInBatch(@Param("postIds") List postIds); boolean existsById(Long id); + + @Query("SELECT p FROM PostJpaEntity p WHERE p.user.id IN (:userIds)") + List findAllByUserIdIn(@Param("userIds") List userIds); } diff --git a/src/main/java/com/ftm/server/application/command/user/RecoverUserAccountCommand.java b/src/main/java/com/ftm/server/application/command/user/RecoverUserAccountCommand.java new file mode 100644 index 0000000..1312a03 --- /dev/null +++ b/src/main/java/com/ftm/server/application/command/user/RecoverUserAccountCommand.java @@ -0,0 +1,14 @@ +package com.ftm.server.application.command.user; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class RecoverUserAccountCommand { + private String email; + + public static RecoverUserAccountCommand of(String email) { + return new RecoverUserAccountCommand(email); + } +} diff --git a/src/main/java/com/ftm/server/application/port/in/user/RecoverUserAccountUseCase.java b/src/main/java/com/ftm/server/application/port/in/user/RecoverUserAccountUseCase.java new file mode 100644 index 0000000..f8b2986 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/in/user/RecoverUserAccountUseCase.java @@ -0,0 +1,11 @@ +package com.ftm.server.application.port.in.user; + +import com.ftm.server.application.command.user.RecoverUserAccountCommand; +import com.ftm.server.application.vo.user.RecoverUserAccountVo; +import com.ftm.server.common.annotation.UseCase; + +@UseCase +public interface RecoverUserAccountUseCase { + + RecoverUserAccountVo execute(RecoverUserAccountCommand command); +} diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java index 9f219af..c59b145 100644 --- a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java +++ b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java @@ -1,6 +1,7 @@ package com.ftm.server.application.port.out.persistence.post; import com.ftm.server.application.query.FindByIdQuery; +import com.ftm.server.application.query.FindByUserIdsQuery; import com.ftm.server.application.query.FindPostByDeleteOptionQuery; import com.ftm.server.common.annotation.Port; import com.ftm.server.domain.entity.Post; @@ -12,5 +13,7 @@ public interface LoadPostPort { Optional loadPost(FindByIdQuery query); + List loadPostListByUsers(FindByUserIdsQuery query); + List loadPostsByDeleteOption(FindPostByDeleteOptionQuery query); } diff --git a/src/main/java/com/ftm/server/application/query/FindByUserIdsQuery.java b/src/main/java/com/ftm/server/application/query/FindByUserIdsQuery.java new file mode 100644 index 0000000..425ddb3 --- /dev/null +++ b/src/main/java/com/ftm/server/application/query/FindByUserIdsQuery.java @@ -0,0 +1,16 @@ +package com.ftm.server.application.query; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class FindByUserIdsQuery { + + private final List userIds; + + public static FindByUserIdsQuery of(List userIds) { + return new FindByUserIdsQuery(userIds); + } +} diff --git a/src/main/java/com/ftm/server/application/service/user/GeneralUserSignupService.java b/src/main/java/com/ftm/server/application/service/user/GeneralUserSignupService.java index ada92dc..9d85536 100644 --- a/src/main/java/com/ftm/server/application/service/user/GeneralUserSignupService.java +++ b/src/main/java/com/ftm/server/application/service/user/GeneralUserSignupService.java @@ -55,7 +55,8 @@ public GeneralUserSignupResponse execute(GeneralUserSignupCommand command) { throw new CustomException(ErrorResponseCode.USER_ALREADY_EXISTS); } - if (emailVerificationLogs.isEmpty()) { // 이메일 인증이 완료되지 않음. + if (emailVerificationLogs.isEmpty() + || !emailVerificationLogs.get().getIsVerified()) { // 이메일 인증이 완료되지 않음. throw new CustomException(ErrorResponseCode.EMAIL_NOT_VERIFIED); } diff --git a/src/main/java/com/ftm/server/application/service/user/RecoverUserAccountService.java b/src/main/java/com/ftm/server/application/service/user/RecoverUserAccountService.java new file mode 100644 index 0000000..b1e3f26 --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/user/RecoverUserAccountService.java @@ -0,0 +1,59 @@ +package com.ftm.server.application.service.user; + +import com.ftm.server.application.command.user.RecoverUserAccountCommand; +import com.ftm.server.application.port.in.user.RecoverUserAccountUseCase; +import com.ftm.server.application.port.out.persistence.user.LoadEmailVerificationLogPort; +import com.ftm.server.application.port.out.persistence.user.LoadUserPort; +import com.ftm.server.application.port.out.persistence.user.UpdateUserPort; +import com.ftm.server.application.query.FindByEmailQuery; +import com.ftm.server.application.vo.user.RecoverUserAccountVo; +import com.ftm.server.common.exception.CustomException; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import com.ftm.server.domain.entity.EmailVerificationLogs; +import com.ftm.server.domain.entity.User; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +@Slf4j +public class RecoverUserAccountService implements RecoverUserAccountUseCase { + + private final LoadUserPort loadUserPort; + private final UpdateUserPort updateUserPort; + + private final LoadEmailVerificationLogPort loadEmailVerificationLogPort; + + @Override + public RecoverUserAccountVo execute(RecoverUserAccountCommand command) { + + // 1. 이메일 인증 마쳤는지 검사 + Optional optionalEmailVerificationLogs = + loadEmailVerificationLogPort.loadEmailVerificationLogByEmail( + FindByEmailQuery.of(command.getEmail())); + // 검증 진행 전이거나, 검증이 완료되지 않은 경우 복구 불가 + if (optionalEmailVerificationLogs.isEmpty() + || !optionalEmailVerificationLogs.get().getIsVerified()) { + throw new CustomException(ErrorResponseCode.RECOVERING_IS_NOT_AVAILABLE); + } + + // 2. soft delete 된 user 조회 + Optional optionalUser = + loadUserPort.loadDeletedUserByEmail(FindByEmailQuery.of(command.getEmail())); + // 삭제되지 않은 회원에 대해 복구 시도하는 경우 복구 불가 + if (optionalUser.isEmpty()) { + throw new CustomException(ErrorResponseCode.RECOVERING_IS_NOT_AVAILABLE); + } + + // 3. soft delete된 user 복구 + User user = optionalUser.get(); + user.updateIsDeleted(false); + user.updateDeletedAt(null); + + updateUserPort.updateUser(user); + + return RecoverUserAccountVo.of(user.getId()); + } +} diff --git a/src/main/java/com/ftm/server/application/service/user/UserExitService.java b/src/main/java/com/ftm/server/application/service/user/UserExitService.java index fe5d94f..957535f 100644 --- a/src/main/java/com/ftm/server/application/service/user/UserExitService.java +++ b/src/main/java/com/ftm/server/application/service/user/UserExitService.java @@ -2,18 +2,12 @@ import com.ftm.server.application.command.user.DeleteUserByIdCommand; import com.ftm.server.application.port.in.user.UserExitUseCase; -import com.ftm.server.application.port.out.persistence.user.LoadPostUserDomainPort; import com.ftm.server.application.port.out.persistence.user.LoadUserPort; -import com.ftm.server.application.port.out.persistence.user.UpdatePostUserDomainPort; import com.ftm.server.application.port.out.persistence.user.UpdateUserPort; import com.ftm.server.application.query.FindByUserIdQuery; -import com.ftm.server.application.query.FindUserByRoleQuery; -import com.ftm.server.domain.entity.Post; import com.ftm.server.domain.entity.User; -import com.ftm.server.domain.enums.UserRole; import jakarta.transaction.Transactional; import java.time.LocalDateTime; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -24,10 +18,8 @@ public class UserExitService implements UserExitUseCase { private final LoadUserPort loadUserPort; - private final LoadPostUserDomainPort loadPostPort; private final UpdateUserPort updateUserPort; - private final UpdatePostUserDomainPort updatePostPort; @Transactional @Override @@ -38,16 +30,5 @@ public void execute(DeleteUserByIdCommand query) { user.updateIsDeleted(true); user.updateDeletedAt(LocalDateTime.now()); updateUserPort.updateUser(user); - - // user가 쓴 게시글의 작성자를 익명 사용자로 변경 - List postList = loadPostPort.loadPostListByUser(FindByUserIdQuery.of(user.getId())); - if (!postList.isEmpty()) { - // 익명 사용자 조회 - User systemUser = loadUserPort.loadUserByRole(FindUserByRoleQuery.of(UserRole.SYSTEM)); - Long systemUserId = systemUser.getId(); - // post update - postList.forEach(p -> p.updateUserId(systemUserId)); - updatePostPort.updatePostListBySystemUser(postList); - } } } diff --git a/src/main/java/com/ftm/server/application/service/user/UserHardDeleteByEmailService.java b/src/main/java/com/ftm/server/application/service/user/UserHardDeleteByEmailService.java index 6b60a92..3ebc419 100644 --- a/src/main/java/com/ftm/server/application/service/user/UserHardDeleteByEmailService.java +++ b/src/main/java/com/ftm/server/application/service/user/UserHardDeleteByEmailService.java @@ -6,7 +6,11 @@ import com.ftm.server.application.port.out.s3.S3ImageDeletePort; import com.ftm.server.application.port.out.transcation.AfterCommitExecutorPort; import com.ftm.server.application.query.FindByEmailQuery; +import com.ftm.server.application.query.FindByUserIdQuery; +import com.ftm.server.application.query.FindUserByRoleQuery; +import com.ftm.server.domain.entity.Post; import com.ftm.server.domain.entity.User; +import com.ftm.server.domain.enums.UserRole; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -17,6 +21,9 @@ public class UserHardDeleteByEmailService implements UserHardDeleteByEmailUseCase { private final LoadUserPort loadUserPort; + private final LoadPostUserDomainPort loadPostPort; + + private final UpdatePostUserDomainPort updatePostPort; private final DeleteBookmarkPort deleteBookmarkPort; private final DeleteGroomingTestResultPort deleteGroomingTestResultPort; @@ -36,23 +43,36 @@ public void execute(DeleteUserByEmailCommand command) { return; } - List userId = List.of(deletedUser.get().getId()); + Long userId = deletedUser.get().getId(); + List userIds = List.of(userId); // user 관련 엔티티 모두 삭제 // 1. 북마크 삭제 - deleteBookmarkPort.deleteBookmarkByUserList(DeleteBookmarkByUserIdCommand.of(userId)); + deleteBookmarkPort.deleteBookmarkByUserList(DeleteBookmarkByUserIdCommand.of(userIds)); // 2. 그루밍 결과 삭제 deleteGroomingTestResultPort.deleteGroomingTestResultByUserList( - DeleteGroomingTestResultByUserIdCommand.of(userId)); + DeleteGroomingTestResultByUserIdCommand.of(userIds)); // 3. user 이미지 삭제 List imageKeyList = deleteUserImagePort.deleteUserImageByUserList( - DeleteUserImageByUserIdCommand.of(userId)); + DeleteUserImageByUserIdCommand.of(userIds)); afterCommitExecutorPort.doAfterCommit( () -> s3ImageDeletePort.deleteImages( imageKeyList)); // transaction commit 이후에 s3에 이미지 삭제 요청 - // 4. user 삭제 - deleteUserPort.deleteAllUserByIdList(DeleteAllUserByIdListCommand.of(userId)); + + // 4. user가 쓴 게시글의 작성자를 익명 사용자로 변경 + List postList = loadPostPort.loadPostListByUser(FindByUserIdQuery.of(userId)); + if (!postList.isEmpty()) { + // 익명 사용자 조회 + User systemUser = loadUserPort.loadUserByRole(FindUserByRoleQuery.of(UserRole.SYSTEM)); + Long systemUserId = systemUser.getId(); + // post update + postList.forEach(p -> p.updateUserId(systemUserId)); + updatePostPort.updatePostListBySystemUser(postList); + } + + // 5. user 삭제 + deleteUserPort.deleteAllUserByIdList(DeleteAllUserByIdListCommand.of(userIds)); } } diff --git a/src/main/java/com/ftm/server/application/service/user/UserHardDeleteService.java b/src/main/java/com/ftm/server/application/service/user/UserHardDeleteService.java index 98ca818..ef1eafc 100644 --- a/src/main/java/com/ftm/server/application/service/user/UserHardDeleteService.java +++ b/src/main/java/com/ftm/server/application/service/user/UserHardDeleteService.java @@ -5,11 +5,16 @@ import com.ftm.server.application.command.user.DeleteGroomingTestResultByUserIdCommand; import com.ftm.server.application.command.user.DeleteUserImageByUserIdCommand; import com.ftm.server.application.port.in.user.UserHardDeleteUseCase; +import com.ftm.server.application.port.out.persistence.post.LoadPostPort; import com.ftm.server.application.port.out.persistence.user.*; import com.ftm.server.application.port.out.s3.S3ImageDeletePort; import com.ftm.server.application.port.out.transcation.AfterCommitExecutorPort; +import com.ftm.server.application.query.FindByUserIdsQuery; import com.ftm.server.application.query.FindUserByDeleteOptionQuery; +import com.ftm.server.application.query.FindUserByRoleQuery; +import com.ftm.server.domain.entity.Post; import com.ftm.server.domain.entity.User; +import com.ftm.server.domain.enums.UserRole; import jakarta.transaction.Transactional; import java.time.LocalDate; import java.util.List; @@ -23,6 +28,7 @@ public class UserHardDeleteService implements UserHardDeleteUseCase { private final LoadUserPort loadUserPort; + private final LoadPostPort loadPostPort; private final DeleteBookmarkPort deleteBookmarkPort; private final DeleteGroomingTestResultPort deleteGroomingTestResultPort; @@ -32,6 +38,8 @@ public class UserHardDeleteService implements UserHardDeleteUseCase { private final AfterCommitExecutorPort afterCommitExecutorPort; private final S3ImageDeletePort s3ImageDeletePort; + private final UpdatePostUserDomainPort updatePostPort; + @Override @Transactional public void execute() { @@ -60,7 +68,21 @@ public void execute() { () -> s3ImageDeletePort.deleteImages( imageKeyList)); // transaction commit 이후에 s3에 이미지 삭제 요청 - // 4. user 삭제 + + // 4. user가 쓴 게시글의 작성자를 익명 사용자로 변경 + List postList = + loadPostPort.loadPostListByUsers(FindByUserIdsQuery.of(deletedUserIdList)); + if (!postList.isEmpty()) { + // 익명 사용자 조회 + User systemUser = loadUserPort.loadUserByRole(FindUserByRoleQuery.of(UserRole.SYSTEM)); + Long systemUserId = systemUser.getId(); + + // post update + postList.forEach(p -> p.updateUserId(systemUserId)); + updatePostPort.updatePostListBySystemUser(postList); + } + + // 5. user 삭제 deleteUserPort.deleteAllUserByIdList(DeleteAllUserByIdListCommand.of(deletedUserIdList)); } } diff --git a/src/main/java/com/ftm/server/application/vo/user/RecoverUserAccountVo.java b/src/main/java/com/ftm/server/application/vo/user/RecoverUserAccountVo.java new file mode 100644 index 0000000..49f5432 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/user/RecoverUserAccountVo.java @@ -0,0 +1,14 @@ +package com.ftm.server.application.vo.user; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class RecoverUserAccountVo { + private final Long userId; + + public static RecoverUserAccountVo of(Long userId) { + return new RecoverUserAccountVo(userId); + } +} diff --git a/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java b/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java index 8d61eed..f13ac22 100644 --- a/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java +++ b/src/main/java/com/ftm/server/common/response/enums/ErrorResponseCode.java @@ -62,6 +62,7 @@ public enum ErrorResponseCode { PASSWORD_NOT_MATCHED(HttpStatus.CONFLICT, "E409_002", "비밀번호가 일치하지 않습니다."), EXCEED_NUMBER_OF_TRIAL(HttpStatus.CONFLICT, "E409_003", "시도 가능 횟수를 초과했습니다. 잠시 후에 다시 시도 해 주세요."), EMAIL_NOT_VERIFIED(HttpStatus.CONFLICT, "E409_004", "이메일 인증이 완료되지 않았습니다."), + RECOVERING_IS_NOT_AVAILABLE(HttpStatus.CONFLICT, "E409_005", "복구할 수 없는 계정입니다."), // 500번 UNKNOWN_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E500_001", "알 수 없는 서버 에러가 발생했습니다."), diff --git a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java index ade2a16..30c4dac 100644 --- a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java +++ b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java @@ -68,7 +68,8 @@ public class SecurityConfig { "/api/users", "/api/users/social", "/api/grooming/tests/submission", - "/api/grooming/tests" + "/api/grooming/tests", + "api/users/me/recover" }; private static final String[] ANONYMOUS_MATCHERS = {"/docs/**"}; diff --git a/src/test/java/com/ftm/server/user/RecoverUserAccountTest.java b/src/test/java/com/ftm/server/user/RecoverUserAccountTest.java new file mode 100644 index 0000000..30e6aa0 --- /dev/null +++ b/src/test/java/com/ftm/server/user/RecoverUserAccountTest.java @@ -0,0 +1,131 @@ +package com.ftm.server.user; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.ftm.server.BaseTest; +import com.ftm.server.adapter.in.web.user.dto.request.RecoverUserAccountRequest; +import com.ftm.server.adapter.out.persistence.mapper.EmailVerificationLogsMapper; +import com.ftm.server.adapter.out.persistence.repository.EmailVerificationLogsRepository; +import com.ftm.server.application.command.user.EmailVerificationLogCreationCommand; +import com.ftm.server.application.port.out.persistence.user.UpdateUserPort; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import com.ftm.server.domain.entity.EmailVerificationLogs; +import com.ftm.server.domain.entity.User; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.snippet.Attributes; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +public class RecoverUserAccountTest extends BaseTest { + + @Autowired private UpdateUserPort updateUserPort; + @Autowired private EmailVerificationLogsRepository emailVerificationLogsRepository; + @Autowired private EmailVerificationLogsMapper emailVerificationLogsMapper; + + private final List requestFieldDescriptors = + List.of( + fieldWithPath("email") + .type(STRING) + .description("복구할 계정의 email") + .attributes(new Attributes.Attribute("constraint", "email 형식"))); + + private final List responseFieldDescriptors = + List.of( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("응답 상태"), + fieldWithPath("code").type(JsonFieldType.STRING).description("상태 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("메시지"), + fieldWithPath("data").type(JsonFieldType.OBJECT).optional().description("data"), + fieldWithPath("data.userId") + .type(JsonFieldType.NUMBER) + .description("사용자 고유 id")); + + private ResultActions getResultActions(RecoverUserAccountRequest request) throws Exception { + return mockMvc.perform( // api 실행 + RestDocumentationRequestBuilders.post("/api/users/me/recover") + .contentType(MediaType.APPLICATION_JSON) // request body content type + .content(mapper.writeValueAsString(request))); + } + + // 문서화 반환 함수 + private RestDocumentationResultHandler getDocument(Integer identifier) { + return document( + "recoverUserAccount/" + identifier, + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint(), getModifiedHeader()), + responseFields(responseFieldDescriptors), + requestFields(requestFieldDescriptors), + resource( + ResourceSnippetParameters.builder() + .tag("회원") + .summary("회원 계정 복구 api") + .description("회원 계정 복구 api입니다.") + .responseFields(responseFieldDescriptors) + .requestFields(requestFieldDescriptors) + .build())); + } + + @Test + @DisplayName("회원 계정 복구 성공") + @Transactional + void test1() throws Exception { + // given + String email = "test@example.com"; + RecoverUserAccountRequest request = new RecoverUserAccountRequest(email); + + // 1. 이메일 검증 로그 생성 및 검증 완료 처리 + EmailVerificationLogs emailVerificationLogs = + EmailVerificationLogs.from(EmailVerificationLogCreationCommand.of(email, "123456")); + emailVerificationLogs.updateVerificationStatus(true); + emailVerificationLogsRepository.save( + emailVerificationLogsMapper.toJpaEntity(emailVerificationLogs)); + + // 2. 사용자 생성 후 soft delete + User user = createTestUser(email, "password123!"); + user.updateIsDeleted(true); + updateUserPort.updateUser(user); + + // when & then + getResultActions(request) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.userId").exists()) + .andDo(getDocument(1)); + } + + @Test + @DisplayName("회원 계정 복구 실패 - 복구 불가능한 상태") + @Transactional + void test2() throws Exception { + // given + String email = "invalid@example.com"; + RecoverUserAccountRequest request = new RecoverUserAccountRequest(email); + + User user = createTestUser(email, "password123!"); + + // when & then + getResultActions(request) + .andExpect( + status().is( + ErrorResponseCode.RECOVERING_IS_NOT_AVAILABLE + .getHttpStatus() + .value())) + .andExpect( + jsonPath("$.code") + .value(ErrorResponseCode.RECOVERING_IS_NOT_AVAILABLE.getCode())) + .andDo(getDocument(2)); + } +}