diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberController.java index ce1a57e..2743a43 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberController.java @@ -3,7 +3,9 @@ import jakarta.validation.Valid; import kr.ac.ks.cs_web_back.domain.member.controller.code.MemberSuccessCode; import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest; +import kr.ac.ks.cs_web_back.domain.member.model.Member; import kr.ac.ks.cs_web_back.domain.member.service.MemberService; +import kr.ac.ks.cs_web_back.global.annotation.IdentifiedUser; import kr.ac.ks.cs_web_back.global.response.CsResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -24,4 +26,13 @@ public CsResponse register( Long id = memberService.createMember(request); return CsResponse.of(MemberSuccessCode.GENERATED_REGISTERED, id); } -} + + @PostMapping("/withdrawal") + @ResponseStatus(HttpStatus.OK) + public CsResponse withdraw( + @IdentifiedUser Member member + ) { + memberService.deleteMember(member); + return CsResponse.of(MemberSuccessCode.OK_WITHDRAWN); + } +} \ No newline at end of file diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/SpringDocMemberController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/SpringDocMemberController.java index 1f5fd2e..66b405f 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/SpringDocMemberController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/SpringDocMemberController.java @@ -1,9 +1,12 @@ package kr.ac.ks.cs_web_back.domain.member.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest; +import kr.ac.ks.cs_web_back.domain.member.model.Member; +import kr.ac.ks.cs_web_back.global.annotation.IdentifiedUser; import kr.ac.ks.cs_web_back.global.response.CsResponse; import kr.ac.ks.cs_web_back.global.swagger.error.ApiErrorResponse; import kr.ac.ks.cs_web_back.global.swagger.error.ErrorCase; @@ -27,4 +30,14 @@ public interface SpringDocMemberController { CsResponse register( @RequestBody MemberCreateRequest request ); + + @Operation(summary = "회원탈퇴", description = "회원 탈퇴") + @ApiResponse(responseCode = "200", description = "회원 탈퇴에 성공했습니다.") + @ApiErrorResponse(status = HttpStatus.UNAUTHORIZED, instance = "/member/withdrawal", errorCases = { + @ErrorCase(description = "토큰 형식 이상", code = 8001, exampleMessage = "유효하지 않은 토큰입니다."), + @ErrorCase(description = "인증 실패 (토큰 만료 혹은 틀린 이메일 / 비밀번호)", code = 8002, exampleMessage = "인증에 실패했습니다.") + }) + CsResponse withdraw( + @Parameter(hidden = true) @IdentifiedUser Member member + ); } diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/code/MemberExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/code/MemberExceptionCode.java index 2dd961a..6fed1fa 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/code/MemberExceptionCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/member/controller/code/MemberExceptionCode.java @@ -9,11 +9,11 @@ public enum MemberExceptionCode implements ExceptionCode { BAD_REQUEST_NO_EMAIL(9010, "이메일이 입력되지 않았습니다."), - BAD_REQUEST_NO_PASSWORD(9011, "비밀번호가 입력되지 않았습니다."), - BAD_REQUEST_NO_USERNAME(9012, "사용자명이 입력되지 않았습니다."), - CONFLICT_EMAIL(5010, "이미 존재하는 이메일입니다."), + BAD_REQUEST_NO_PASSWORD(9010, "비밀번호가 입력되지 않았습니다."), + BAD_REQUEST_NO_USERNAME(9010, "사용자명이 입력되지 않았습니다."), + CONFLICT_EMAIL(5010, "이미 가입된 이메일입니다."), CONFLICT_USERNAME(5012, "이미 존재하는 사용자명입니다."), - NOT_FOUND_USER(6001, "일치하는 사용자가 존재하지 않습니다."), + UNAUTHORIZED_PASSWORD(8011, "비밀번호가 일치하지 않습니다."), ; private final int code; diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java index bacb9a8..5f81eb4 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/member/service/MemberService.java @@ -39,6 +39,10 @@ public Long createMember(MemberCreateRequest request) { return memberRepository.save(member).getId(); } + public void deleteMember(Member member) { + memberRepository.delete(member); + } + @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/exeption/dto/ExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/global/exeption/dto/ExceptionCode.java index d714162..a1a3adb 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/exeption/dto/ExceptionCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/exeption/dto/ExceptionCode.java @@ -4,3 +4,4 @@ public interface ExceptionCode { int getCode(); String getMessage(); } + diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberControllerTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberControllerTest.java index 6e1ab09..0c59070 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberControllerTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/member/controller/MemberControllerTest.java @@ -2,9 +2,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest; +import kr.ac.ks.cs_web_back.domain.member.fixture.MemberFixture; +import kr.ac.ks.cs_web_back.domain.member.model.Member; import kr.ac.ks.cs_web_back.domain.member.service.MemberService; import kr.ac.ks.cs_web_back.global.jwt.JwtTokenResolver; import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; +import kr.ac.ks.cs_web_back.global.resolver.IdentifiedUserArgumentResolver; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -13,6 +16,9 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -30,6 +36,9 @@ public class MemberControllerTest { @MockitoBean private JwtTokenResolver jwtTokenResolver; + @MockitoBean + private IdentifiedUserArgumentResolver identifiedUserArgumentResolver; + @Autowired private MockMvc mockMvc; @@ -155,5 +164,19 @@ public void tooLongUsernameReturns400BadRequest() throws Exception { .andExpect(status().isBadRequest()); } + @Test + @DisplayName("회원탈퇴 성공: 회원 탈퇴에 성공하면 200 Ok를 반환한다.") + public void withdrawalSuccessfullyReturns200Ok() throws Exception { + // given + String token = "Bearer valid-access-token"; + Member member = MemberFixture.memberFixture(); + given(identifiedUserArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(member); + doNothing().when(memberService).deleteMember(member); + + // when & then + mockMvc.perform(post("/member/withdrawal") + .header("Authorization", token)) + .andExpect(status().isOk()); + } } diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java index 926fdc0..097dbc2 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/member/service/MemberServiceTest.java @@ -2,6 +2,7 @@ import kr.ac.ks.cs_web_back.domain.member.controller.code.MemberExceptionCode; import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest; +import kr.ac.ks.cs_web_back.domain.member.fixture.MemberFixture; import kr.ac.ks.cs_web_back.domain.member.model.Member; import kr.ac.ks.cs_web_back.domain.member.repository.MemberRepository; import kr.ac.ks.cs_web_back.global.exeption.domain.ConflictException; @@ -13,6 +14,8 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; +import java.util.Collection; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -103,6 +106,21 @@ void registeredFailedByConflictUsername() { }); } + @Test + @DisplayName("회원탈퇴 성공: 멤버 객체를 삭제한다.") + void shouldDeleteMember() { + // givent + Member member = MemberFixture.memberFixture(); + memberRepository.saveAndFlush(member); + + // when + memberService.deleteMember(member); + + // then + Collection members = memberRepository.findAll(); + assertThat(members).hasSize(0); + } + @Test @DisplayName("loadUserByUsername 실패: 존재하지 않는 이메일로 사용자 로드 시 UsernameNotFoundException이 발생한다.") void shouldThrowUsernameNotFoundExceptionWhenLoadingNonExistentUser() {