From f2c1b17f301c00341492c57a9dd25d4f210a1e97 Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Tue, 14 Nov 2023 21:46:59 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=ED=83=88=ED=87=B4=EB=90=9C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EA=B6=8C=ED=95=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/io/oduck/api/domain/member/entity/Role.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/oduck/api/domain/member/entity/Role.java b/src/main/java/io/oduck/api/domain/member/entity/Role.java index 7945f8e3..ed23e4f3 100644 --- a/src/main/java/io/oduck/api/domain/member/entity/Role.java +++ b/src/main/java/io/oduck/api/domain/member/entity/Role.java @@ -1,5 +1,5 @@ package io.oduck.api.domain.member.entity; public enum Role { - ADMIN, MEMBER, GUEST + ADMIN, MEMBER, GUEST, WITHDRAWAL } From ebe388a3ab094463dca180df03a2e851d59be3e4 Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Tue, 14 Nov 2023 21:48:25 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EB=94=9C=EB=A6=AC=ED=8A=B8=20=EB=A9=94=EC=86=8C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oduck/api/domain/member/entity/Member.java | 16 ++++++++++++++++ .../api/domain/member/entity/MemberProfile.java | 7 +++++-- .../global/security/auth/entity/AuthLocal.java | 4 +++- .../global/security/auth/entity/AuthSocial.java | 4 +++- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/oduck/api/domain/member/entity/Member.java b/src/main/java/io/oduck/api/domain/member/entity/Member.java index 9c00054c..174e539d 100644 --- a/src/main/java/io/oduck/api/domain/member/entity/Member.java +++ b/src/main/java/io/oduck/api/domain/member/entity/Member.java @@ -10,6 +10,7 @@ import io.oduck.api.global.security.auth.entity.AuthSocial; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; +import java.time.LocalDateTime; import java.util.List; import jakarta.persistence.CascadeType; @@ -96,4 +97,19 @@ public void relateMemberProfile(MemberProfile memberProfile) { memberProfile.relateMember(this); } } + + public void delete() { + this.deletedAt = LocalDateTime.now(); + this.memberProfile.delete(); + + if(this.authLocal != null) { + this.authLocal.delete(); + } + + if (this.authSocial != null) { + this.authSocial.delete(); + } + + this.role = Role.WITHDRAWAL; + } } diff --git a/src/main/java/io/oduck/api/domain/member/entity/MemberProfile.java b/src/main/java/io/oduck/api/domain/member/entity/MemberProfile.java index d2f775f3..b970c3db 100644 --- a/src/main/java/io/oduck/api/domain/member/entity/MemberProfile.java +++ b/src/main/java/io/oduck/api/domain/member/entity/MemberProfile.java @@ -3,14 +3,13 @@ import io.oduck.api.global.audit.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; +import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -71,4 +70,8 @@ public void updateName(String name) { public void updateInfo(String info) { this.info = info; } + + public void delete() { + this.deletedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/io/oduck/api/global/security/auth/entity/AuthLocal.java b/src/main/java/io/oduck/api/global/security/auth/entity/AuthLocal.java index c9472b66..cc7e5299 100644 --- a/src/main/java/io/oduck/api/global/security/auth/entity/AuthLocal.java +++ b/src/main/java/io/oduck/api/global/security/auth/entity/AuthLocal.java @@ -4,12 +4,12 @@ import io.oduck.api.global.audit.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; +import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -43,4 +43,6 @@ public AuthLocal(Member member, String email, String password) { public void relateMember(Member member) { this.member = member; } + + public void delete() { this.deletedAt = LocalDateTime.now(); } } diff --git a/src/main/java/io/oduck/api/global/security/auth/entity/AuthSocial.java b/src/main/java/io/oduck/api/global/security/auth/entity/AuthSocial.java index 11ddd86a..4fa4d330 100644 --- a/src/main/java/io/oduck/api/global/security/auth/entity/AuthSocial.java +++ b/src/main/java/io/oduck/api/global/security/auth/entity/AuthSocial.java @@ -6,12 +6,12 @@ import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; +import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -50,4 +50,6 @@ public AuthSocial(Long id, Member member, String email, String socialId, SocialT public void relateMember(Member member) { this.member = member; } + + public void delete() { this.deletedAt = LocalDateTime.now(); } } From a8d98575e489c94c7797f4e8c7e72e4d34bb1732 Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Tue, 14 Nov 2023 21:49:15 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=20=EC=83=81=ED=83=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=EC=8B=9C=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oduck/api/global/security/auth/service/AuthService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/oduck/api/global/security/auth/service/AuthService.java b/src/main/java/io/oduck/api/global/security/auth/service/AuthService.java index ff818205..2b5da816 100644 --- a/src/main/java/io/oduck/api/global/security/auth/service/AuthService.java +++ b/src/main/java/io/oduck/api/global/security/auth/service/AuthService.java @@ -42,7 +42,7 @@ public Status getStatus(AuthUser user) { .point(memberProfile.getPoint()) .build(); - if (user.getRole().equals("ADMIN")) { + if (user.getRole().equals("ADMIN") || user.getRole().equals("WITHDRAWAL")) { status = AdminStatus.builder() .status(status) .role(user.getRole()) @@ -74,7 +74,7 @@ public void login(LocalAuthDto localAuthDto) { } private String extractPasswordIfAdmin(Role role, String password) { - if (role.equals(Role.MEMBER)) return password; + if (!role.equals(Role.ADMIN)) return password; LocalTime now = LocalTime.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HHmm"); From a37c3e8172d3686b7e7697792a04315eba74fb83 Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Tue, 14 Nov 2023 21:51:42 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EC=82=AD=EC=A0=9C=EB=90=9C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EC=A0=9C=EC=99=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oduck/api/domain/member/repository/MemberRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/oduck/api/domain/member/repository/MemberRepository.java b/src/main/java/io/oduck/api/domain/member/repository/MemberRepository.java index 63dd92a0..28e99a55 100644 --- a/src/main/java/io/oduck/api/domain/member/repository/MemberRepository.java +++ b/src/main/java/io/oduck/api/domain/member/repository/MemberRepository.java @@ -1,10 +1,11 @@ package io.oduck.api.domain.member.repository; import io.oduck.api.domain.member.entity.Member; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface MemberRepository extends JpaRepository, MemberRepositoryCustom{ - + Optional findByIdAndDeletedAtIsNull(Long id); } From 5708ddab51b83ce4fdc6128f136394b2a5afab4f Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Tue, 14 Nov 2023 21:51:56 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20#104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/domain/member/service/MemberService.java | 3 +++ .../domain/member/service/MemberServiceImpl.java | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/io/oduck/api/domain/member/service/MemberService.java b/src/main/java/io/oduck/api/domain/member/service/MemberService.java index d4d136d8..22508d33 100644 --- a/src/main/java/io/oduck/api/domain/member/service/MemberService.java +++ b/src/main/java/io/oduck/api/domain/member/service/MemberService.java @@ -13,4 +13,7 @@ public interface MemberService { // 회원 정보 수정 로직 void updateProfile(PatchReq body, Long memberId); + + // 회원 탈퇴 로직 + void withdrawMember(Long memberId); } diff --git a/src/main/java/io/oduck/api/domain/member/service/MemberServiceImpl.java b/src/main/java/io/oduck/api/domain/member/service/MemberServiceImpl.java index 6c63b07b..e30e6c10 100644 --- a/src/main/java/io/oduck/api/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/io/oduck/api/domain/member/service/MemberServiceImpl.java @@ -125,6 +125,21 @@ public void updateProfile(PatchReq body, Long memberId) { memberProfileRepository.save(memberProfile); } + @Override + @Transactional + public void withdrawMember(Long memberId) { + Member member = getMemberById(memberId); + member.delete(); + memberRepository.save(member); + } + + private Member getMemberById(Long memberId) { + return memberRepository.findByIdAndDeletedAtIsNull(memberId) + .orElseThrow( + () -> new NotFoundException("Member") + ); + } + private MemberProfile getProfileByMemberId(Long memberId) { return memberProfileRepository.findByMemberId(memberId) .orElseThrow( From 6486fa5c913e2926f8ec5013ec4c05ff04b32443 Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Tue, 14 Nov 2023 21:52:29 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=ED=83=88=ED=87=B4=EB=90=9C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EC=A0=9C=EC=99=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oduck/api/domain/member/repository/MemberRepositoryImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/io/oduck/api/domain/member/repository/MemberRepositoryImpl.java b/src/main/java/io/oduck/api/domain/member/repository/MemberRepositoryImpl.java index 8263ca58..0b2eeaf7 100644 --- a/src/main/java/io/oduck/api/domain/member/repository/MemberRepositoryImpl.java +++ b/src/main/java/io/oduck/api/domain/member/repository/MemberRepositoryImpl.java @@ -37,6 +37,7 @@ public Optional selectProfileByName(String name) { ) .from(memberProfile) .where(memberProfile.name.eq(name)) + .where(memberProfile.deletedAt.isNull()) .fetchOne()); } From ec8c183889a0f37f8ee3c3735a73047898e2c6fe Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Tue, 14 Nov 2023 21:52:41 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/oduck/api/domain/member/controller/MemberController.java b/src/main/java/io/oduck/api/domain/member/controller/MemberController.java index 10c04422..e2e4e25f 100644 --- a/src/main/java/io/oduck/api/domain/member/controller/MemberController.java +++ b/src/main/java/io/oduck/api/domain/member/controller/MemberController.java @@ -19,6 +19,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -59,17 +60,6 @@ public ResponseEntity getProfileByName( return ResponseEntity.ok(res); } - // 회원 프로필 수정 - @PatchMapping - public ResponseEntity patchProfile( - @RequestBody @Valid PatchReq body, - @LoginUser AuthUser user - ) { - // TODO: 회원 정보 수정 로직 구현 - memberService.updateProfile(body, user.getId()); - - return ResponseEntity.noContent().build(); - } @GetMapping("/{id}/bookmarks") public ResponseEntity getBookmaks( @@ -104,4 +94,25 @@ public ResponseEntity getShoertReviewsCount( ) { return ResponseEntity.ok(shortReviewService.getShortReviewCountByMemberId(id)); } + + // 회원 프로필 수정 + @PatchMapping + public ResponseEntity patchProfile( + @RequestBody @Valid PatchReq body, + @LoginUser AuthUser user + ) { + // TODO: 회원 정보 수정 로직 구현 + memberService.updateProfile(body, user.getId()); + + return ResponseEntity.noContent().build(); + } + + // 회원 탈퇴 + @DeleteMapping() + public ResponseEntity deleteMember( + @LoginUser AuthUser user + ) { + memberService.withdrawMember(user.getId()); + return ResponseEntity.noContent().build(); + } } From 5e7aeae19fca39d4b8a4e05e808da355a3a0a50d Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Tue, 14 Nov 2023 21:52:56 +0900 Subject: [PATCH 08/10] =?UTF-8?q?test:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20=EB=8D=94?= =?UTF-8?q?=EB=AF=B8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20#104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/db/data.sql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/resources/db/data.sql b/src/test/resources/db/data.sql index 2e5dfae3..e70e8e7a 100644 --- a/src/test/resources/db/data.sql +++ b/src/test/resources/db/data.sql @@ -10,6 +10,10 @@ INSERT INTO member(created_at, updated_at, login_type, `role`) VALUES('2023-10-1 INSERT INTO auth_local(created_at, member_id, updated_at, password, email) VALUES('2023-10-12 21:05:31.859', 3, '2023-10-12 21:05:31.859', '{bcrypt}$2a$10$C0G6uQz.MzfsSH7BZFRBz.MPBmFSV2zAloqqBIwaUpxnmMgCQK..i', 'david@gmail.com'); INSERT INTO member_profile (created_at, member_id, updated_at, name, info, thumbnail) VALUES('2023-10-12 21:05:31.859', 3, '2023-10-12 21:05:31.859', 'david', 'david info', 'http://thumbnail.com'); +INSERT INTO member(created_at, updated_at, login_type, `role`) VALUES('2023-10-13 21:05:31.859', '2023-10-13 21:05:31.859', 'LOCAL', 'MEMBER'); +INSERT INTO auth_local(created_at, member_id, updated_at, password, email) VALUES('2023-10-13 21:05:31.859', 4, '2023-10-13 21:05:31.859', '{bcrypt}$2a$10$C0G6uQz.MzfsSH7BZFRBz.MPBmFSV2zAloqqBIwaUpxnmMgCQK..i', 'reina@gmail.com'); +INSERT INTO member_profile (created_at, member_id, updated_at, name, info, thumbnail) VALUES('2023-10-13 21:05:31.859', 4, '2023-10-13 21:05:31.859', 'reina', 'reina info', 'http://thumbnail.com'); + INSERT INTO series(created_at, title, updated_at) VALUES('2023-10-10 21:05:31.859', '귀멸의 칼날', '2023-10-10 21:05:31.859'); INSERT INTO series(created_at, title, updated_at) VALUES('2023-10-10 21:05:31.859', '원피스', '2023-10-10 21:05:31.859'); INSERT INTO series(created_at, title, updated_at) VALUES('2023-10-10 21:05:31.859', '나루토', '2023-10-10 21:05:31.859'); From 0888700817c92c6dd51ac07a9ca7ca5dee576b7c Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Tue, 14 Nov 2023 21:53:05 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20#104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/service/MemberServiceTest.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/test/java/io/oduck/api/unit/member/service/MemberServiceTest.java b/src/test/java/io/oduck/api/unit/member/service/MemberServiceTest.java index c877b044..5f8aa40b 100644 --- a/src/test/java/io/oduck/api/unit/member/service/MemberServiceTest.java +++ b/src/test/java/io/oduck/api/unit/member/service/MemberServiceTest.java @@ -167,4 +167,34 @@ void updateProfileFailureWhenSameNameAsAlreadyExist() { } // TODO: 회원 북마크 애니 목록 + + // 회원 탈퇴 + @DisplayName("회원 탈퇴") + @Nested + class DeleteMember { + @DisplayName("회원 탈퇴 성공") + @Test + void deleteMemberSuccess() { + // given + Member member = new MemberStub().getMember(); + given(memberRepository.findByIdAndDeletedAtIsNull(anyLong())).willReturn(Optional.ofNullable(member)); + + // when + // then + assertDoesNotThrow(() -> memberService.withdrawMember(1L)); + } + + @DisplayName("회원 탈퇴 실패. 존재하지 않는 회원") + @Test + void deleteMemberFailureWhenNotExist() { + // given + given(memberRepository.findByIdAndDeletedAtIsNull(anyLong())).willReturn(Optional.empty()); + + // when + // then + assertThrows(NotFoundException.class, + () -> memberService.withdrawMember(1L) + ); + } + } } From 059c2be479a6ab09f7ebfe9d0a3457cfb6cd813f Mon Sep 17 00:00:00 2001 From: FaberJoo Date: Tue, 14 Nov 2023 21:53:17 +0900 Subject: [PATCH 10/10] =?UTF-8?q?test:=20=ED=9A=8C=EC=9B=A5=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?#104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/e2e/member/MemberControllerTest.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/java/io/oduck/api/e2e/member/MemberControllerTest.java b/src/test/java/io/oduck/api/e2e/member/MemberControllerTest.java index 12732bb5..02c4cb79 100644 --- a/src/test/java/io/oduck/api/e2e/member/MemberControllerTest.java +++ b/src/test/java/io/oduck/api/e2e/member/MemberControllerTest.java @@ -710,4 +710,33 @@ void getReviewCountSuccess() throws Exception { ); } } + + @DisplayName("회원 탈퇴") + @Nested + class DeleteWithdrawal { + @DisplayName("회원 탈퇴 성공시 204 NoContent 응답") + @Test + @WithCustomMockMember(id = 4L, email = "reina", password = "Qwer!234", role = Role.MEMBER) + void deleteWithdrawalSuccess() throws Exception { + // given + + // when + ResultActions actions = mockMvc.perform( + delete(BASE_URL) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.COOKIE, "oDuckio.sid={SESSION_VALUE}") + ); + + // then + actions + .andExpect(status().isNoContent()) + .andDo( + document("deleteWithdrawal/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()) + ) + ); + } + } }