diff --git a/README.md b/README.md index a286863c..58f7d82e 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ ## ✈ 기본 + - 클래스 선언부와 필드 사이에 공백 하나 추가한다. - 어노테이션은 클래스 혹은 메서드와 가장 관련된 것을 선언부와 가깝게 한다. -- 객체 필드와 메서드 파라미터에 ```final``` 무조건 붙인다. +- 객체 필드, 메서드 파라미터, 변수 등 재할당이 불가능한 값이면 `final`을 붙인다. - 패키지명은 단수로 한다. -- ```DTO```는 매개변수가 3개 이상일 경우 생성한다. +- `DTO`는 매개변수가 3개 이상일 경우 생성한다. - 생성자 선언 순서 + ``` 1. 기본 생성자 2. 모든 파라미터를 받는 생성자 @@ -17,11 +19,13 @@ ``` ## ✈ Class + - 기능들의 의존성을 낮추기 위해 유지/보수를 위해 도메인 단위로 패키지를 나눈다. - ```application```은 service, ```domain```은 entity/repository, ```presentation```은 controller 객체들을 포함한다. - ```dto```와 ```exception```은 특정 도메인 하위에서 구현한다. + ``` src └ main @@ -47,22 +51,28 @@ src └ ... └ ... ``` + - 객체 네이밍은 다음을 따른다. + ``` Entity(도메인) + Controller/Service/Repository/... ``` ## ✈ Method + ### Presentation Layer + - ```CUD```는 save, update, delete로 통일한다. - 단일건에 대한 ```Read```의 경우에는 파라미터에 따라 정한다. ```findById(final long Id)``` - 복수건에 대한 ```Read```의 경우 메서드 네임에 도메인 복수명과 파라미터에 대해서 작성한다. ```findByMemberId(final long memberId)``` -- ```Read```를 할 때 URL이 /me로 끝나는 경우 My로 시작하는 메서드 네이밍으로 작성한다. ```url : /api/category/me -> findMyCategories(final long MemberId)``` +- ```Read```를 할 때 URL이 /me로 끝나는 경우 My로 시작하는 메서드 네이밍으로 + 작성한다. ```url : /api/category/me -> findMyCategories(final long MemberId)``` ### Service Layer + - ```CUD```는 컨트롤러 메서드와 네이밍을 통일한다. - ```Read```의 경우에는 파라미터에 따라 네이밍을 정한다. ```findByIdAndMemberId(final long Id, final long memberId)``` @@ -72,15 +82,18 @@ Entity(도메인) + Controller/Service/Repository/... - 불가피하게 검증 로직이 필요한 경우 ```validate```로 시작하고 검증하려는 로직에 대해서 적는다. ```validateExistCategory(final long Id)``` ### Repository Layer -- ```Spring Data Jpa```가 지원하는 쿼리 메서드를 작성하는 네이밍 방식과 통일한다. -[[Spring Data Jpa 쿼리 메서드]](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods) + +- ```Spring Data Jpa```가 지원하는 쿼리 메서드를 작성하는 네이밍 방식과 통일한다. + [[Spring Data Jpa 쿼리 메서드]](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods) - 조회에 관한 메서드는 ```get```으로 시작한다. ## ✈ DTO + - ```DTO(Data Transfer Object)```는 ```Request```와 ```Response```로 나누어 제작한다. ```SignUpRequest, SignUpResponse``` ## ✈ Test Code + - 테스트 메서드명은 영어로, ```@DisplayName```을 한글로 작성한다. - 테스트가 어려운 외부 서비스는 Test Double을 적용한다. @@ -90,13 +103,14 @@ Entity(도메인) + Controller/Service/Repository/... - 테스트 클래스의 ```bean```주입은 필드 주입을 사용한다. - ```given, when, then``` 주석을 명시적으로 붙인다. 생략하지 않는다. - - ```given, when, then``` 절을 나누기 곤란한 경우 ```given, when & then```처럼 ```&``` 으로 합쳐서 작성한다. - ``` - // given & when - // when & then - // given & when & then - ``` + - ```given, when, then``` 절을 나누기 곤란한 경우 ```given, when & then```처럼 ```&``` 으로 합쳐서 작성한다. + ``` + // given & when + // when & then + // given & when & then + ``` - 예외 케이스에 대한 테스트 메서드 네이밍은 ```~ 하면 예외가 발생한다.```로 통일한다. + ``` @Test @DisplayName("없는 이미지를 조회하면 예외가 발생한다.") @@ -105,7 +119,9 @@ Entity(도메인) + Controller/Service/Repository/... // when & then } ``` + - 작은 기능 단위로 ```@Nested```를 사용해 클래스로 그룹화한다. + ``` @Nested @DisplayName("카테고리 생성 시") diff --git a/build.gradle b/build.gradle index 8e74b1f1..b7efbb11 100644 --- a/build.gradle +++ b/build.gradle @@ -56,11 +56,22 @@ tasks.named('test') { } asciidoctor { + configurations 'asciidoctorExtensions' + baseDirFollowsSourceFile() + inputs.dir snippetsDir dependsOn test - configurations 'asciidoctorExtentions' } -tasks.named('asciidoctor') { - inputs.dir snippetsDir - dependsOn test +asciidoctor.doFirst { + delete file('src/main/resources/static/docs') +} + +tasks.register('copyDocument', Copy) { + dependsOn asciidoctor + from file("build/docs/asciidoc") + into file("src/main/resources/static/docs") +} + +bootJar { + dependsOn copyDocument } diff --git a/src/main/java/com/coverflow/global/config/SecurityConfig.java b/src/main/java/com/coverflow/global/config/SecurityConfig.java index aa0254c1..78848c81 100644 --- a/src/main/java/com/coverflow/global/config/SecurityConfig.java +++ b/src/main/java/com/coverflow/global/config/SecurityConfig.java @@ -31,6 +31,7 @@ @Configuration @EnableWebSecurity public class SecurityConfig { + private final String[] ALLOWED_URIS = {"/", "/index.html"}; private final LoginService loginService; private final JwtService jwtService; @@ -41,7 +42,9 @@ public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain filterChain( + final HttpSecurity http + ) throws Exception { http .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) @@ -58,19 +61,20 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .successHandler(oAuth2LoginSuccessHandler) // 동의하기 눌렀을 때 핸들러 설정 .failureHandler(oAuth2LoginFailureHandler) // 소셜 로그인 실패 시 핸들러 설정 .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) - ); - - // 원래 스프링 시큐리티 필터 순서가 LogoutFilter 이후에 로그인 필터 동작 - // 따라서, LogoutFilter 이후에 우리가 만든 필터 동작하도록 설정 - // 순서 : LogoutFilter -> JwtAuthenticationProcessingFilter -> CustomJsonUsernamePasswordAuthenticationFilter - http.addFilterAfter(customJsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class); - http.addFilterBefore(jwtAuthenticationFilter(), CustomJsonUsernamePasswordAuthenticationFilter.class); + ) + // 원래 스프링 시큐리티 필터 순서가 LogoutFilter 이후에 로그인 필터 동작 + // 따라서, LogoutFilter 이후에 우리가 만든 필터 동작하도록 설정 + // 순서 : LogoutFilter -> JwtAuthenticationProcessingFilter -> CustomJsonUsernamePasswordAuthenticationFilter + .addFilterAfter(customJsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class) + .addFilterBefore(jwtAuthenticationFilter(), CustomJsonUsernamePasswordAuthenticationFilter.class) + ; return http.build(); } @Bean - public PasswordEncoder passwordEncoder() { + public PasswordEncoder passwordEncoder( + ) { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @@ -82,7 +86,8 @@ public PasswordEncoder passwordEncoder() { * 또한, FormLogin과 동일하게 AuthenticationManager로는 구현체인 ProviderManager 사용(return ProviderManager) */ @Bean - public AuthenticationManager authenticationManager() { + public AuthenticationManager authenticationManager( + ) { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setPasswordEncoder(passwordEncoder()); provider.setUserDetailsService(loginService); @@ -93,7 +98,8 @@ public AuthenticationManager authenticationManager() { * 로그인 성공 시 호출되는 LoginSuccessJWTProviderHandler 빈 등록 */ @Bean - public LoginSuccessHandler loginSuccessHandler() { + public LoginSuccessHandler loginSuccessHandler( + ) { return new LoginSuccessHandler(jwtService, memberRepository); } @@ -101,7 +107,8 @@ public LoginSuccessHandler loginSuccessHandler() { * 로그인 실패 시 호출되는 LoginFailureHandler 빈 등록 */ @Bean - public LoginFailureHandler loginFailureHandler() { + public LoginFailureHandler loginFailureHandler( + ) { return new LoginFailureHandler(); } @@ -112,7 +119,8 @@ public LoginFailureHandler loginFailureHandler() { * 로그인 성공 시 호출할 handler, 실패 시 호출할 handler로 위에서 등록한 handler 설정 */ @Bean - public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordAuthenticationFilter() { + public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordAuthenticationFilter( + ) { CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordLoginFilter = new CustomJsonUsernamePasswordAuthenticationFilter(objectMapper); customJsonUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager()); @@ -122,7 +130,8 @@ public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePassword } @Bean - public JwtAuthenticationFilter jwtAuthenticationFilter() { + public JwtAuthenticationFilter jwtAuthenticationFilter( + ) { return new JwtAuthenticationFilter(jwtService, memberRepository); } diff --git a/src/main/java/com/coverflow/global/entity/BaseTimeEntity.java b/src/main/java/com/coverflow/global/entity/BaseTimeEntity.java index 1c5769da..ba3be6b6 100644 --- a/src/main/java/com/coverflow/global/entity/BaseTimeEntity.java +++ b/src/main/java/com/coverflow/global/entity/BaseTimeEntity.java @@ -22,13 +22,17 @@ public abstract class BaseTimeEntity { private LocalDateTime modifiedAt; @PrePersist - public void prePersist() { + public void prePersist( + ) { + LocalDateTime now = LocalDateTime.now(); createdAt = now; } @PreUpdate - public void preUpdate() { + public void preUpdate( + ) { + modifiedAt = LocalDateTime.now(); } } diff --git a/src/main/java/com/coverflow/global/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/coverflow/global/jwt/filter/JwtAuthenticationFilter.java index 483c7025..7224ca13 100644 --- a/src/main/java/com/coverflow/global/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/coverflow/global/jwt/filter/JwtAuthenticationFilter.java @@ -35,18 +35,22 @@ @Slf4j @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final String NO_CHECK_URL = "/login"; // "/login"으로 들어오는 요청은 Filter 작동 X private final JwtService jwtService; private final MemberRepository memberRepository; private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal( + final HttpServletRequest request, + final HttpServletResponse response, + final FilterChain filterChain + ) throws ServletException, IOException { if (request.getRequestURI().equals(NO_CHECK_URL)) { filterChain.doFilter(request, response); // "/login" 요청이 들어오면, 다음 필터 호출 return; // return으로 이후 현재 필터 진행 막기 (안해주면 아래로 내려가서 계속 필터 진행시킴) } - // 사용자 요청 헤더에서 RefreshToken 추출 // -> RefreshToken이 없거나 유효하지 않다면(DB에 저장된 RefreshToken과 다르다면) null을 반환 // 사용자의 요청 헤더에 RefreshToken이 있는 경우는, AccessToken이 만료되어 요청한 경우밖에 없다. @@ -54,7 +58,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String refreshToken = jwtService.extractRefreshToken(request) .filter(jwtService::isTokenValid) .orElse(null); - // 리프레시 토큰이 요청 헤더에 존재했다면, 사용자가 AccessToken이 만료되어서 // RefreshToken까지 보낸 것이므로 리프레시 토큰이 DB의 리프레시 토큰과 일치하는지 판단 후, // 일치한다면 AccessToken을 재발급해준다. @@ -62,7 +65,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse checkRefreshTokenAndReIssueAccessToken(response, refreshToken); return; // RefreshToken을 보낸 경우에는 AccessToken을 재발급 하고 인증 처리는 하지 않게 하기위해 바로 return으로 필터 진행 막기 } - // RefreshToken이 없거나 유효하지 않다면, AccessToken을 검사하고 인증을 처리하는 로직 수행 // AccessToken이 없거나 유효하지 않다면, 인증 객체가 담기지 않은 상태로 다음 필터로 넘어가기 때문에 403 에러 발생 // AccessToken이 유효하다면, 인증 객체가 담긴 상태로 다음 필터로 넘어가기 때문에 인증 성공 @@ -76,7 +78,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse * reIssueRefreshToken()로 리프레시 토큰 재발급 & DB에 리프레시 토큰 업데이트 메소드 호출 * 그 후 JwtService.sendAccessTokenAndRefreshToken()으로 응답 헤더에 보내기 */ - public void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) { + public void checkRefreshTokenAndReIssueAccessToken( + final HttpServletResponse response, + final String refreshToken + ) { + memberRepository.findByRefreshToken(refreshToken) .ifPresent(user -> { String reIssuedRefreshToken = reIssueRefreshToken(user); @@ -90,7 +96,10 @@ public void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, * jwtService.createRefreshToken()으로 리프레시 토큰 재발급 후 * DB에 재발급한 리프레시 토큰 업데이트 후 Flush */ - private String reIssueRefreshToken(Member member) { + private String reIssueRefreshToken( + final Member member + ) { + String reIssuedRefreshToken = jwtService.createRefreshToken(); member.updateRefreshToken(reIssuedRefreshToken); memberRepository.saveAndFlush(member); @@ -105,8 +114,11 @@ private String reIssueRefreshToken(Member member) { * 인증 허가 처리된 객체를 SecurityContextHolder에 담기 * 그 후 다음 인증 필터로 진행 */ - public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { + public void checkAccessTokenAndAuthentication( + final HttpServletRequest request, + final HttpServletResponse response, + final FilterChain filterChain + ) throws ServletException, IOException { log.info("checkAccessTokenAndAuthentication() 호출"); jwtService.extractAccessToken(request) .filter(jwtService::isTokenValid) @@ -131,7 +143,9 @@ public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpSe * SecurityContextHolder.getContext()로 SecurityContext를 꺼낸 후, * setAuthentication()을 이용하여 위에서 만든 Authentication 객체에 대한 인증 허가 처리 */ - public void saveAuthentication(Member myMember) { + public void saveAuthentication( + final Member myMember + ) { String password = myMember.getPassword(); if (password == null) { // 소셜 로그인 유저의 비밀번호 임의로 설정 하여 소셜 로그인 유저도 인증 되도록 설정 password = PasswordUtil.generateRandomPassword(); diff --git a/src/main/java/com/coverflow/global/jwt/service/JwtService.java b/src/main/java/com/coverflow/global/jwt/service/JwtService.java index 566af3bc..a2c8f414 100644 --- a/src/main/java/com/coverflow/global/jwt/service/JwtService.java +++ b/src/main/java/com/coverflow/global/jwt/service/JwtService.java @@ -19,7 +19,6 @@ @Getter @Service public class JwtService { - /** * JWT의 Subject와 Claim으로 email 사용 -> 클레임의 name을 "email"으로 설정 * JWT의 헤더에 들어오는 값 : 'Authorization(Key) = Bearer {토큰} (Value)' 형식 @@ -43,7 +42,9 @@ public class JwtService { /** * AccessToken 생성 메소드 */ - public String createAccessToken(String email) { + public String createAccessToken( + final String email + ) { Date now = new Date(); return JWT.create() // JWT 토큰을 생성하는 빌더 반환 .withSubject(ACCESS_TOKEN_SUBJECT) // JWT의 Subject 지정 -> AccessToken이므로 AccessToken @@ -60,7 +61,8 @@ public String createAccessToken(String email) { * RefreshToken 생성 * RefreshToken은 Claim에 email도 넣지 않으므로 withClaim() X */ - public String createRefreshToken() { + public String createRefreshToken( + ) { Date now = new Date(); return JWT.create() .withSubject(REFRESH_TOKEN_SUBJECT) @@ -71,7 +73,9 @@ public String createRefreshToken() { /** * AccessToken 헤더에 실어서 보내기 */ - public void sendAccessToken(HttpServletResponse response, String accessToken) { + public void sendAccessToken( + final HttpServletResponse response, final String accessToken + ) { response.setStatus(HttpServletResponse.SC_OK); response.setHeader(accessHeader, accessToken); @@ -81,9 +85,13 @@ public void sendAccessToken(HttpServletResponse response, String accessToken) { /** * AccessToken + RefreshToken 헤더에 실어서 보내기 */ - public void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken) { + public void sendAccessAndRefreshToken( + final HttpServletResponse response, + final String accessToken, + final String refreshToken + ) { response.setStatus(HttpServletResponse.SC_OK); - + System.out.println("refreshToken = " + refreshToken); setAccessTokenHeader(response, accessToken); setRefreshTokenHeader(response, refreshToken); log.info("Access Token, Refresh Token 헤더 설정 완료"); @@ -94,7 +102,9 @@ public void sendAccessAndRefreshToken(HttpServletResponse response, String acces * 토큰 형식 : Bearer XXX에서 Bearer를 제외하고 순수 토큰만 가져오기 위해서 * 헤더를 가져온 후 "Bearer"를 삭제(""로 replace) */ - public Optional extractRefreshToken(HttpServletRequest request) { + public Optional extractRefreshToken( + final HttpServletRequest request + ) { return Optional.ofNullable(request.getHeader(refreshHeader)) .filter(refreshToken -> refreshToken.startsWith(BEARER)) .map(refreshToken -> refreshToken.replace(BEARER, "")); @@ -105,7 +115,9 @@ public Optional extractRefreshToken(HttpServletRequest request) { * 토큰 형식 : Bearer XXX에서 Bearer를 제외하고 순수 토큰만 가져오기 위해서 * 헤더를 가져온 후 "Bearer"를 삭제(""로 replace) */ - public Optional extractAccessToken(HttpServletRequest request) { + public Optional extractAccessToken( + final HttpServletRequest request + ) { return Optional.ofNullable(request.getHeader(accessHeader)) .filter(refreshToken -> refreshToken.startsWith(BEARER)) .map(refreshToken -> refreshToken.replace(BEARER, "")); @@ -118,7 +130,9 @@ public Optional extractAccessToken(HttpServletRequest request) { * 유효하다면 getClaim()으로 이메일 추출 * 유효하지 않다면 빈 Optional 객체 반환 */ - public Optional extractEmail(String accessToken) { + public Optional extractEmail( + final String accessToken + ) { try { // 토큰 유효성 검사하는 데에 사용할 알고리즘이 있는 JWT verifier builder 반환 return Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey)) @@ -135,29 +149,40 @@ public Optional extractEmail(String accessToken) { /** * AccessToken 헤더 설정 */ - public void setAccessTokenHeader(HttpServletResponse response, String accessToken) { + public void setAccessTokenHeader( + final HttpServletResponse response, + final String accessToken + ) { response.setHeader(accessHeader, accessToken); } /** * RefreshToken 헤더 설정 */ - public void setRefreshTokenHeader(HttpServletResponse response, String refreshToken) { + public void setRefreshTokenHeader( + final HttpServletResponse response, + final String refreshToken + ) { response.setHeader(refreshHeader, refreshToken); } /** * RefreshToken DB 저장(업데이트) */ - public void updateRefreshToken(String email, String refreshToken) { + public void updateRefreshToken( + final String email, + final String refreshToken + ) { memberRepository.findByEmail(email) .ifPresentOrElse( - user -> user.updateRefreshToken(refreshToken), + member -> member.updateRefreshToken(refreshToken), () -> new Exception("일치하는 회원이 없습니다.") ); } - public boolean isTokenValid(String token) { + public boolean isTokenValid( + final String token + ) { try { JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token); return true; diff --git a/src/main/java/com/coverflow/global/login/filter/CustomJsonUsernamePasswordAuthenticationFilter.java b/src/main/java/com/coverflow/global/login/filter/CustomJsonUsernamePasswordAuthenticationFilter.java index e78e2d72..05ee1aae 100644 --- a/src/main/java/com/coverflow/global/login/filter/CustomJsonUsernamePasswordAuthenticationFilter.java +++ b/src/main/java/com/coverflow/global/login/filter/CustomJsonUsernamePasswordAuthenticationFilter.java @@ -34,7 +34,9 @@ public class CustomJsonUsernamePasswordAuthenticationFilter extends AbstractAuth private final ObjectMapper objectMapper; - public CustomJsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper) { + public CustomJsonUsernamePasswordAuthenticationFilter( + final ObjectMapper objectMapper + ) { super(DEFAULT_LOGIN_PATH_REQUEST_MATCHER); // 위에서 설정한 "login" + POST로 온 요청을 처리하기 위해 설정 this.objectMapper = objectMapper; } @@ -58,7 +60,10 @@ public CustomJsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper) * (여기서 AuthenticationManager 객체는 ProviderManager -> SecurityConfig에서 설정) */ @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException { + public Authentication attemptAuthentication( + final HttpServletRequest request, + final HttpServletResponse response + ) throws AuthenticationException, IOException { if (request.getContentType() == null || !request.getContentType().equals(CONTENT_TYPE)) { throw new AuthenticationServiceException("Authentication Content-Type not supported: " + request.getContentType()); } diff --git a/src/main/java/com/coverflow/global/login/handler/LoginFailureHandler.java b/src/main/java/com/coverflow/global/login/handler/LoginFailureHandler.java index d0a1d425..d0009263 100644 --- a/src/main/java/com/coverflow/global/login/handler/LoginFailureHandler.java +++ b/src/main/java/com/coverflow/global/login/handler/LoginFailureHandler.java @@ -16,8 +16,11 @@ public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException { + public void onAuthenticationFailure( + final HttpServletRequest request, + final HttpServletResponse response, + final AuthenticationException exception + ) throws IOException { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.setCharacterEncoding("UTF-8"); response.setContentType("text/plain;charset=UTF-8"); diff --git a/src/main/java/com/coverflow/global/login/handler/LoginSuccessHandler.java b/src/main/java/com/coverflow/global/login/handler/LoginSuccessHandler.java index 28c1b25b..23efe752 100644 --- a/src/main/java/com/coverflow/global/login/handler/LoginSuccessHandler.java +++ b/src/main/java/com/coverflow/global/login/handler/LoginSuccessHandler.java @@ -22,8 +22,11 @@ public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private String accessTokenExpiration; @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, - Authentication authentication) { + public void onAuthenticationSuccess( + final HttpServletRequest request, + final HttpServletResponse response, + final Authentication authentication + ) { String email = extractUsername(authentication); // 인증 정보에서 Username(email) 추출 String accessToken = jwtService.createAccessToken(email); // JwtService의 createAccessToken을 사용하여 AccessToken 발급 String refreshToken = jwtService.createRefreshToken(); // JwtService의 createRefreshToken을 사용하여 RefreshToken 발급 @@ -40,7 +43,9 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo log.info("발급된 AccessToken 만료 기간 : {}", accessTokenExpiration); } - private String extractUsername(Authentication authentication) { + private String extractUsername( + final Authentication authentication + ) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); return userDetails.getUsername(); } diff --git a/src/main/java/com/coverflow/global/login/service/LoginService.java b/src/main/java/com/coverflow/global/login/service/LoginService.java index 79e261e5..d8c83145 100644 --- a/src/main/java/com/coverflow/global/login/service/LoginService.java +++ b/src/main/java/com/coverflow/global/login/service/LoginService.java @@ -15,7 +15,9 @@ public class LoginService implements UserDetailsService { private final MemberRepository memberRepository; @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + public UserDetails loadUserByUsername( + final String email + ) throws UsernameNotFoundException { Member user = memberRepository.findByEmail(email) .orElseThrow(() -> new UsernameNotFoundException("해당 이메일이 존재하지 않습니다.")); diff --git a/src/main/java/com/coverflow/global/oauth2/CustomOAuth2User.java b/src/main/java/com/coverflow/global/oauth2/CustomOAuth2User.java index 2472404a..46a09897 100644 --- a/src/main/java/com/coverflow/global/oauth2/CustomOAuth2User.java +++ b/src/main/java/com/coverflow/global/oauth2/CustomOAuth2User.java @@ -22,9 +22,13 @@ public class CustomOAuth2User extends DefaultOAuth2User { * @param nameAttributeKey the key used to access the user's "name" from * {@link #getAttributes()} */ - public CustomOAuth2User(Collection authorities, - Map attributes, String nameAttributeKey, - String email, Role role) { + public CustomOAuth2User( + final Collection authorities, + final Map attributes, + final String nameAttributeKey, + final String email, + final Role role + ) { super(authorities, attributes, nameAttributeKey); this.email = email; this.role = role; diff --git a/src/main/java/com/coverflow/global/oauth2/OAuthAttributes.java b/src/main/java/com/coverflow/global/oauth2/OAuthAttributes.java index 7e44b353..31d36d45 100644 --- a/src/main/java/com/coverflow/global/oauth2/OAuthAttributes.java +++ b/src/main/java/com/coverflow/global/oauth2/OAuthAttributes.java @@ -24,7 +24,10 @@ public class OAuthAttributes { private final OAuth2UserInfo oauth2UserInfo; // 소셜 타입별 로그인 유저 정보(닉네임, 이메일, 프로필 사진 등등) @Builder - private OAuthAttributes(String nameAttributeKey, OAuth2UserInfo oauth2UserInfo) { + private OAuthAttributes( + final String nameAttributeKey, + final OAuth2UserInfo oauth2UserInfo + ) { this.nameAttributeKey = nameAttributeKey; this.oauth2UserInfo = oauth2UserInfo; } @@ -35,9 +38,11 @@ private OAuthAttributes(String nameAttributeKey, OAuth2UserInfo oauth2UserInfo) * 소셜별 of 메소드(ofGoogle, ofKaKao, ofNaver)들은 각각 소셜 로그인 API에서 제공하는 * 회원의 식별값(id), attributes, nameAttributeKey를 저장 후 build */ - public static OAuthAttributes of(SocialType socialType, - String userNameAttributeName, Map attributes) { - + public static OAuthAttributes of( + final SocialType socialType, + final String userNameAttributeName, + final Map attributes + ) { if (socialType == SocialType.NAVER) { return ofNaver(userNameAttributeName, attributes); } @@ -47,21 +52,30 @@ public static OAuthAttributes of(SocialType socialType, return ofGoogle(userNameAttributeName, attributes); } - public static OAuthAttributes ofNaver(String userNameAttributeName, Map attributes) { + public static OAuthAttributes ofNaver( + final String userNameAttributeName, + final Map attributes + ) { return OAuthAttributes.builder() .nameAttributeKey(userNameAttributeName) .oauth2UserInfo(new NaverOAuth2UserInfo(attributes)) .build(); } - private static OAuthAttributes ofKakao(String userNameAttributeName, Map attributes) { + private static OAuthAttributes ofKakao( + final String userNameAttributeName, + final Map attributes + ) { return OAuthAttributes.builder() .nameAttributeKey(userNameAttributeName) .oauth2UserInfo(new KakaoOAuth2UserInfo(attributes)) .build(); } - public static OAuthAttributes ofGoogle(String userNameAttributeName, Map attributes) { + public static OAuthAttributes ofGoogle( + final String userNameAttributeName, + final Map attributes + ) { return OAuthAttributes.builder() .nameAttributeKey(userNameAttributeName) .oauth2UserInfo(new GoogleOAuth2UserInfo(attributes)) @@ -74,12 +88,21 @@ public static OAuthAttributes ofGoogle(String userNameAttributeName, Map new IllegalArgumentException("이메일에 해당하는 유저가 없습니다.")); -// findUser.authorizeUser(); } else { loginSuccess(response, oAuth2User); // 로그인에 성공한 경우 access, refresh 토큰 생성 } } - // TODO : 소셜 로그인 시에도 무조건 토큰 생성하지 말고 JWT 인증 필터처럼 RefreshToken 유/무에 따라 다르게 처리해보기 - private void loginSuccess(HttpServletResponse response, CustomOAuth2User oAuth2User) { + // 소셜 로그인 시에도 무조건 토큰 생성하지 말고 JWT 인증 필터처럼 RefreshToken 유/무에 따라 다르게 처리해보기 + private void loginSuccess( + final HttpServletResponse response, + final CustomOAuth2User oAuth2User + ) { String accessToken = jwtService.createAccessToken(oAuth2User.getEmail()); String refreshToken = jwtService.createRefreshToken(); response.addHeader(jwtService.getAccessHeader(), "Bearer " + accessToken); diff --git a/src/main/java/com/coverflow/global/oauth2/presentation/OAuth2LoginController.java b/src/main/java/com/coverflow/global/oauth2/presentation/OAuth2LoginController.java new file mode 100644 index 00000000..f27f7c8b --- /dev/null +++ b/src/main/java/com/coverflow/global/oauth2/presentation/OAuth2LoginController.java @@ -0,0 +1,11 @@ +package com.coverflow.global.oauth2.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/login/oauth") +@RestController +public class OAuth2LoginController { +} diff --git a/src/main/java/com/coverflow/global/oauth2/service/CustomOAuth2UserService.java b/src/main/java/com/coverflow/global/oauth2/service/CustomOAuth2UserService.java index cea6e589..1f52b2d7 100644 --- a/src/main/java/com/coverflow/global/oauth2/service/CustomOAuth2UserService.java +++ b/src/main/java/com/coverflow/global/oauth2/service/CustomOAuth2UserService.java @@ -28,7 +28,9 @@ public class CustomOAuth2UserService implements OAuth2UserService attributes) { + public GoogleOAuth2UserInfo( + final Map attributes + ) { super(attributes); } diff --git a/src/main/java/com/coverflow/global/oauth2/userinfo/KakaoOAuth2UserInfo.java b/src/main/java/com/coverflow/global/oauth2/userinfo/KakaoOAuth2UserInfo.java index 2f121cc8..b8b20f53 100644 --- a/src/main/java/com/coverflow/global/oauth2/userinfo/KakaoOAuth2UserInfo.java +++ b/src/main/java/com/coverflow/global/oauth2/userinfo/KakaoOAuth2UserInfo.java @@ -4,7 +4,9 @@ public class KakaoOAuth2UserInfo extends OAuth2UserInfo { - public KakaoOAuth2UserInfo(Map attributes) { + public KakaoOAuth2UserInfo( + final Map attributes + ) { super(attributes); } diff --git a/src/main/java/com/coverflow/global/oauth2/userinfo/NaverOAuth2UserInfo.java b/src/main/java/com/coverflow/global/oauth2/userinfo/NaverOAuth2UserInfo.java index 98678c07..08856da1 100644 --- a/src/main/java/com/coverflow/global/oauth2/userinfo/NaverOAuth2UserInfo.java +++ b/src/main/java/com/coverflow/global/oauth2/userinfo/NaverOAuth2UserInfo.java @@ -4,7 +4,9 @@ public class NaverOAuth2UserInfo extends OAuth2UserInfo { - public NaverOAuth2UserInfo(Map attributes) { + public NaverOAuth2UserInfo( + final Map attributes + ) { super(attributes); } diff --git a/src/main/java/com/coverflow/global/oauth2/userinfo/OAuth2UserInfo.java b/src/main/java/com/coverflow/global/oauth2/userinfo/OAuth2UserInfo.java index 0b0fbf39..43422c00 100644 --- a/src/main/java/com/coverflow/global/oauth2/userinfo/OAuth2UserInfo.java +++ b/src/main/java/com/coverflow/global/oauth2/userinfo/OAuth2UserInfo.java @@ -4,9 +4,11 @@ public abstract class OAuth2UserInfo { - protected Map attributes; + final protected Map attributes; - public OAuth2UserInfo(Map attributes) { + public OAuth2UserInfo( + final Map attributes + ) { this.attributes = attributes; } diff --git a/src/main/java/com/coverflow/global/util/PasswordUtil.java b/src/main/java/com/coverflow/global/util/PasswordUtil.java index efd5ce1c..88ee835f 100644 --- a/src/main/java/com/coverflow/global/util/PasswordUtil.java +++ b/src/main/java/com/coverflow/global/util/PasswordUtil.java @@ -3,7 +3,10 @@ import java.util.Random; public class PasswordUtil { - public static String generateRandomPassword() { + + public static String generateRandomPassword( + + ) { int index = 0; char[] charSet = new char[]{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', diff --git a/src/main/java/com/coverflow/member/application/MemberService.java b/src/main/java/com/coverflow/member/application/MemberService.java index 08037034..0585c2c5 100644 --- a/src/main/java/com/coverflow/member/application/MemberService.java +++ b/src/main/java/com/coverflow/member/application/MemberService.java @@ -2,52 +2,73 @@ import com.coverflow.member.domain.Member; import com.coverflow.member.domain.MemberRepository; -import com.coverflow.member.domain.Role; -import com.coverflow.member.dto.MemberSignUpDTO; -import com.coverflow.member.dto.request.DuplicationNicknameRequest; -import com.coverflow.member.dto.response.DuplicationNicknameResponse; -import jakarta.transaction.Transactional; +import com.coverflow.member.dto.request.MemberSaveMemberInfoRequest; +import com.coverflow.member.dto.request.MemberVerifyDuplicationNicknameRequest; +import com.coverflow.member.dto.response.MemberVerifyDuplicationNicknameResponse; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; +import lombok.extern.log4j.Log4j2; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +@Log4j2 @RequiredArgsConstructor @Transactional @Service public class MemberService { + private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; - public void signUp(final MemberSignUpDTO memberSignUpDTO) throws Exception { - if (memberRepository.findByEmail(memberSignUpDTO.getEmail()).isPresent()) { - throw new Exception("이미 존재하는 이메일입니다."); - } - - if (memberRepository.findByNickname(memberSignUpDTO.getNickname()).isPresent()) { - throw new Exception("이미 존재하는 닉네임입니다."); - } - - Member member = Member.builder() - .email(memberSignUpDTO.getEmail()) - .password(memberSignUpDTO.getPassword()) - .nickname(memberSignUpDTO.getNickname()) - .age(memberSignUpDTO.getAge()) - .role(Role.MEMBER) - .build(); - - member.passwordEncode(passwordEncoder); - memberRepository.save(member); +// public void signUp(final MemberSignUpDTO memberSignUpDTO) throws Exception { +// if (memberRepository.findByEmail(memberSignUpDTO.getEmail()).isPresent()) { +// throw new Exception("이미 존재하는 이메일입니다."); +// } +// +// if (memberRepository.findByNickname(memberSignUpDTO.getNickname()).isPresent()) { +// throw new Exception("이미 존재하는 닉네임입니다."); +// } +// +// final Member member = Member.builder() +// .email(memberSignUpDTO.getEmail()) +// .password(memberSignUpDTO.getPassword()) +// .nickname(memberSignUpDTO.getNickname()) +// .age(memberSignUpDTO.getAge()) +// .role(Role.MEMBER) +// .build(); +// +// member.passwordEncode(passwordEncoder); +// memberRepository.save(member); +// } + + @Transactional(readOnly = true) + public MemberVerifyDuplicationNicknameResponse verifyDuplicationNickname( + final MemberVerifyDuplicationNicknameRequest request + ) { + final AtomicBoolean result = new AtomicBoolean(false); + + memberRepository.findByNickname(request.nickname()) + .ifPresentOrElse( + member -> result.set(false), + () -> result.set(true) + ); + +// if (nickname.isPresent()) { +// return new DuplicationNicknameResponse(HttpStatus.OK, HttpStatus.OK.toString(), "이미 존재하는 닉네임입니다."); +// } +// return new DuplicationNicknameResponse(HttpStatus.OK, HttpStatus.OK.toString(), "사용 가능한 닉네임입니다."); + return MemberVerifyDuplicationNicknameResponse.of(result.get()); } - public DuplicationNicknameResponse verifyDuplicationNickname(final DuplicationNicknameRequest request) { - final Optional nickname = memberRepository.findByNickname(request.nickname()); - - if (nickname.isPresent()) { - return new DuplicationNicknameResponse(HttpStatus.OK, HttpStatus.OK.toString(), "이미 존재하는 닉네임입니다."); - } - return new DuplicationNicknameResponse(HttpStatus.OK, HttpStatus.OK.toString(), "사용 가능한 닉네임입니다."); + public void saveMemberInfo( + final String username, + final MemberSaveMemberInfoRequest request + ) { + final Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new IllegalArgumentException("일치하는 회원이 없습니다.")); + + member.saveMemberInfo(request); } } diff --git a/src/main/java/com/coverflow/member/domain/Member.java b/src/main/java/com/coverflow/member/domain/Member.java index a4cd1118..6ea1a7e0 100644 --- a/src/main/java/com/coverflow/member/domain/Member.java +++ b/src/main/java/com/coverflow/member/domain/Member.java @@ -1,6 +1,7 @@ package com.coverflow.member.domain; import com.coverflow.global.entity.BaseEntity; +import com.coverflow.member.dto.request.MemberSaveMemberInfoRequest; import jakarta.persistence.*; import lombok.*; import org.springframework.security.crypto.password.PasswordEncoder; @@ -15,6 +16,7 @@ @Entity @Table(name = "tbl_member") public class Member extends BaseEntity { + @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID member_id; @@ -22,10 +24,13 @@ public class Member extends BaseEntity { private String email; private String nickname; private String tag; + private String age; private String gender; - private int age; + private int fishShapedBun; private String status; private LocalDateTime lastLoginTime; + private String socialId; // 로그인한 소셜 타입의 식별자 값 (일반 로그인인 경우 null) + private String refreshToken; // 리프레시 토큰 @Enumerated(EnumType.STRING) private Role role; @@ -33,32 +38,48 @@ public class Member extends BaseEntity { @Enumerated(EnumType.STRING) private SocialType socialType; // KAKAO, NAVER, GOOGLE - private String socialId; // 로그인한 소셜 타입의 식별자 값 (일반 로그인인 경우 null) - - private String refreshToken; // 리프레시 토큰 - // 유저 권한 설정 메소드 public void authorizeMember() { this.role = Role.MEMBER; } - public void passwordEncode(PasswordEncoder passwordEncoder) { + public void passwordEncode( + final PasswordEncoder passwordEncoder + ) { this.password = passwordEncoder.encode(this.password); } - public void updateNickname(String updateNickname) { + public void saveMemberInfo( + final MemberSaveMemberInfoRequest request + ) { + this.nickname = request.nickname(); + this.tag = request.tag(); + this.age = request.age(); + this.gender = request.gender(); + } + + public void updateNickname( + final String updateNickname + ) { this.nickname = updateNickname; } - public void updateAge(int updateAge) { + public void updateAge( + final String updateAge + ) { this.age = updateAge; } - public void updatePassword(String updatePassword, PasswordEncoder passwordEncoder) { + public void updatePassword( + final String updatePassword, + final PasswordEncoder passwordEncoder + ) { this.password = passwordEncoder.encode(updatePassword); } - public void updateRefreshToken(String updateRefreshToken) { + public void updateRefreshToken( + final String updateRefreshToken + ) { this.refreshToken = updateRefreshToken; } } diff --git a/src/main/java/com/coverflow/member/domain/MemberRepository.java b/src/main/java/com/coverflow/member/domain/MemberRepository.java index 3792e9db..bf27955b 100644 --- a/src/main/java/com/coverflow/member/domain/MemberRepository.java +++ b/src/main/java/com/coverflow/member/domain/MemberRepository.java @@ -6,11 +6,12 @@ import java.util.UUID; public interface MemberRepository extends JpaRepository { - Optional findByEmail(String email); - Optional findByNickname(String nickname); + Optional findByEmail(final String email); - Optional findByRefreshToken(String refreshToken); + Optional findByNickname(final String nickname); + + Optional findByRefreshToken(final String refreshToken); /** * 소셜 타입과 소셜의 식별값으로 회원 찾는 메소드 @@ -18,6 +19,6 @@ public interface MemberRepository extends JpaRepository { * 유저 객체는 DB에 있지만, 추가 정보가 빠진 상태이다. * 따라서 추가 정보를 입력받아 회원 가입을 진행할 때 소셜 타입, 식별자로 해당 회원을 찾기 위한 메소드 */ - Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId); + Optional findBySocialTypeAndSocialId(final SocialType socialType, final String socialId); } diff --git a/src/main/java/com/coverflow/member/domain/SocialType.java b/src/main/java/com/coverflow/member/domain/SocialType.java index da079501..cc3a1913 100644 --- a/src/main/java/com/coverflow/member/domain/SocialType.java +++ b/src/main/java/com/coverflow/member/domain/SocialType.java @@ -1,6 +1,7 @@ package com.coverflow.member.domain; public enum SocialType { + KAKAO, NAVER, GOOGLE } diff --git a/src/main/java/com/coverflow/member/dto/MemberSignUpDTO.java b/src/main/java/com/coverflow/member/dto/MemberSignUpDTO.java index 127ed35a..756f1d5c 100644 --- a/src/main/java/com/coverflow/member/dto/MemberSignUpDTO.java +++ b/src/main/java/com/coverflow/member/dto/MemberSignUpDTO.java @@ -6,6 +6,7 @@ @NoArgsConstructor @Getter public class MemberSignUpDTO { + private String email; private String password; private String nickname; diff --git a/src/main/java/com/coverflow/member/dto/request/MemberSaveMemberInfoRequest.java b/src/main/java/com/coverflow/member/dto/request/MemberSaveMemberInfoRequest.java new file mode 100644 index 00000000..435ea743 --- /dev/null +++ b/src/main/java/com/coverflow/member/dto/request/MemberSaveMemberInfoRequest.java @@ -0,0 +1,9 @@ +package com.coverflow.member.dto.request; + +public record MemberSaveMemberInfoRequest( + String nickname, + String tag, + String age, + String gender +) { +} diff --git a/src/main/java/com/coverflow/member/dto/request/DuplicationNicknameRequest.java b/src/main/java/com/coverflow/member/dto/request/MemberVerifyDuplicationNicknameRequest.java similarity index 57% rename from src/main/java/com/coverflow/member/dto/request/DuplicationNicknameRequest.java rename to src/main/java/com/coverflow/member/dto/request/MemberVerifyDuplicationNicknameRequest.java index 7bf3f621..9d9416dc 100644 --- a/src/main/java/com/coverflow/member/dto/request/DuplicationNicknameRequest.java +++ b/src/main/java/com/coverflow/member/dto/request/MemberVerifyDuplicationNicknameRequest.java @@ -1,6 +1,6 @@ package com.coverflow.member.dto.request; -public record DuplicationNicknameRequest( +public record MemberVerifyDuplicationNicknameRequest( String nickname ) { } diff --git a/src/main/java/com/coverflow/member/dto/response/DuplicationNicknameResponse.java b/src/main/java/com/coverflow/member/dto/response/DuplicationNicknameResponse.java deleted file mode 100644 index f3362c60..00000000 --- a/src/main/java/com/coverflow/member/dto/response/DuplicationNicknameResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.coverflow.member.dto.response; - -import org.springframework.http.HttpStatus; - -public record DuplicationNicknameResponse( - HttpStatus status, - String message, - String data -) { -} diff --git a/src/main/java/com/coverflow/member/dto/response/MemberVerifyDuplicationNicknameResponse.java b/src/main/java/com/coverflow/member/dto/response/MemberVerifyDuplicationNicknameResponse.java new file mode 100644 index 00000000..f3f410fc --- /dev/null +++ b/src/main/java/com/coverflow/member/dto/response/MemberVerifyDuplicationNicknameResponse.java @@ -0,0 +1,11 @@ +package com.coverflow.member.dto.response; + +public record MemberVerifyDuplicationNicknameResponse( + boolean result +) { + public static MemberVerifyDuplicationNicknameResponse of(final boolean result) { + return new MemberVerifyDuplicationNicknameResponse( + result + ); + } +} diff --git a/src/main/java/com/coverflow/member/presentation/MemberController.java b/src/main/java/com/coverflow/member/presentation/MemberController.java index 573e6f81..ccacd076 100644 --- a/src/main/java/com/coverflow/member/presentation/MemberController.java +++ b/src/main/java/com/coverflow/member/presentation/MemberController.java @@ -1,24 +1,37 @@ package com.coverflow.member.presentation; import com.coverflow.member.application.MemberService; -import com.coverflow.member.dto.request.DuplicationNicknameRequest; -import com.coverflow.member.dto.response.DuplicationNicknameResponse; +import com.coverflow.member.dto.request.MemberSaveMemberInfoRequest; +import com.coverflow.member.dto.request.MemberVerifyDuplicationNicknameRequest; +import com.coverflow.member.dto.response.MemberVerifyDuplicationNicknameResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RequestMapping("/api/member") @RestController public class MemberController { + private final MemberService memberService; @GetMapping("/verify-duplication-nickname") - public ResponseEntity verifyDuplicationNickname(@RequestBody final DuplicationNicknameRequest request) { - final DuplicationNicknameResponse duplicationNicknameResponse = memberService.verifyDuplicationNickname(request); - return ResponseEntity.ok(duplicationNicknameResponse); + public ResponseEntity verifyDuplicationNickname( + @RequestBody @Valid final MemberVerifyDuplicationNicknameRequest request + ) { + MemberVerifyDuplicationNicknameResponse duplicationNicknameResponse = memberService.verifyDuplicationNickname(request); + return ResponseEntity.ok().body(duplicationNicknameResponse); + } + + @PostMapping("/save-member-info") + public ResponseEntity saveMemberInfo( + @AuthenticationPrincipal UserDetails userDetails, + @RequestBody @Valid final MemberSaveMemberInfoRequest request + ) { + memberService.saveMemberInfo(userDetails.getUsername(), request); + return ResponseEntity.ok().build(); } }