Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,4 +26,13 @@ public CsResponse<Long> register(
Long id = memberService.createMember(request);
return CsResponse.of(MemberSuccessCode.GENERATED_REGISTERED, id);
}
}

@PostMapping("/withdrawal")
@ResponseStatus(HttpStatus.OK)
public CsResponse<Void> withdraw(
@IdentifiedUser Member member
) {
memberService.deleteMember(member);
return CsResponse.of(MemberSuccessCode.OK_WITHDRAWN);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -27,4 +30,14 @@ public interface SpringDocMemberController {
CsResponse<Long> 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<Void> withdraw(
@Parameter(hidden = true) @IdentifiedUser Member member
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ public interface ExceptionCode {
int getCode();
String getMessage();
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -30,6 +36,9 @@ public class MemberControllerTest {
@MockitoBean
private JwtTokenResolver jwtTokenResolver;

@MockitoBean
private IdentifiedUserArgumentResolver identifiedUserArgumentResolver;

@Autowired
private MockMvc mockMvc;

Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -103,6 +106,21 @@ void registeredFailedByConflictUsername() {
});
}

@Test
@DisplayName("회원탈퇴 성공: 멤버 객체를 삭제한다.")
void shouldDeleteMember() {
// givent
Member member = MemberFixture.memberFixture();
memberRepository.saveAndFlush(member);

// when
memberService.deleteMember(member);

// then
Collection<Member> members = memberRepository.findAll();
assertThat(members).hasSize(0);
}

@Test
@DisplayName("loadUserByUsername 실패: 존재하지 않는 이메일로 사용자 로드 시 UsernameNotFoundException이 발생한다.")
void shouldThrowUsernameNotFoundExceptionWhenLoadingNonExistentUser() {
Expand Down
Loading