From 74b5690339977bd1740a3f4cd4a5a93f3ece5447 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Tue, 16 Jan 2024 04:51:16 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=83=88=ED=87=B4?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20(#155)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 회원 탈퇴 로직 구현 * feat: @kdomo 리뷰 반영 * fix: 마지막 로그인 시간 테스트 코드 삭제 --- .../domain/auth/api/AuthController.java | 11 +----- .../domain/auth/application/AuthService.java | 18 +++++---- .../com/depromeet/domain/member/api/.gitkeep | 0 .../domain/member/api/MemberController.java | 39 +++++++++++++++++++ .../domain/member/application/.gitkeep | 0 .../member/application/MemberService.java | 37 ++++++++++++++++++ .../domain/member/domain/Member.java | 15 ++++++- .../global/error/exception/ErrorCode.java | 2 + .../domain/member/domain/MemberTest.java | 14 ------- 9 files changed, 102 insertions(+), 34 deletions(-) delete mode 100644 src/main/java/com/depromeet/domain/member/api/.gitkeep create mode 100644 src/main/java/com/depromeet/domain/member/api/MemberController.java delete mode 100644 src/main/java/com/depromeet/domain/member/application/.gitkeep create mode 100644 src/main/java/com/depromeet/domain/member/application/MemberService.java diff --git a/src/main/java/com/depromeet/domain/auth/api/AuthController.java b/src/main/java/com/depromeet/domain/auth/api/AuthController.java index 50724aae8..a8ae5539d 100644 --- a/src/main/java/com/depromeet/domain/auth/api/AuthController.java +++ b/src/main/java/com/depromeet/domain/auth/api/AuthController.java @@ -2,7 +2,6 @@ import com.depromeet.domain.auth.application.AuthService; import com.depromeet.domain.auth.dto.request.MemberRegisterRequest; -import com.depromeet.domain.auth.dto.request.UsernameCheckRequest; import com.depromeet.domain.auth.dto.request.UsernamePasswordRequest; import com.depromeet.domain.auth.dto.response.TokenPairResponse; import io.swagger.v3.oas.annotations.Operation; @@ -16,7 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "1. [인증]", description = "인증 관련 API") +@Tag(name = "1-1. [인증]", description = "인증 관련 API") @RestController @RequestMapping("/auth") @RequiredArgsConstructor @@ -39,14 +38,6 @@ public ResponseEntity memberTempRegister( return ResponseEntity.status(HttpStatus.CREATED).body(response); } - @Operation(summary = "아이디 중복 체크", description = "아이디 중복 체크를 진행합니다.") - @PostMapping("/check-username") - public ResponseEntity memberUsernameCheck( - @Valid @RequestBody UsernameCheckRequest request) { - authService.checkUsername(request); - return ResponseEntity.ok().build(); - } - @Operation(summary = "로그인", description = "토큰 발급을 위해 로그인을 진행합니다.") @PostMapping("/login") public ResponseEntity memberLogin( diff --git a/src/main/java/com/depromeet/domain/auth/application/AuthService.java b/src/main/java/com/depromeet/domain/auth/application/AuthService.java index 0eedc0718..d91bbbd1f 100644 --- a/src/main/java/com/depromeet/domain/auth/application/AuthService.java +++ b/src/main/java/com/depromeet/domain/auth/application/AuthService.java @@ -1,12 +1,12 @@ package com.depromeet.domain.auth.application; import com.depromeet.domain.auth.dto.request.MemberRegisterRequest; -import com.depromeet.domain.auth.dto.request.UsernameCheckRequest; import com.depromeet.domain.auth.dto.request.UsernamePasswordRequest; import com.depromeet.domain.auth.dto.response.TokenPairResponse; import com.depromeet.domain.member.dao.MemberRepository; import com.depromeet.domain.member.domain.Member; import com.depromeet.domain.member.domain.MemberRole; +import com.depromeet.domain.member.domain.MemberStatus; import com.depromeet.global.error.exception.CustomException; import com.depromeet.global.error.exception.ErrorCode; import com.depromeet.global.util.MemberUtil; @@ -56,6 +56,9 @@ public TokenPairResponse loginMember(UsernamePasswordRequest request) { validateNotGuestMember(member); validatePasswordMatches(member, request.password()); + validateNormalMember(member); + + member.updateLastLoginAt(); return getLoginResponse(member); } @@ -66,6 +69,12 @@ private void validateNotGuestMember(Member member) { } } + private void validateNormalMember(Member member) { + if (member.getStatus() != MemberStatus.NORMAL) { + throw new CustomException(ErrorCode.MEMBER_INVALID_NORMAL); + } + } + private void validatePasswordMatches(Member member, String password) { if (!passwordEncoder.matches(password, member.getPassword())) { throw new CustomException(ErrorCode.PASSWORD_NOT_MATCHES); @@ -78,11 +87,4 @@ private TokenPairResponse getLoginResponse(Member member) { return TokenPairResponse.from(accessToken, refreshToken); } - - @Transactional(readOnly = true) - public void checkUsername(UsernameCheckRequest request) { - if (memberRepository.existsByUsername(request.username())) { - throw new CustomException(ErrorCode.MEMBER_ALREADY_REGISTERED); - } - } } diff --git a/src/main/java/com/depromeet/domain/member/api/.gitkeep b/src/main/java/com/depromeet/domain/member/api/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/main/java/com/depromeet/domain/member/api/MemberController.java b/src/main/java/com/depromeet/domain/member/api/MemberController.java new file mode 100644 index 000000000..e6c44bba3 --- /dev/null +++ b/src/main/java/com/depromeet/domain/member/api/MemberController.java @@ -0,0 +1,39 @@ +package com.depromeet.domain.member.api; + +import com.depromeet.domain.auth.dto.request.UsernameCheckRequest; +import com.depromeet.domain.member.application.MemberService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "1-2. [회원]", description = "회원 관련 API") +@RestController +@RequestMapping("/members") +@RequiredArgsConstructor +public class MemberController { + + private final MemberService memberService; + + @Operation(summary = "아이디 중복 체크", description = "아이디 중복 체크를 진행합니다.") + @PostMapping("/check-username") + public ResponseEntity memberUsernameCheck( + @Valid @RequestBody UsernameCheckRequest request) { + memberService.checkUsername(request); + return ResponseEntity.ok().build(); + } + + // TODO: 테스트 코드 작성 필요 + @Operation(summary = "회원 탈퇴", description = "회원탈퇴를 진행합니다.") + @DeleteMapping("/withdrawal") + public ResponseEntity memberWithdrawal(@Valid @RequestBody UsernameCheckRequest request) { + memberService.withdrawal(request); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/depromeet/domain/member/application/.gitkeep b/src/main/java/com/depromeet/domain/member/application/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/main/java/com/depromeet/domain/member/application/MemberService.java b/src/main/java/com/depromeet/domain/member/application/MemberService.java new file mode 100644 index 000000000..f5be3294d --- /dev/null +++ b/src/main/java/com/depromeet/domain/member/application/MemberService.java @@ -0,0 +1,37 @@ +package com.depromeet.domain.member.application; + +import com.depromeet.domain.auth.dao.RefreshTokenRepository; +import com.depromeet.domain.auth.dto.request.UsernameCheckRequest; +import com.depromeet.domain.member.dao.MemberRepository; +import com.depromeet.domain.member.domain.Member; +import com.depromeet.global.error.exception.CustomException; +import com.depromeet.global.error.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class MemberService { + + private final MemberRepository memberRepository; + private final RefreshTokenRepository refreshTokenRepository; + + @Transactional(readOnly = true) + public void checkUsername(UsernameCheckRequest request) { + if (memberRepository.existsByUsername(request.username())) { + throw new CustomException(ErrorCode.MEMBER_ALREADY_REGISTERED); + } + } + + public void withdrawal(UsernameCheckRequest request) { + final Member member = + memberRepository + .findByUsername(request.username()) + .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); + + refreshTokenRepository.deleteById(member.getId()); + member.withdrawal(); + } +} diff --git a/src/main/java/com/depromeet/domain/member/domain/Member.java b/src/main/java/com/depromeet/domain/member/domain/Member.java index facc7a8af..cbd204c9b 100644 --- a/src/main/java/com/depromeet/domain/member/domain/Member.java +++ b/src/main/java/com/depromeet/domain/member/domain/Member.java @@ -103,8 +103,12 @@ public static Member createNormalMember(Profile profile) { .build(); } - public void updateLastLoginAt(LocalDateTime lastLoginAt) { - this.lastLoginAt = lastLoginAt; + public void updateMemberStatus(MemberStatus memberStatus) { + this.status = memberStatus; + } + + public void updateLastLoginAt() { + this.lastLoginAt = LocalDateTime.now(); } public void register(String nickname) { @@ -115,6 +119,13 @@ public void register(String nickname) { this.role = MemberRole.USER; } + public void withdrawal() { + if (this.status == MemberStatus.DELETED) { + throw new CustomException(ErrorCode.MEMBER_ALREADY_DELETED); + } + this.status = MemberStatus.DELETED; + } + private void validateRegisterAvailable() { if (role != MemberRole.GUEST) { throw new CustomException(ErrorCode.MEMBER_ALREADY_REGISTERED); diff --git a/src/main/java/com/depromeet/global/error/exception/ErrorCode.java b/src/main/java/com/depromeet/global/error/exception/ErrorCode.java index 1b3d0841b..7e1530842 100644 --- a/src/main/java/com/depromeet/global/error/exception/ErrorCode.java +++ b/src/main/java/com/depromeet/global/error/exception/ErrorCode.java @@ -16,6 +16,7 @@ public enum ErrorCode { // Member MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원을 찾을 수 없습니다."), + MEMBER_INVALID_NORMAL(HttpStatus.FORBIDDEN, "일반 회원이 아닙니다."), // Security AUTH_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "시큐리티 인증 정보를 찾을 수 없습니다."), @@ -24,6 +25,7 @@ public enum ErrorCode { INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 엑세스 토큰입니다."), INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 리프레시 토큰입니다."), MEMBER_ALREADY_REGISTERED(HttpStatus.CONFLICT, "이미 가입된 회원입니다."), + MEMBER_ALREADY_DELETED(HttpStatus.NOT_FOUND, "이미 탈퇴한 회원입니다."), SOCIAL_AUTHENTICATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류로 인해 소셜 로그인에 실패했습니다."), INVALID_APPLE_PRIVATE_KEY(HttpStatus.INTERNAL_SERVER_ERROR, "애플 로그인에 필요한 비밀 키가 올바르지 않습니다."), PASSWORD_NOT_MATCHES(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다."), diff --git a/src/test/java/com/depromeet/domain/member/domain/MemberTest.java b/src/test/java/com/depromeet/domain/member/domain/MemberTest.java index 43195515c..cf46dc1be 100644 --- a/src/test/java/com/depromeet/domain/member/domain/MemberTest.java +++ b/src/test/java/com/depromeet/domain/member/domain/MemberTest.java @@ -5,7 +5,6 @@ import com.depromeet.global.error.exception.CustomException; import com.depromeet.global.error.exception.ErrorCode; -import java.time.LocalDateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -55,19 +54,6 @@ void setUp() { assertEquals(MemberVisibility.PUBLIC, visibility); } - @Test - void 마지막_로그인_시간을_업데이트한다() { - // given - Member member = Member.createNormalMember(profile); - LocalDateTime lastLoginAt = LocalDateTime.of(2024, 1, 10, 0, 0); - - // when - member.updateLastLoginAt(lastLoginAt); - - // then - assertEquals(lastLoginAt, member.getLastLoginAt()); - } - @Test void 회원가입시_게스트멤버의_닉네임이_설정된다() { // given