diff --git a/build.gradle b/build.gradle index aa114e1..7fd85d8 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,9 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'org.springframework.boot:spring-boot-starter-actuator' - + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + //Feign implementation("org.springframework.cloud:spring-cloud-starter-openfeign") diff --git a/src/docs/asciidoc/auth.adoc b/src/docs/asciidoc/auth.adoc index 8885e59..d371bbe 100644 --- a/src/docs/asciidoc/auth.adoc +++ b/src/docs/asciidoc/auth.adoc @@ -3,73 +3,149 @@ :toclevels: 2 :sectlinks: -[[resources-post]] +[[resources-auth]] == Authentication -[[resources-post-create]] -=== 로그인 시도 -==== HTTP request +[[resources-auth-kakao]] +=== 카카오 로그인 + +==== 로그인 성공 + +===== HTTP request +include::{snippets}/kakao-login-success/http-request.adoc[] + +===== request-body 설명 +include::{snippets}/kakao-login-success/request-fields.adoc[] + +===== HTTP response +include::{snippets}/kakao-login-success/http-response.adoc[] + +===== response-body 설명 +include::{snippets}/kakao-login-success/response-fields.adoc[] + +==== 회원가입 필요 + +===== HTTP request +include::{snippets}/kakao-login-need-signup/http-request.adoc[] + +===== request-body 설명 +include::{snippets}/kakao-login-need-signup/request-fields.adoc[] + +===== HTTP response +include::{snippets}/kakao-login-need-signup/http-response.adoc[] + +===== response-body 설명 +include::{snippets}/kakao-login-need-signup/response-fields.adoc[] + + +[[resources-auth-apple]] +=== 애플 로그인 + +==== 로그인 성공 + +===== HTTP request +include::{snippets}/apple-login-success/http-request.adoc[] + +===== request-body 설명 +include::{snippets}/apple-login-success/request-fields.adoc[] + +===== HTTP response +include::{snippets}/apple-login-success/http-response.adoc[] + +===== response-body 설명 +include::{snippets}/apple-login-success/response-fields.adoc[] + +==== 회원가입 필요 + +===== HTTP request +include::{snippets}/apple-login-need-signup/http-request.adoc[] + +===== request-body 설명 +include::{snippets}/apple-login-need-signup/request-fields.adoc[] + +===== HTTP response +include::{snippets}/apple-login-need-signup/http-response.adoc[] -include::{snippets}/auth-login/http-request.adoc[] +===== response-body 설명 +include::{snippets}/apple-login-need-signup/response-fields.adoc[] + +==== 토큰 파싱 실패 + +===== HTTP request +include::{snippets}/apple-login-token-parse-failed/http-request.adoc[] + +===== HTTP response +include::{snippets}/apple-login-token-parse-failed/http-response.adoc[] + +[[resources-auth-signup]] +=== 회원가입 + +==== HTTP request +include::{snippets}/signup-success/http-request.adoc[] ==== request-body 설명 -include::{snippets}/auth-login/request-fields.adoc[] +include::{snippets}/signup-success/request-fields.adoc[] ==== HTTP response - -include::{snippets}/auth-login/http-response.adoc[] +include::{snippets}/signup-success/http-response.adoc[] ==== response-body 설명 -include::{snippets}/auth-login/response-fields.adoc[] +include::{snippets}/signup-success/response-fields.adoc[] + +=== 회원가입 중복 실패 + +==== HTTP request +include::{snippets}/signup-already-registered/http-request.adoc[] + +==== request-body 설명 +include::{snippets}/signup-already-registered/request-fields.adoc[] + +==== HTTP response +include::{snippets}/signup-already-registered/http-response.adoc[] +[[resources-auth-logout]] === 로그아웃 ==== HTTP request - include::{snippets}/auth-logout/http-request.adoc[] ==== request-body 설명 include::{snippets}/auth-logout/request-fields.adoc[] ==== HTTP response - include::{snippets}/auth-logout/http-response.adoc[] - - -=== 토큰재발급 +[[resources-auth-reissue]] +=== 토큰 재발급 ==== HTTP request - include::{snippets}/auth-reissue/http-request.adoc[] ==== request-body 설명 include::{snippets}/auth-reissue/request-fields.adoc[] ==== HTTP response - include::{snippets}/auth-reissue/http-response.adoc[] ==== response-body 설명 include::{snippets}/auth-reissue/response-fields.adoc[] +[[resources-auth-withdraw]] === 회원탈퇴 ==== HTTP request - include::{snippets}/auth-withdraw/http-request.adoc[] ==== request-header 설명 include::{snippets}/auth-withdraw/request-headers.adoc[] ==== HTTP response - include::{snippets}/auth-withdraw/http-response.adoc[] ==== response-body 설명 -include::{snippets}/auth-withdraw/response-fields.adoc[] \ No newline at end of file +include::{snippets}/auth-withdraw/response-fields.adoc[] diff --git a/src/main/java/com/example/matzipbookserver/global/config/SecurityConfig.java b/src/main/java/com/example/matzipbookserver/global/config/SecurityConfig.java new file mode 100644 index 0000000..bdb778a --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/global/config/SecurityConfig.java @@ -0,0 +1,43 @@ +package com.example.matzipbookserver.global.config; + +import com.example.matzipbookserver.global.jwt.JwtAuthenticationFilter; +import com.example.matzipbookserver.global.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + private final JwtTokenProvider jwtTokenProvider; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .formLogin(form -> form.disable()) + .httpBasic(httpBasic -> httpBasic.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/**", "/api/signup").permitAll() + .anyRequest().authenticated() + ) + .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())) + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/example/matzipbookserver/global/config/WebMvcConfig.java b/src/main/java/com/example/matzipbookserver/global/config/WebMvcConfig.java new file mode 100644 index 0000000..2d836ce --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/global/config/WebMvcConfig.java @@ -0,0 +1,21 @@ +package com.example.matzipbookserver.global.config; + +import com.example.matzipbookserver.global.resolver.CurrentMemberArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final CurrentMemberArgumentResolver currentMemberArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(currentMemberArgumentResolver); + } +} diff --git a/src/main/java/com/example/matzipbookserver/global/jwt/JwtAuthenticationFilter.java b/src/main/java/com/example/matzipbookserver/global/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..3b570ce --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/global/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,38 @@ +package com.example.matzipbookserver.global.jwt; + +import com.example.matzipbookserver.member.domain.Member; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String token = jwtTokenProvider.resolveToken(request); + if (token != null && jwtTokenProvider.validateToken(token)) { + Member member = jwtTokenProvider.getMemberFromToken(token); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(member, null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); + + + SecurityContextHolder.getContext().setAuthentication(authentication); + + + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/example/matzipbookserver/global/jwt/JwtTokenProvider.java b/src/main/java/com/example/matzipbookserver/global/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..045b023 --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/global/jwt/JwtTokenProvider.java @@ -0,0 +1,86 @@ +package com.example.matzipbookserver.global.jwt; + +import com.example.matzipbookserver.global.exception.RestApiException; +import com.example.matzipbookserver.global.response.error.AuthErrorCode; +import com.example.matzipbookserver.global.response.error.MemberErrorCode; +import com.example.matzipbookserver.member.domain.Member; +import com.example.matzipbookserver.member.repository.MemberRepository; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Base64; +import java.util.Date; + +@Component +public class JwtTokenProvider { + private final MemberRepository memberRepository; + @Value("${jwt.secret}") + private String secret; + + private SecretKey secretKey; + + public JwtTokenProvider(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @PostConstruct + public void init() { + byte[] decodedKey = Base64.getDecoder().decode(secret); + this.secretKey = Keys.hmacShaKeyFor(decodedKey); + } + + public String createAccessToken(String provider, String providerId) { + return Jwts.builder() + .setSubject(provider + ":" + providerId) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) + .signWith(secretKey) + .compact(); + } + + public String resolveToken(HttpServletRequest request) { + String bearer = request.getHeader("Authorization"); + if (bearer != null && bearer.startsWith("Bearer ")) { + return bearer.substring(7); + } + return null; + } + + public String getSubject(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token).getBody().getSubject(); + } + + public Member getMemberFromToken(String token) { + String subject = getSubject(token); // ex) "kakao:12345" + String[] parts = subject.split(":"); + if (parts.length != 2) { + throw new RestApiException(AuthErrorCode.INVALID_TOKEN); + } + + String provider = parts[0]; + String providerId = parts[1]; + + return memberRepository.findByProviderAndProviderId(provider,providerId) + .orElseThrow(() -> new RestApiException(MemberErrorCode.MEMBER_NOT_FOUND)); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/matzipbookserver/global/resolver/CurrentMember.java b/src/main/java/com/example/matzipbookserver/global/resolver/CurrentMember.java new file mode 100644 index 0000000..3c8a538 --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/global/resolver/CurrentMember.java @@ -0,0 +1,9 @@ +package com.example.matzipbookserver.global.resolver; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface CurrentMember { +} diff --git a/src/main/java/com/example/matzipbookserver/global/resolver/CurrentMemberArgumentResolver.java b/src/main/java/com/example/matzipbookserver/global/resolver/CurrentMemberArgumentResolver.java new file mode 100644 index 0000000..e20717a --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/global/resolver/CurrentMemberArgumentResolver.java @@ -0,0 +1,47 @@ +package com.example.matzipbookserver.global.resolver; + + +import com.example.matzipbookserver.global.exception.RestApiException; +import com.example.matzipbookserver.global.jwt.JwtTokenProvider; +import com.example.matzipbookserver.global.response.error.AuthErrorCode; +import com.example.matzipbookserver.member.domain.Member; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + + + +@Component +@RequiredArgsConstructor +public class CurrentMemberArgumentResolver implements HandlerMethodArgumentResolver { + private final JwtTokenProvider jwtTokenProvider; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(CurrentMember.class) + && parameter.getParameterType().equals(Member.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + String token = jwtTokenProvider.resolveToken(request); + + if (token == null || !jwtTokenProvider.validateToken(token)) { + throw new RestApiException(AuthErrorCode.INVALID_TOKEN); + } + + return jwtTokenProvider.getMemberFromToken(token); + } + + + + + +} diff --git a/src/main/java/com/example/matzipbookserver/global/response/error/AuthErrorCode.java b/src/main/java/com/example/matzipbookserver/global/response/error/AuthErrorCode.java index 9a8de92..6af2bcc 100644 --- a/src/main/java/com/example/matzipbookserver/global/response/error/AuthErrorCode.java +++ b/src/main/java/com/example/matzipbookserver/global/response/error/AuthErrorCode.java @@ -8,7 +8,8 @@ public enum AuthErrorCode implements ErrorCode { KAKAO_EMAIL_NOT_PROVIDED("AUTH-003", HttpStatus.BAD_REQUEST, "이메일 정보가 제공되지 않았습니다."), APPLE_TOKEN_REQUEST_FAIL("AUTH-004", HttpStatus.BAD_REQUEST, "애플 토큰 요청에 실패했습니다."), APPLE_EMAIL_NOT_PROVIDED("AUTH-005", HttpStatus.BAD_REQUEST, "애플 이메일 정보가 제공되지 않았습니다."), - APPLE_ID_TOKEN_PARSE_FAILED("AUTH-006", HttpStatus.UNAUTHORIZED, "Apple ID Token 파싱에 실패했습니다."); + APPLE_ID_TOKEN_PARSE_FAILED("AUTH-006", HttpStatus.UNAUTHORIZED, "Apple ID Token 파싱에 실패했습니다."), + INVALID_TOKEN("AUTH-007", HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."); private final String developCode; private final HttpStatus httpStatus; diff --git a/src/main/java/com/example/matzipbookserver/global/response/error/MemberErrorCode.java b/src/main/java/com/example/matzipbookserver/global/response/error/MemberErrorCode.java new file mode 100644 index 0000000..3a79b89 --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/global/response/error/MemberErrorCode.java @@ -0,0 +1,36 @@ +package com.example.matzipbookserver.global.response.error; + + +import org.springframework.http.HttpStatus; + +public enum MemberErrorCode implements ErrorCode { + + ALREADY_REGISTERED("MEMBER-001",HttpStatus.BAD_REQUEST, "이미 가입된 사용자입니다."), + MEMBER_NOT_FOUND("MEMBER-002", HttpStatus.NOT_FOUND, "사용자 정보를 찾을 수 없습니다."); + + private final String developCode; + private final HttpStatus httpStatus; + private final String message; + + MemberErrorCode(String developCode, HttpStatus httpStatus, String message) { + this.developCode = developCode; + this.httpStatus = httpStatus; + this.message = message; + } + + @Override + public String getDevelopCode() { + return developCode; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } + +} diff --git a/src/main/java/com/example/matzipbookserver/global/response/success/MemberSuccessCode.java b/src/main/java/com/example/matzipbookserver/global/response/success/MemberSuccessCode.java index 9db84e8..3443d7b 100644 --- a/src/main/java/com/example/matzipbookserver/global/response/success/MemberSuccessCode.java +++ b/src/main/java/com/example/matzipbookserver/global/response/success/MemberSuccessCode.java @@ -4,7 +4,11 @@ public enum MemberSuccessCode implements SuccessCode { LOGIN_SUCCESS(HttpStatus.OK,"MEMBER-001","로그인 성공"), - SIGNUP_REQUIRED(HttpStatus.OK, "MEMBER-002", "회원가입 필요"); + SIGNUP_REQUIRED(HttpStatus.OK, "MEMBER-002", "회원가입 필요"), + SIGNUP_SUCCESS(HttpStatus.OK, "MEMBER-003","회원가입 성공"), + FCM_TOKEN_SAVED(HttpStatus.OK, "MEMBER-004", "FCM 토큰 저장 완료"), + MEMBER_INFO_SUCCESS(HttpStatus.OK,"MEMBER-005", "회원 정보 조회 성공"); + private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/com/example/matzipbookserver/member/controller/AuthController.java b/src/main/java/com/example/matzipbookserver/member/controller/AuthController.java index 33afa20..d346204 100644 --- a/src/main/java/com/example/matzipbookserver/member/controller/AuthController.java +++ b/src/main/java/com/example/matzipbookserver/member/controller/AuthController.java @@ -1,15 +1,18 @@ package com.example.matzipbookserver.member.controller; +import com.example.matzipbookserver.global.resolver.CurrentMember; import com.example.matzipbookserver.global.response.SuccessResponse; import com.example.matzipbookserver.global.response.success.MemberSuccessCode; import com.example.matzipbookserver.member.controller.dto.request.AppleLoginRequest; +import com.example.matzipbookserver.member.controller.dto.request.FcmTokenRequest; import com.example.matzipbookserver.member.controller.dto.request.KakaoLoginRequest; -import com.example.matzipbookserver.member.controller.dto.response.AppleLoginResponse; -import com.example.matzipbookserver.member.controller.dto.response.KakaoLoginResponse; -import com.example.matzipbookserver.member.controller.dto.response.LoginResponse; +import com.example.matzipbookserver.member.controller.dto.request.SignupRequest; +import com.example.matzipbookserver.member.controller.dto.response.*; +import com.example.matzipbookserver.member.domain.Member; import com.example.matzipbookserver.member.service.AuthService; import com.example.matzipbookserver.member.service.FcmTokenService; +import com.example.matzipbookserver.member.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -21,15 +24,18 @@ public class AuthController { private final AuthService authService; private final FcmTokenService fcmTokenService; + private final MemberService memberService; + + // 테스트용 @GetMapping("/kakao/callback") public ResponseEntity kakaoCallback(@RequestParam String code) { return ResponseEntity.ok("카카오 인가코드 수신 완료: " + code); } @PostMapping("/login/kakao") - public ResponseEntity kakaoLogin(@RequestBody KakaoLoginRequest request) { + public SuccessResponse kakaoLogin(@RequestBody KakaoLoginRequest request) { LoginResponse response = authService.kakaoLogin(request.code(), request.fcmToken()); if(response instanceof KakaoLoginResponse) { @@ -39,7 +45,7 @@ public ResponseEntity kakaoLogin(@RequestBody KakaoLoginRequest request) { } } - + // 테스트용 @PostMapping("/apple/callback") public ResponseEntity appleCallback(@RequestParam String code, @RequestParam(required = false) String id_token) { @@ -47,7 +53,7 @@ public ResponseEntity appleCallback(@RequestParam String code, } @PostMapping("/login/apple") - public ResponseEntity appleLogin(@RequestBody AppleLoginRequest request) { + public SuccessResponse appleLogin(@RequestBody AppleLoginRequest request) { LoginResponse response = authService.appleLogin(request.code(), request.fcmToken()); @@ -58,5 +64,23 @@ public ResponseEntity appleLogin(@RequestBody AppleLoginRequest request) { } } + @PostMapping("/signup") + public SuccessResponse signup(@RequestBody SignupRequest request) { + SignupResponse response = memberService.signup(request); + return SuccessResponse.of(MemberSuccessCode.SIGNUP_SUCCESS,response); + } + + @GetMapping("/me") + public SuccessResponsegetCurrentMember(@CurrentMember Member member) { + return SuccessResponse.of(MemberSuccessCode.MEMBER_INFO_SUCCESS, MemberInfoResponse.from(member)); + } + + + @PostMapping("/fcm") + public SuccessResponse saveFcmToken(@CurrentMember Member member, @RequestBody FcmTokenRequest request) { + fcmTokenService.saveOrUpdate(member, request.fcmToken()); + return SuccessResponse.of(MemberSuccessCode.FCM_TOKEN_SAVED, new FcmTokenSaveResponse("success")); + } + } diff --git a/src/main/java/com/example/matzipbookserver/member/controller/dto/request/SignupRequest.java b/src/main/java/com/example/matzipbookserver/member/controller/dto/request/SignupRequest.java new file mode 100644 index 0000000..3afdebc --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/member/controller/dto/request/SignupRequest.java @@ -0,0 +1,12 @@ +package com.example.matzipbookserver.member.controller.dto.request; + +public record SignupRequest ( + String email, + String provider, + String providerId, + String nickname, + String birth, + String gender, + String profileImagePath, + String university +) {} diff --git a/src/main/java/com/example/matzipbookserver/member/controller/dto/response/FcmTokenSaveResponse.java b/src/main/java/com/example/matzipbookserver/member/controller/dto/response/FcmTokenSaveResponse.java new file mode 100644 index 0000000..c6b0fc8 --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/member/controller/dto/response/FcmTokenSaveResponse.java @@ -0,0 +1,3 @@ +package com.example.matzipbookserver.member.controller.dto.response; + +public record FcmTokenSaveResponse(String result) {} diff --git a/src/main/java/com/example/matzipbookserver/member/controller/dto/response/MemberInfoResponse.java b/src/main/java/com/example/matzipbookserver/member/controller/dto/response/MemberInfoResponse.java new file mode 100644 index 0000000..9ae8814 --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/member/controller/dto/response/MemberInfoResponse.java @@ -0,0 +1,35 @@ +package com.example.matzipbookserver.member.controller.dto.response; + +import com.example.matzipbookserver.member.domain.Member; +import lombok.Builder; + +import java.time.LocalDateTime; + +@Builder +public record MemberInfoResponse( + Long id, + String email, + String nickname, + String provider, + String providerId, + String birth, + String gender, + String profileImagePath, + String university, + LocalDateTime createdAt +) { + public static MemberInfoResponse from(Member m) { + return MemberInfoResponse.builder() + .id(m.getId()) + .email(m.getEmail()) + .nickname(m.getNickname()) + .provider(m.getProvider()) + .providerId(m.getProviderId()) + .birth(m.getBirth()) + .gender(m.getGender()) + .profileImagePath(m.getProfileImagePath()) + .university(m.getUniversity()) + .createdAt(m.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/example/matzipbookserver/member/controller/dto/response/SignupResponse.java b/src/main/java/com/example/matzipbookserver/member/controller/dto/response/SignupResponse.java new file mode 100644 index 0000000..0352d27 --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/member/controller/dto/response/SignupResponse.java @@ -0,0 +1,11 @@ +package com.example.matzipbookserver.member.controller.dto.response; + +import lombok.Builder; + +@Builder +public record SignupResponse ( + Long id, + String email, + String nickname, + String jwtToken +) {} diff --git a/src/main/java/com/example/matzipbookserver/member/domain/Member.java b/src/main/java/com/example/matzipbookserver/member/domain/Member.java index 91d4c0a..a7c97a5 100644 --- a/src/main/java/com/example/matzipbookserver/member/domain/Member.java +++ b/src/main/java/com/example/matzipbookserver/member/domain/Member.java @@ -7,6 +7,8 @@ import jakarta.persistence.Id; import lombok.Getter; +import java.time.LocalDateTime; + @Entity @Getter public class Member { @@ -18,4 +20,26 @@ public class Member { private String provider; private String providerId; private String email; + + private String nickname; + private String birth; + private String gender; + private String profileImagePath; + + private LocalDateTime createdAt; + private String university; + + protected Member() {} + + public Member(String provider, String providerId, String email, String nickname, String birth, String gender, String profileImagePath, String university) { + this.provider = provider; + this.providerId = providerId; + this.email = email; + this.nickname = nickname; + this.birth = birth; + this.gender = gender; + this.profileImagePath = profileImagePath; + this.university = university; + this.createdAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/example/matzipbookserver/member/service/AuthService.java b/src/main/java/com/example/matzipbookserver/member/service/AuthService.java index 646d1bc..dc4bc58 100644 --- a/src/main/java/com/example/matzipbookserver/member/service/AuthService.java +++ b/src/main/java/com/example/matzipbookserver/member/service/AuthService.java @@ -1,6 +1,7 @@ package com.example.matzipbookserver.member.service; import com.example.matzipbookserver.global.exception.RestApiException; +import com.example.matzipbookserver.global.jwt.JwtTokenProvider; import com.example.matzipbookserver.global.response.error.AuthErrorCode; import com.example.matzipbookserver.member.controller.dto.response.KakaoLoginResponse; import com.example.matzipbookserver.member.controller.dto.response.AppleLoginResponse; @@ -59,7 +60,7 @@ public LoginResponse kakaoLogin(String code, String fcmToken) { if(member.isPresent()){ fcmTokenService.saveOrUpdate(member.get(), fcmToken); - String jwt = jwtTokenProvider.createAccessToken(member.get().getEmail()); + String jwt = jwtTokenProvider.createAccessToken("kakao", providerId); return new KakaoLoginResponse(jwt, new KakaoLoginResponse.UserInfo(member.get().getId(), member.get().getEmail())); } else { return new SignupNeededResponse(email, providerId); @@ -97,7 +98,7 @@ public LoginResponse appleLogin(String code, String fcmToken) { if (member.isPresent()) { fcmTokenService.saveOrUpdate(member.get(), fcmToken); - String jwt = jwtTokenProvider.createAccessToken(member.get().getEmail()); + String jwt = jwtTokenProvider.createAccessToken("apple",providerId); return new AppleLoginResponse(jwt, new AppleLoginResponse.UserInfo(member.get().getId(), member.get().getEmail())); } else { return new SignupNeededResponse(email, providerId); diff --git a/src/main/java/com/example/matzipbookserver/member/service/JwtTokenProvider.java b/src/main/java/com/example/matzipbookserver/member/service/JwtTokenProvider.java deleted file mode 100644 index e38344b..0000000 --- a/src/main/java/com/example/matzipbookserver/member/service/JwtTokenProvider.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.matzipbookserver.member.service; - -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; -import jakarta.annotation.PostConstruct; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import javax.crypto.SecretKey; -import java.util.Base64; -import java.util.Date; - -@Component -public class JwtTokenProvider { - @Value("${jwt.secret}") - private String secret; - - private SecretKey secretKey; - - @PostConstruct - public void init() { - byte[] decodedKey = Base64.getDecoder().decode(secret); - this.secretKey = Keys.hmacShaKeyFor(decodedKey); - } - public String createAccessToken(String email) { - return Jwts.builder() - .setSubject(email) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) - .signWith(secretKey) - .compact(); - } -} diff --git a/src/main/java/com/example/matzipbookserver/member/service/MemberService.java b/src/main/java/com/example/matzipbookserver/member/service/MemberService.java new file mode 100644 index 0000000..56b2a43 --- /dev/null +++ b/src/main/java/com/example/matzipbookserver/member/service/MemberService.java @@ -0,0 +1,46 @@ +package com.example.matzipbookserver.member.service; + +import com.example.matzipbookserver.global.exception.RestApiException; +import com.example.matzipbookserver.global.jwt.JwtTokenProvider; +import com.example.matzipbookserver.member.controller.dto.request.SignupRequest; +import com.example.matzipbookserver.member.controller.dto.response.SignupResponse; +import com.example.matzipbookserver.member.domain.Member; +import com.example.matzipbookserver.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import com.example.matzipbookserver.global.response.error.MemberErrorCode; + + +@Service +@RequiredArgsConstructor +public class MemberService { + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + + public SignupResponse signup(SignupRequest request) { + memberRepository.findByProviderAndProviderId(request.provider(), request.providerId()).ifPresent(member -> { + throw new RestApiException(MemberErrorCode.ALREADY_REGISTERED); + }); + + Member member = new Member( + request.provider(), + request.providerId(), + request.email(), + request.nickname(), + request.birth(), + request.gender(), + request.profileImagePath(), + request.university() + ); + Member saved = memberRepository.save(member); + + String jwt = jwtTokenProvider.createAccessToken(saved.getProvider(), saved.getProviderId()); + + return new SignupResponse( + saved.getId(), + saved.getEmail(), + saved.getNickname(), + jwt + ); + } +} diff --git a/src/test/java/com/example/matzipbookserver/member/controller/AuthControllerTest.java b/src/test/java/com/example/matzipbookserver/member/controller/AuthControllerTest.java index f700297..512a55c 100644 --- a/src/test/java/com/example/matzipbookserver/member/controller/AuthControllerTest.java +++ b/src/test/java/com/example/matzipbookserver/member/controller/AuthControllerTest.java @@ -1,190 +1,197 @@ -//package com.example.matzipbookserver.member.controller; -// -//import com.example.matzipbookserver.global.exception.ApiExceptionHandler; -//import com.example.matzipbookserver.global.exception.RestApiException; -//import com.example.matzipbookserver.global.response.error.AuthErrorCode; -//import com.example.matzipbookserver.member.controller.dto.request.AppleLoginRequest; -//import com.example.matzipbookserver.member.controller.dto.request.KakaoLoginRequest; -//import com.example.matzipbookserver.member.controller.dto.response.AppleLoginResponse; -//import com.example.matzipbookserver.member.controller.dto.response.KakaoLoginResponse; -//import com.example.matzipbookserver.member.controller.dto.response.SignupNeededResponse; -//import com.example.matzipbookserver.member.service.AuthService; -//import com.example.matzipbookserver.member.service.FcmTokenService; -//import com.fasterxml.jackson.databind.ObjectMapper; -//import org.junit.jupiter.api.BeforeEach; -//import org.junit.jupiter.api.Test; -//import org.junit.jupiter.api.extension.ExtendWith; -//import org.mockito.Mockito; -//import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -//import org.springframework.boot.test.context.TestConfiguration; -//import org.springframework.context.annotation.Bean; -//import org.springframework.context.annotation.Import; -//import org.springframework.http.MediaType; -//import org.springframework.restdocs.RestDocumentationContextProvider; -//import org.springframework.restdocs.RestDocumentationExtension; -//import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -//import org.springframework.test.web.servlet.MockMvc; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.test.web.servlet.setup.MockMvcBuilders; -//import org.springframework.web.context.WebApplicationContext; -// -//import static org.hamcrest.Matchers.containsString; -//import static org.mockito.ArgumentMatchers.anyString; -//import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -//import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -//import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -// -//@ExtendWith(RestDocumentationExtension.class) -//@WebMvcTest(AuthController.class) -//@Import(AuthControllerTest.AuthControllerTestConfig.class) -//public class AuthControllerTest { -// -// @Autowired -// private WebApplicationContext context; -// -// private RestDocumentationResultHandler restDocs; -// -// @Autowired -// private ObjectMapper objectMapper; -// -// private MockMvc mockMvc; -// -// private AuthService authService = Mockito.mock(AuthService.class); -// private FcmTokenService fcmTokenService = Mockito.mock(FcmTokenService.class); -// -// @BeforeEach -// void setup(RestDocumentationContextProvider restDocumentation) { -// -// this.restDocs = document("{class-name}/{method-name}"); -// this.mockMvc = MockMvcBuilders -// .standaloneSetup(new AuthController(authService, fcmTokenService)) -// .setControllerAdvice(new ApiExceptionHandler()) -// .apply(documentationConfiguration(restDocumentation)) -// .alwaysDo(print()) -// .alwaysDo(restDocs) -// .build(); -// } -// -// @TestConfiguration -// static class AuthControllerTestConfig { -// @Bean -// public AuthService authService() { -// return Mockito.mock(AuthService.class); -// } -// -// @Bean -// public FcmTokenService fcmTokenService() { -// return Mockito.mock(FcmTokenService.class); -// } -// } -// -// @Test -// void 카카오_로그인_성공_테스트() throws Exception { -// // given -// KakaoLoginRequest request = new KakaoLoginRequest("code111", "fcmToken111"); -// KakaoLoginResponse response = new KakaoLoginResponse("fake-jwt-token", new KakaoLoginResponse.UserInfo(1L,"test@email.com")); -// -// Mockito.when(authService.kakaoLogin(anyString(), anyString())).thenReturn(response); -// -// // when & then -// mockMvc.perform(post("/api/auth/login/kakao") -// .contentType(MediaType.APPLICATION_JSON) -// .content(objectMapper.writeValueAsString(request))) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.code").value("로그인 성공")) -// .andExpect(jsonPath("$.result.jwtToken").value("fake-jwt-token")) -// .andExpect(jsonPath("$.result.user.email").value("test@email.com")) -// .andDo(document("kakao-login-success")); -// -// } -// -// @Test -// void 카카오_로그인_회원가입필요_테스트() throws Exception { -// // given -// KakaoLoginRequest request = new KakaoLoginRequest("code111", "fcmToken111"); -// KakaoLoginResponse response = new KakaoLoginResponse("fake-jwt-token", new KakaoLoginResponse.UserInfo(1L,"test@email.com")); -// -// -// Mockito.when(authService.kakaoLogin(anyString(), anyString())) -// .thenReturn(new SignupNeededResponse("test@email.com", "kakao123")); -// -// -// // when&then -// mockMvc.perform(post("/api/auth/login/kakao") -// .contentType(MediaType.APPLICATION_JSON) -// .content(objectMapper.writeValueAsString(request))) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.code").value("회원가입 필요")) -// .andExpect(jsonPath("$.result.email").value("test@email.com")) -// .andDo(document("kakao-login-need-signup")); -// } -// -// -// -// @Test -// void 애플_로그인_성공_테스트() throws Exception { -// // given -// AppleLoginRequest request = new AppleLoginRequest("code111", "fcmToken111"); -// AppleLoginResponse response = new AppleLoginResponse("fake-jwt-token", new AppleLoginResponse.UserInfo(1L,"test@email.com")); -// -// Mockito.when(authService.appleLogin(anyString(), anyString())).thenReturn(response); -// -// // when & then -// mockMvc.perform(post("/api/auth/login/apple") -// .contentType(MediaType.APPLICATION_JSON) -// .content(objectMapper.writeValueAsString(request))) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.code").value("로그인 성공")) -// .andExpect(jsonPath("$.result.jwtToken").value("fake-jwt-token")) -// .andExpect(jsonPath("$.result.user.email").value("test@email.com")) -// .andDo(document("apple-login-success")); -// } -// -// @Test -// void 애플_로그인_회원가입필요_테스트() throws Exception { -// // given -// AppleLoginRequest request = new AppleLoginRequest("code111", "fcmToken111"); -// AppleLoginResponse response = new AppleLoginResponse("fake-jwt-token", new AppleLoginResponse.UserInfo(1L,"test@email.com")); -// -// -// Mockito.when(authService.appleLogin(anyString(), anyString())) -// .thenReturn(new SignupNeededResponse("test@email.com", "apple123")); -// -// -// -// // when&then -// mockMvc.perform(post("/api/auth/login/apple") -// .contentType(MediaType.APPLICATION_JSON) -// .content(objectMapper.writeValueAsString(request))) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.code").value("회원가입 필요")) -// .andExpect(jsonPath("$.result.email").value("test@email.com")) -// .andDo(print()) -// .andDo(document("apple-login-need-signup")); -// } -// -// -// -// @Test -// void 애플_로그인_토큰파싱_실패_테스트() throws Exception { -// //given -// AppleLoginRequest request = new AppleLoginRequest("code111", "fcmToken111"); -// -// Mockito.when(authService.appleLogin(anyString(), anyString())) -// .thenThrow(new RestApiException(AuthErrorCode.APPLE_ID_TOKEN_PARSE_FAILED)); -// -// //when&then -// mockMvc.perform(post("/api/auth/login/apple") -// .contentType(MediaType.APPLICATION_JSON) -// .content(objectMapper.writeValueAsString(request))) -// .andExpect(status().isUnauthorized()) -// .andExpect(content().string(containsString("AUTH-006"))) -// .andDo(document("apple-login-token-parse-failed")) -// .andDo(print()); -// } -// -// -// -//} \ No newline at end of file +package com.example.matzipbookserver.member.controller; + +import com.example.matzipbookserver.global.exception.RestApiException; +import com.example.matzipbookserver.global.jwt.JwtTokenProvider; +import com.example.matzipbookserver.global.response.error.AuthErrorCode; +import com.example.matzipbookserver.global.response.error.MemberErrorCode; +import com.example.matzipbookserver.member.controller.dto.request.*; +import com.example.matzipbookserver.member.controller.dto.response.*; +import com.example.matzipbookserver.member.service.AuthService; +import com.example.matzipbookserver.member.service.FcmTokenService; +import com.example.matzipbookserver.member.service.MemberService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.anyString; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ExtendWith(RestDocumentationExtension.class) +@WebMvcTest(AuthController.class) +@Import(AuthControllerTest.AuthControllerTestConfig.class) +public class AuthControllerTest { + + @Autowired + private WebApplicationContext context; + + private RestDocumentationResultHandler restDocs; + + @Autowired private MockMvc mockMvc; + @Autowired private ObjectMapper objectMapper; + + @MockBean private AuthService authService; + @MockBean private FcmTokenService fcmTokenService; + @MockBean private JwtTokenProvider jwtTokenProvider; + @MockBean private MemberService memberService; + + @BeforeEach + void setup(RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(documentationConfiguration(restDocumentation)) + .alwaysDo(document("{class-name}/{method-name}")) + .alwaysDo(print()) + .build(); + } + + @TestConfiguration + static class AuthControllerTestConfig { + @Bean + public AuthService authService() { + return Mockito.mock(AuthService.class); + } + + @Bean + public FcmTokenService fcmTokenService() { + return Mockito.mock(FcmTokenService.class); + } + } + + @Test + void 카카오_로그인_성공_테스트() throws Exception { + KakaoLoginRequest request = new KakaoLoginRequest("code111", "fcmToken111"); + KakaoLoginResponse response = new KakaoLoginResponse("fake-jwt-token", new KakaoLoginResponse.UserInfo(1L,"test@email.com")); + + Mockito.when(authService.kakaoLogin(anyString(), anyString())).thenReturn(response); + + mockMvc.perform(post("/api/auth/login/kakao") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("MEMBER-001")) + .andExpect(jsonPath("$.result.jwtToken").value("fake-jwt-token")) + .andExpect(jsonPath("$.result.user.email").value("test@email.com")); + } + + @Test + void 카카오_로그인_회원가입필요_테스트() throws Exception { + KakaoLoginRequest request = new KakaoLoginRequest("code111", "fcmToken111"); + Mockito.when(authService.kakaoLogin(anyString(), anyString())) + .thenReturn(new SignupNeededResponse("test@email.com", "kakao123")); + + mockMvc.perform(post("/api/auth/login/kakao") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("MEMBER-002")) + .andExpect(jsonPath("$.result.email").value("test@email.com")); + } + + @Test + void 애플_로그인_성공_테스트() throws Exception { + AppleLoginRequest request = new AppleLoginRequest("code111", "fcmToken111"); + AppleLoginResponse response = new AppleLoginResponse("fake-jwt-token", new AppleLoginResponse.UserInfo(1L,"test@email.com")); + + Mockito.when(authService.appleLogin(anyString(), anyString())).thenReturn(response); + + mockMvc.perform(post("/api/auth/login/apple") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("MEMBER-001")) + .andExpect(jsonPath("$.result.jwtToken").value("fake-jwt-token")) + .andExpect(jsonPath("$.result.user.email").value("test@email.com")); + } + + @Test + void 애플_로그인_회원가입필요_테스트() throws Exception { + AppleLoginRequest request = new AppleLoginRequest("code111", "fcmToken111"); + Mockito.when(authService.appleLogin(anyString(), anyString())) + .thenReturn(new SignupNeededResponse("test@email.com", "apple123")); + + mockMvc.perform(post("/api/auth/login/apple") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("MEMBER-002")) + .andExpect(jsonPath("$.result.email").value("test@email.com")); + } + + @Test + void 애플_로그인_토큰파싱_실패_테스트() throws Exception { + AppleLoginRequest request = new AppleLoginRequest("code111", "fcmToken111"); + + Mockito.when(authService.appleLogin(anyString(), anyString())) + .thenThrow(new RestApiException(AuthErrorCode.APPLE_ID_TOKEN_PARSE_FAILED)); + + mockMvc.perform(post("/api/auth/login/apple") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isUnauthorized()) + .andExpect(content().string(containsString("AUTH-006"))); + } + + @Test + void 회원가입_성공_테스트() throws Exception { + SignupRequest request = new SignupRequest( + "test@email.com", "kakao", "kakao123", "테스터", "2004-04-21", + "F", "https://image.com/profile.png","부경대학교" + ); + + SignupResponse response = SignupResponse.builder() + .id(1L) + .email("test@email.com") + .nickname("테스터") + .jwtToken("jwt.token.value") + .build(); + + Mockito.when(memberService.signup(Mockito.any())).thenReturn(response); + + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/auth/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("MEMBER-003")) + .andExpect(jsonPath("$.result.email").value("test@email.com")) + .andExpect(jsonPath("$.result.jwtToken").value("jwt.token.value")); + } + + @Test + void 회원가입_중복_실패_테스트() throws Exception { + SignupRequest request = new SignupRequest( + "test@email.com", "apple123", "apple", "테스터", + "2004-04-21", "F", "https://image.com/profile.png", "부경대학교" + ); + + Mockito.when(memberService.signup(Mockito.any())) + .thenThrow(new RestApiException(MemberErrorCode.ALREADY_REGISTERED)); + + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/auth/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(content().string(containsString("MEMBER-001"))); + } +}