Skip to content

Commit

Permalink
feat: 회원 탈퇴 로직 구현 (#155)
Browse files Browse the repository at this point in the history
* feat: 회원 탈퇴 로직 구현

* feat: @kdomo 리뷰 반영

* fix: 마지막 로그인 시간 테스트 코드 삭제
  • Loading branch information
char-yb authored Jan 15, 2024
1 parent e1c4126 commit 74b5690
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 34 deletions.
11 changes: 1 addition & 10 deletions src/main/java/com/depromeet/domain/auth/api/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -39,14 +38,6 @@ public ResponseEntity<TokenPairResponse> memberTempRegister(
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@Operation(summary = "아이디 중복 체크", description = "아이디 중복 체크를 진행합니다.")
@PostMapping("/check-username")
public ResponseEntity<Void> memberUsernameCheck(
@Valid @RequestBody UsernameCheckRequest request) {
authService.checkUsername(request);
return ResponseEntity.ok().build();
}

@Operation(summary = "로그인", description = "토큰 발급을 위해 로그인을 진행합니다.")
@PostMapping("/login")
public ResponseEntity<TokenPairResponse> memberLogin(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -56,6 +56,9 @@ public TokenPairResponse loginMember(UsernamePasswordRequest request) {

validateNotGuestMember(member);
validatePasswordMatches(member, request.password());
validateNormalMember(member);

member.updateLastLoginAt();

return getLoginResponse(member);
}
Expand All @@ -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);
Expand All @@ -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);
}
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -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<Void> memberUsernameCheck(
@Valid @RequestBody UsernameCheckRequest request) {
memberService.checkUsername(request);
return ResponseEntity.ok().build();
}

// TODO: 테스트 코드 작성 필요
@Operation(summary = "회원 탈퇴", description = "회원탈퇴를 진행합니다.")
@DeleteMapping("/withdrawal")
public ResponseEntity<Void> memberWithdrawal(@Valid @RequestBody UsernameCheckRequest request) {
memberService.withdrawal(request);
return ResponseEntity.ok().build();
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -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();
}
}
15 changes: 13 additions & 2 deletions src/main/java/com/depromeet/domain/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, "시큐리티 인증 정보를 찾을 수 없습니다."),
Expand All @@ -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, "비밀번호가 일치하지 않습니다."),
Expand Down
14 changes: 0 additions & 14 deletions src/test/java/com/depromeet/domain/member/domain/MemberTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 74b5690

Please sign in to comment.