diff --git a/build.gradle b/build.gradle index 6dd1f49ee..3c0aeb7bb 100644 --- a/build.gradle +++ b/build.gradle @@ -26,15 +26,25 @@ repositories { dependencies { // Spring Boot 스타터 implementation 'org.springframework.boot:spring-boot-starter-data-jpa' -// implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + //Security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + + // OAuth2 + implementation 'org.springframework.security:spring-security-oauth2-client' + // Runtime DB 드라이버 runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' @@ -42,7 +52,7 @@ dependencies { // Test testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' -// testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.springframework.security:spring-security-test' testImplementation 'com.h2database:h2:2.1.214' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -55,16 +65,10 @@ dependencies { def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile -// QueryDSL Q-클래스는 compileJava 단계에서만 생성 -tasks.named('compileJava', org.gradle.api.tasks.compile.JavaCompile) { +tasks.withType(JavaCompile).configureEach { options.generatedSourceOutputDirectory.set(querydslDir) } -// test 컴파일 단계에서는 어노테이션 프로세서를 비워서 Q-클래스 중복 생성 방지 -tasks.named('compileTestJava', org.gradle.api.tasks.compile.JavaCompile) { - options.annotationProcessorPath = files() -} - sourceSets { main.java.srcDirs += [ querydslDir ] } diff --git a/src/main/java/konkuk/thip/common/exception/AuthException.java b/src/main/java/konkuk/thip/common/exception/AuthException.java new file mode 100644 index 000000000..8c2709f1f --- /dev/null +++ b/src/main/java/konkuk/thip/common/exception/AuthException.java @@ -0,0 +1,18 @@ +package konkuk.thip.common.exception; + +import konkuk.thip.common.exception.code.ErrorCode; +import lombok.Getter; + +@Getter +public class AuthException extends RuntimeException { + private final ErrorCode errorCode; + + public AuthException(ErrorCode errorCode) { + this.errorCode = errorCode; + } + + public AuthException(ErrorCode errorCode, Exception e) { + super(e); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index b538f025d..5fadd59cd 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -16,6 +16,13 @@ public enum ErrorCode implements ResponseCode { API_INVALID_PARAM(HttpStatus.BAD_REQUEST, 40002, "파라미터 값 중 유효하지 않은 값이 있습니다."), API_INVALID_TYPE(HttpStatus.BAD_REQUEST, 40003, "파라미터 타입이 잘못되었습니다."), + AUTH_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, 40100, "유효하지 않은 토큰입니다."), + AUTH_EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, 40101, "만료된 토큰입니다."), + AUTH_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 40102, "인증되지 않은 사용자입니다."), + AUTH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, 40103, "토큰을 찾을 수 없습니다."), + AUTH_LOGIN_FAILED(HttpStatus.UNAUTHORIZED, 40104, "로그인에 실패했습니다."), + AUTH_UNSUPPORTED_SOCIAL_LOGIN(HttpStatus.UNAUTHORIZED, 40105, "지원하지 않는 소셜 로그인입니다."), + /* 60000부터 비즈니스 예외 */ /** * 60000 : alias error diff --git a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java index e6d0671d7..58ac9d5f2 100644 --- a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package konkuk.thip.common.exception.handler; import konkuk.thip.common.dto.ErrorResponse; +import konkuk.thip.common.exception.AuthException; import konkuk.thip.common.exception.BusinessException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -71,6 +72,15 @@ public ResponseEntity missingServletRequestParameterExceptionHand .body(ErrorResponse.of(API_MISSING_PARAM, e.getParameterName() + "를 추가해서 요청해주세요.")); } + // 인증, 인가 권한 관련 예외 처리 + @ExceptionHandler(AuthException.class) + public ResponseEntity authExceptionHandler(AuthException e) { + log.error("[AuthExceptionHandler] {}", e.getMessage()); + return ResponseEntity + .status(e.getErrorCode().getHttpStatus()) + .body(ErrorResponse.of(e.getErrorCode())); + } + // 비즈니스 로직에서 발생한 예외 처리 @ExceptionHandler(BusinessException.class) public ResponseEntity businessExceptionHandler(BusinessException e) { @@ -81,12 +91,21 @@ public ResponseEntity businessExceptionHandler(BusinessException } // 서버 내부 오류 예외 처리 - @ExceptionHandler({RuntimeException.class, IllegalStateException.class}) - public ResponseEntity runtimeExceptionHandler(Exception e) { + @ExceptionHandler(RuntimeException.class) + public ResponseEntity runtimeExceptionHandler(RuntimeException e) { log.error("[RuntimeExceptionHandler] {}", e.getMessage()); return ResponseEntity .status(API_SERVER_ERROR.getHttpStatus()) .body(ErrorResponse.of(API_SERVER_ERROR)); } + // IllegalStateException 예외 처리 + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity illegalStateExceptionHandler(IllegalStateException e) { + log.error("[IllegalStateExceptionHandler] {}", e.getMessage()); + return ResponseEntity + .status(API_SERVER_ERROR.getHttpStatus()) + .body(ErrorResponse.of(API_SERVER_ERROR)); + } + } diff --git a/src/main/java/konkuk/thip/common/security/annotation/Oauth2Id.java b/src/main/java/konkuk/thip/common/security/annotation/Oauth2Id.java new file mode 100644 index 000000000..65ac29d59 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/annotation/Oauth2Id.java @@ -0,0 +1,11 @@ +package konkuk.thip.common.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Oauth2Id { +} diff --git a/src/main/java/konkuk/thip/common/security/annotation/UserId.java b/src/main/java/konkuk/thip/common/security/annotation/UserId.java new file mode 100644 index 000000000..c8ef09c18 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/annotation/UserId.java @@ -0,0 +1,11 @@ +package konkuk.thip.common.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserId { +} diff --git a/src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java b/src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java new file mode 100644 index 000000000..34d590e14 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java @@ -0,0 +1,38 @@ +package konkuk.thip.common.security.argument_resolver; + +import jakarta.servlet.http.HttpServletRequest; +import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.security.annotation.Oauth2Id; +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; + +import static konkuk.thip.common.exception.code.ErrorCode.AUTH_TOKEN_NOT_FOUND; + +@Component +@RequiredArgsConstructor +public class Oauth2IdArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Oauth2Id.class) + && parameter.getParameterType().equals(String.class); + } + + @Override + public String resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + + Object oauth2Id = ((HttpServletRequest) webRequest.getNativeRequest()).getAttribute("oauth2Id"); + if (oauth2Id == null) { + throw new AuthException(AUTH_TOKEN_NOT_FOUND); + } + return (String) oauth2Id; + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java b/src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java new file mode 100644 index 000000000..f2b8a7d06 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java @@ -0,0 +1,38 @@ +package konkuk.thip.common.security.argument_resolver; + +import jakarta.servlet.http.HttpServletRequest; +import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.security.annotation.UserId; +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; + +import static konkuk.thip.common.exception.code.ErrorCode.AUTH_TOKEN_NOT_FOUND; + +@Component +@RequiredArgsConstructor +public class UserIdArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(UserId.class) + && parameter.getParameterType().equals(Long.class); + } + + @Override + public Long resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + + Object userId = ((HttpServletRequest) webRequest.getNativeRequest()).getAttribute("userId"); + if (userId == null) { + throw new AuthException(AUTH_TOKEN_NOT_FOUND); + } + return (Long) userId; + } +} diff --git a/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java b/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java new file mode 100644 index 000000000..3349db319 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java @@ -0,0 +1,23 @@ +package konkuk.thip.common.security.constant; + +import lombok.Getter; + +@Getter +public enum AuthParameters { + JWT_HEADER_KEY("Authorization"), + JWT_PREFIX("Bearer "), + KAKAO("kakao"), + GOOGLE("google"), + KAKAO_PROVIDER_ID_KEY("id"), + GOOGLE_PROVIDER_ID_KEY("sub"), + JWT_ACCESS_TOKEN_KEY("userId"), + JWT_SIGNUP_TOKEN_KEY("oauth2Id"), + ; + + private final String value; + + AuthParameters(String value) { + this.value = value; + } +} + diff --git a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java new file mode 100644 index 000000000..b52dfa329 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java @@ -0,0 +1,30 @@ +package konkuk.thip.common.security.filter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import java.io.IOException; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + private final HandlerExceptionResolver resolver; + + public JwtAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver){ + this.resolver = resolver; + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + Exception e = (Exception) request.getAttribute("exception"); + if(e == null){ + e = authException; + } + resolver.resolveException(request, response, null, e); + } +} diff --git a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 000000000..abcb45aa5 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,79 @@ +package konkuk.thip.common.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.security.oauth2.CustomOAuth2User; +import konkuk.thip.common.security.oauth2.LoginUser; +import konkuk.thip.common.security.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static konkuk.thip.common.exception.code.ErrorCode.*; +import static konkuk.thip.common.security.constant.AuthParameters.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + String token = extractToken(request); + if (token == null) { + throw new AuthException(AUTH_TOKEN_NOT_FOUND); + } + + if (!jwtUtil.validateToken(token)) { + throw new AuthException(AUTH_INVALID_TOKEN); + } + + if (jwtUtil.isExpired(token)) { + throw new AuthException(AUTH_EXPIRED_TOKEN); + } + + LoginUser loginUser = jwtUtil.getLoginUser(token); + + if (loginUser.userId() != null) { + request.setAttribute(JWT_ACCESS_TOKEN_KEY.getValue(), loginUser.userId()); + } + else { + request.setAttribute(JWT_SIGNUP_TOKEN_KEY.getValue(), loginUser.oauth2Id()); + } + + CustomOAuth2User customOAuth2User = new CustomOAuth2User(loginUser); + + Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authToken); + } catch (Exception e) { + log.error("JWT 필터에서 오류 발생: {}", e.getMessage()); + request.setAttribute("exception", e); + } finally { + filterChain.doFilter(request, response); + } + } + + private String extractToken(HttpServletRequest request) { + String authorization = request.getHeader(JWT_HEADER_KEY.getValue()); + if (authorization != null && authorization.startsWith(JWT_PREFIX.getValue())) { + return authorization.split(" ")[1]; + } + log.info("토큰이 없습니다."); + return null; + } + +} diff --git a/src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2User.java b/src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2User.java new file mode 100644 index 000000000..7b675a92a --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2User.java @@ -0,0 +1,48 @@ +package konkuk.thip.common.security.oauth2; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +@Getter +@RequiredArgsConstructor +public class CustomOAuth2User implements OAuth2User { + + private final LoginUser loginUser; + + @Override + public Map getAttributes() { + return null; + } + + @Override + public String getName() { + return loginUser.oauth2Id(); + } + + @Override + public Collection getAuthorities() { + Collection authorities = new ArrayList<>(); + authorities.add(new GrantedAuthority() { + @Override + public String getAuthority() { + return "NORMAL_USER"; // 임시 사용자 권한 + } + }); + return authorities; + } + + public String getOauth2Id() { + return loginUser.oauth2Id(); + } + + public boolean isNewUser() { + return loginUser.isNewUser(); + } + +} diff --git a/src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserService.java b/src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserService.java new file mode 100644 index 000000000..e6deabd7a --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserService.java @@ -0,0 +1,58 @@ +package konkuk.thip.common.security.oauth2; + +import konkuk.thip.common.exception.AuthException; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +import static konkuk.thip.common.exception.code.ErrorCode.AUTH_UNSUPPORTED_SOCIAL_LOGIN; +import static konkuk.thip.common.security.constant.AuthParameters.GOOGLE; +import static konkuk.thip.common.security.constant.AuthParameters.KAKAO; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final UserJpaRepository userJpaRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + + OAuth2User oAuth2User = super.loadUser(userRequest); + + log.info("OAuth2User: {}", oAuth2User.getAttributes()); + + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + OAuth2UserDetails oAuth2UserDetails = null; + if (registrationId.equals(KAKAO.getValue())) { + oAuth2UserDetails = new KakaoUserDetails(oAuth2User.getAttributes()); + } + else if (registrationId.equals(GOOGLE.getValue())) { + oAuth2UserDetails = new GoogleUserDetails(oAuth2User.getAttributes()); + } + else { + log.warn("카카오 또는 구글 소셜 로그인만 지원합니다."); + throw new AuthException(AUTH_UNSUPPORTED_SOCIAL_LOGIN); + } + + String oauth2Id = oAuth2UserDetails.getProvider() + "_" + oAuth2UserDetails.getProviderId(); //kakao_1234567890 + Optional existingUser = userJpaRepository.findByOauth2Id(oauth2Id); + if(existingUser.isEmpty()) { + LoginUser newUser = LoginUser.createNewUser(oauth2Id); + return new CustomOAuth2User(newUser); + } + + LoginUser loginUser = LoginUser.createExistingUser(oauth2Id, existingUser.get().getUserId()); + return new CustomOAuth2User(loginUser); + } +} + diff --git a/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java new file mode 100644 index 000000000..778b9671c --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java @@ -0,0 +1,53 @@ +package konkuk.thip.common.security.oauth2; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import konkuk.thip.common.dto.BaseResponse; +import konkuk.thip.common.security.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +import static konkuk.thip.common.security.constant.AuthParameters.JWT_HEADER_KEY; +import static konkuk.thip.common.security.constant.AuthParameters.JWT_PREFIX; + +@Component +@RequiredArgsConstructor +public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + private static final String CONTENT_TYPE = "application/json"; + private static final String ENCODING = "UTF-8"; + private final JwtUtil jwtUtil; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); + LoginUser loginUser = oAuth2User.getLoginUser(); + + if(oAuth2User.isNewUser()) { + // 최초 로그인 : 회원가입을 위한 임시 토큰 발급 + String tempToken = jwtUtil.createSignupToken(loginUser.oauth2Id()); + response.setHeader(JWT_HEADER_KEY.getValue(), JWT_PREFIX.getValue() + tempToken); + writeResponse(response, BaseResponse.ok(oAuth2User.getLoginUser())); + return; + } + + // 기존 회원 : Access Token 발급 + String accessToken = jwtUtil.createAccessToken(loginUser.userId()); + response.setHeader(JWT_HEADER_KEY.getValue(), JWT_PREFIX.getValue() + accessToken); + writeResponse(response, BaseResponse.ok(oAuth2User.getLoginUser())); + } + + private void writeResponse(HttpServletResponse response, Object value) throws IOException { + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType(CONTENT_TYPE); + response.setCharacterEncoding(ENCODING); + ObjectMapper objectMapper = new ObjectMapper(); + String body = objectMapper.writeValueAsString(value); + response.getWriter().write(body); + } +} diff --git a/src/main/java/konkuk/thip/common/security/oauth2/GoogleUserDetails.java b/src/main/java/konkuk/thip/common/security/oauth2/GoogleUserDetails.java new file mode 100644 index 000000000..da43c50e4 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/GoogleUserDetails.java @@ -0,0 +1,34 @@ +package konkuk.thip.common.security.oauth2; + +import java.util.Map; + +import static konkuk.thip.common.security.constant.AuthParameters.GOOGLE; +import static konkuk.thip.common.security.constant.AuthParameters.GOOGLE_PROVIDER_ID_KEY; + +public class GoogleUserDetails implements OAuth2UserDetails{ + + private final Map attributes; + + public GoogleUserDetails(Map attributes) { + + this.attributes = attributes; + } + + @Override + public String getProvider() { + + return GOOGLE.getValue(); + } + + @Override + public String getProviderId() { + + return attributes.get(GOOGLE_PROVIDER_ID_KEY.getValue()).toString(); + } + +// @Override +// public String getEmail() { +// +// return attribute.get("email").toString(); +// } +} diff --git a/src/main/java/konkuk/thip/common/security/oauth2/KakaoUserDetails.java b/src/main/java/konkuk/thip/common/security/oauth2/KakaoUserDetails.java new file mode 100644 index 000000000..86a754664 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/KakaoUserDetails.java @@ -0,0 +1,32 @@ +package konkuk.thip.common.security.oauth2; + +import java.util.Map; + +import static konkuk.thip.common.security.constant.AuthParameters.KAKAO; +import static konkuk.thip.common.security.constant.AuthParameters.KAKAO_PROVIDER_ID_KEY; + +public class KakaoUserDetails implements OAuth2UserDetails { + + private final Map attributes; + + public KakaoUserDetails(Map attributes) { + this.attributes = attributes; + } + + @Override + public String getProvider() { + return KAKAO.getValue(); + } + + @Override + public String getProviderId() { + return attributes.get(KAKAO_PROVIDER_ID_KEY.getValue()).toString(); + } + +// @Override +// public String getEmail() { +// Object object = attributes.get("kakao_account"); +// LinkedHashMap accountMap = (LinkedHashMap) object; +// return accountMap.get("email").toString(); +// } +} diff --git a/src/main/java/konkuk/thip/common/security/oauth2/LoginUser.java b/src/main/java/konkuk/thip/common/security/oauth2/LoginUser.java new file mode 100644 index 000000000..3b5dd37b2 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/LoginUser.java @@ -0,0 +1,15 @@ +package konkuk.thip.common.security.oauth2; + +public record LoginUser( + String oauth2Id, + Long userId, + boolean isNewUser +) { + public static LoginUser createNewUser(String oauth2Id) { + return new LoginUser(oauth2Id, null, true); + } + + public static LoginUser createExistingUser(String oauth2Id, Long userId) { + return new LoginUser(oauth2Id, userId, false); + } +} diff --git a/src/main/java/konkuk/thip/common/security/oauth2/OAuth2UserDetails.java b/src/main/java/konkuk/thip/common/security/oauth2/OAuth2UserDetails.java new file mode 100644 index 000000000..8e4b07468 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/oauth2/OAuth2UserDetails.java @@ -0,0 +1,7 @@ +package konkuk.thip.common.security.oauth2; + +// 구글, 카카오 소셜 로그인 통합을 위한 인터페이스 +public interface OAuth2UserDetails { + String getProvider(); // (e.g., "kakao", "google", etc.) + String getProviderId(); +} diff --git a/src/main/java/konkuk/thip/common/security/util/JwtUtil.java b/src/main/java/konkuk/thip/common/security/util/JwtUtil.java new file mode 100644 index 000000000..947cd9b27 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/util/JwtUtil.java @@ -0,0 +1,89 @@ +package konkuk.thip.common.security.util; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import konkuk.thip.common.security.oauth2.LoginUser; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +import static konkuk.thip.common.security.constant.AuthParameters.JWT_ACCESS_TOKEN_KEY; +import static konkuk.thip.common.security.constant.AuthParameters.JWT_SIGNUP_TOKEN_KEY; + +@Slf4j +@Component +public class JwtUtil { + + private final SecretKey secretKey; + + //todo 확정 후 환경변수로 변경 + private final long tokenExpiredMs = 86400000; // 24시간 + private final long signupTokenExpiredMs = 300000; // 5분 + + public JwtUtil(@Value("${jwt.secret}") String secret) { + secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); + } + + public String createSignupToken(String oauth2Id) { + return Jwts.builder() + .claim(JWT_SIGNUP_TOKEN_KEY.getValue(), oauth2Id) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + signupTokenExpiredMs)) + .signWith(secretKey) + .compact(); + } + + public String createAccessToken(Long userId) { + return Jwts.builder() + .claim(JWT_ACCESS_TOKEN_KEY.getValue(), userId) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + tokenExpiredMs)) + .signWith(secretKey) + .compact(); + } + + public boolean validateToken(String token) { + try { + Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token); + return true; + } catch (MalformedJwtException e) { + log.info("Invalid JWT Token", e); + } catch (ExpiredJwtException e) { + log.info("Expired JWT Token", e); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT Token", e); + } catch (IllegalArgumentException e) { + log.info("JWT claims string is empty.", e); + } + return false; + } + + public Boolean isExpired(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); + } + + private String getOauth2Id(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(JWT_SIGNUP_TOKEN_KEY.getValue(), String.class); + } + + private Long getUserId(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(JWT_ACCESS_TOKEN_KEY.getValue(), Long.class); + } + + public LoginUser getLoginUser(String token) { + String oauth2Id = getOauth2Id(token); + Long userId = getUserId(token); + + if (userId == null) { + return LoginUser.createNewUser(oauth2Id); + } + return LoginUser.createExistingUser(oauth2Id, userId); + } +} diff --git a/src/main/java/konkuk/thip/config/SecurityConfig.java b/src/main/java/konkuk/thip/config/SecurityConfig.java new file mode 100644 index 000000000..c31804c1b --- /dev/null +++ b/src/main/java/konkuk/thip/config/SecurityConfig.java @@ -0,0 +1,105 @@ +package konkuk.thip.config; + +import konkuk.thip.common.security.filter.JwtAuthenticationEntryPoint; +import konkuk.thip.common.security.filter.JwtAuthenticationFilter; +import konkuk.thip.common.security.oauth2.CustomOAuth2UserService; +import konkuk.thip.common.security.oauth2.CustomSuccessHandler; +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.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Collections; +import java.util.List; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomOAuth2UserService customOAuth2UserService; + private final CustomSuccessHandler customSuccessHandler; + + private static final String[] WHITELIST = { + "/swagger-ui/**", "/api-docs/**", "/swagger-ui.html", + "/v3/api-docs/**","/oauth2/authorization/**", + "/login/oauth2/code/**", + +// //테스트를 위한 url + "/api/test/public", + "/api/test/auth-status", + "/api/test/protected", + "/auth/kakao/**", + "/kakao-login-test.html", + "/google-login-test.html", + "/index.html", + }; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .oauth2Login((oauth2) -> oauth2 + .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig + .userService(customOAuth2UserService) + ) + .successHandler(customSuccessHandler) // OAuth2 로그인 성공 시 처리 + ) + .authorizeHttpRequests(auth -> auth + .requestMatchers(WHITELIST).permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling(handler -> handler.authenticationEntryPoint(jwtAuthenticationEntryPoint)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + ; + + return http.build(); + } + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + + // CORS 설정 + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(Collections.singletonList("*")); // 배포 시 도메인 명시 + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + config.setAllowedHeaders(Collections.singletonList("*")); + config.setAllowCredentials(true); + config.setMaxAge(3600L); + + config.setExposedHeaders(Collections.singletonList("Authorization")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return source; + } + +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/config/WebMvcConfig.java b/src/main/java/konkuk/thip/config/WebMvcConfig.java new file mode 100644 index 000000000..12d47b9a4 --- /dev/null +++ b/src/main/java/konkuk/thip/config/WebMvcConfig.java @@ -0,0 +1,24 @@ +package konkuk.thip.config; + +import konkuk.thip.common.security.argument_resolver.Oauth2IdArgumentResolver; +import konkuk.thip.common.security.argument_resolver.UserIdArgumentResolver; +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 UserIdArgumentResolver userIdArgumentResolver; + private final Oauth2IdArgumentResolver oauth2IdArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(userIdArgumentResolver); + resolvers.add(oauth2IdArgumentResolver); + } +} diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java index 8fcfa2206..e95ab982f 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java @@ -1,36 +1,46 @@ package konkuk.thip.user.adapter.in.web; +import jakarta.servlet.http.HttpServletResponse; import konkuk.thip.common.dto.BaseResponse; -import konkuk.thip.user.adapter.in.web.request.PostUserSignupRequest; -import konkuk.thip.user.adapter.in.web.request.PostUserVerifyNicknameRequest; -import konkuk.thip.user.adapter.in.web.response.PostUserSignupResponse; -import konkuk.thip.user.adapter.in.web.response.PostUserVerifyNicknameResponse; +import konkuk.thip.common.security.annotation.Oauth2Id; +import konkuk.thip.common.security.util.JwtUtil; +import konkuk.thip.user.adapter.in.web.request.UserSignupRequest; +import konkuk.thip.user.adapter.in.web.request.UserVerifyNicknameRequest; +import konkuk.thip.user.adapter.in.web.response.UserSignupResponse; +import konkuk.thip.user.adapter.in.web.response.UserVerifyNicknameResponse; import konkuk.thip.user.application.port.in.UserSignupUseCase; -import konkuk.thip.user.application.port.in.VerifyNicknameUseCase; +import konkuk.thip.user.application.port.in.UserVerifyNicknameUseCase; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import static konkuk.thip.common.security.constant.AuthParameters.JWT_HEADER_KEY; +import static konkuk.thip.common.security.constant.AuthParameters.JWT_PREFIX; + @RestController @RequiredArgsConstructor public class UserCommandController { private final UserSignupUseCase userSignupUseCase; - private final VerifyNicknameUseCase verifyNicknameUseCase; + private final UserVerifyNicknameUseCase userVerifyNicknameUseCase; + private final JwtUtil jwtUtil; @PostMapping("/users/signup") - public BaseResponse signup(@Validated @RequestBody PostUserSignupRequest request) { - return BaseResponse.ok(PostUserSignupResponse.of( - userSignupUseCase.signup(request.toCommand())) - ); + public BaseResponse signup(@Validated @RequestBody UserSignupRequest request, + @Oauth2Id String oauth2Id, + HttpServletResponse response) { + Long userId = userSignupUseCase.signup(request.toCommand(oauth2Id)); + String accessToken = jwtUtil.createAccessToken(userId); + response.setHeader(JWT_HEADER_KEY.getValue(), JWT_PREFIX.getValue() + accessToken); + return BaseResponse.ok(UserSignupResponse.of(userId)); } @PostMapping("/users/nickname") - public BaseResponse verifyNickname(@Validated @RequestBody PostUserVerifyNicknameRequest request) { - return BaseResponse.ok(PostUserVerifyNicknameResponse.of( - verifyNicknameUseCase.isNicknameUnique(request.nickname())) + public BaseResponse verifyNickname(@Validated @RequestBody UserVerifyNicknameRequest request) { + return BaseResponse.ok(UserVerifyNicknameResponse.of( + userVerifyNicknameUseCase.isNicknameUnique(request.nickname())) ); } } diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java index 01ca9f6b0..3a4436721 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java @@ -1,8 +1,8 @@ package konkuk.thip.user.adapter.in.web; import konkuk.thip.common.dto.BaseResponse; -import konkuk.thip.user.adapter.in.web.response.GetUserShowAliasChoiceResponse; -import konkuk.thip.user.application.port.in.ShowAliasChoiceViewUseCase; +import konkuk.thip.user.adapter.in.web.response.UserViewAliasChoiceResponse; +import konkuk.thip.user.application.port.in.UserViewAliasChoiceUseCase; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -11,12 +11,12 @@ @RequiredArgsConstructor public class UserQueryController { - private final ShowAliasChoiceViewUseCase showAliasChoiceViewUseCase; + private final UserViewAliasChoiceUseCase userViewAliasChoiceUseCase; @GetMapping("/users/alias") - public BaseResponse showAliasChoiceView() { - return BaseResponse.ok(GetUserShowAliasChoiceResponse.of( - showAliasChoiceViewUseCase.getAllAliasesAndCategories() + public BaseResponse showAliasChoiceView() { + return BaseResponse.ok(UserViewAliasChoiceResponse.of( + userViewAliasChoiceUseCase.getAllAliasesAndCategories() )); } } diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserSignupRequest.java b/src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java similarity index 66% rename from src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserSignupRequest.java rename to src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java index 99e45578c..fe13a0804 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserSignupRequest.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java @@ -1,25 +1,22 @@ package konkuk.thip.user.adapter.in.web.request; import jakarta.validation.constraints.*; +import jakarta.validation.constraints.NotNull; import konkuk.thip.user.application.port.in.dto.UserSignupCommand; -public record PostUserSignupRequest( +public record UserSignupRequest( @NotNull(message = "aliasId는 필수입니다.") Long aliasId, @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") @Size(max = 10, message = "닉네임은 최대 10자 입니다.") - String nickname, - - @NotBlank(message = "이메일은 공백일 수 없습니다.") - @Email(message = "이메일 형식이 올바르지 않습니다.") - String email + String nickname ) { - public UserSignupCommand toCommand() { + public UserSignupCommand toCommand(String oAuth2Id) { return UserSignupCommand.builder() .aliasId(aliasId) .nickname(nickname) - .email(email) + .oauth2Id(oAuth2Id) .build(); } } diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserVerifyNicknameRequest.java b/src/main/java/konkuk/thip/user/adapter/in/web/request/UserVerifyNicknameRequest.java similarity index 89% rename from src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserVerifyNicknameRequest.java rename to src/main/java/konkuk/thip/user/adapter/in/web/request/UserVerifyNicknameRequest.java index 3a8734d9f..54050a79c 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserVerifyNicknameRequest.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/request/UserVerifyNicknameRequest.java @@ -3,7 +3,7 @@ import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; -public record PostUserVerifyNicknameRequest( +public record UserVerifyNicknameRequest( @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") @Size(max = 10, message = "닉네임은 최대 10자 입니다.") String nickname diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserSignupResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserSignupResponse.java deleted file mode 100644 index c3cbb993c..000000000 --- a/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserSignupResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package konkuk.thip.user.adapter.in.web.response; - -public record PostUserSignupResponse(Long userId) { - public static PostUserSignupResponse of(Long userId) { - return new PostUserSignupResponse(userId); - } -} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserVerifyNicknameResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserVerifyNicknameResponse.java deleted file mode 100644 index c254d052e..000000000 --- a/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserVerifyNicknameResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package konkuk.thip.user.adapter.in.web.response; - -public record PostUserVerifyNicknameResponse(boolean isVerified) { - public static PostUserVerifyNicknameResponse of(boolean isVerified) { - return new PostUserVerifyNicknameResponse(isVerified); - } -} diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/UserSignupResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserSignupResponse.java new file mode 100644 index 000000000..f5a490e6e --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserSignupResponse.java @@ -0,0 +1,7 @@ +package konkuk.thip.user.adapter.in.web.response; + +public record UserSignupResponse(Long userId) { + public static UserSignupResponse of(Long userId) { + return new UserSignupResponse(userId); + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/UserVerifyNicknameResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserVerifyNicknameResponse.java new file mode 100644 index 000000000..449dd9c20 --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserVerifyNicknameResponse.java @@ -0,0 +1,7 @@ +package konkuk.thip.user.adapter.in.web.response; + +public record UserVerifyNicknameResponse(boolean isVerified) { + public static UserVerifyNicknameResponse of(boolean isVerified) { + return new UserVerifyNicknameResponse(isVerified); + } +} diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/GetUserShowAliasChoiceResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserViewAliasChoiceResponse.java similarity index 67% rename from src/main/java/konkuk/thip/user/adapter/in/web/response/GetUserShowAliasChoiceResponse.java rename to src/main/java/konkuk/thip/user/adapter/in/web/response/UserViewAliasChoiceResponse.java index b9f3116af..8af48cfef 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/response/GetUserShowAliasChoiceResponse.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserViewAliasChoiceResponse.java @@ -1,12 +1,12 @@ package konkuk.thip.user.adapter.in.web.response; -import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; +import konkuk.thip.user.application.port.in.dto.UserViewAliasChoiceResult; import java.util.List; -public record GetUserShowAliasChoiceResponse(List aliasChoices) { +public record UserViewAliasChoiceResponse(List aliasChoices) { - public static GetUserShowAliasChoiceResponse of(AliasChoiceViewResult result) { + public static UserViewAliasChoiceResponse of(UserViewAliasChoiceResult result) { List choices = result.aliasChoices().stream() .map(ac -> new AliasChoice( ac.aliasId(), @@ -16,7 +16,7 @@ public static GetUserShowAliasChoiceResponse of(AliasChoiceViewResult result) { ac.color() )) .toList(); - return new GetUserShowAliasChoiceResponse(choices); + return new UserViewAliasChoiceResponse(choices); } public record AliasChoice( diff --git a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java index 6258411ae..e7556dd11 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java +++ b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java @@ -18,15 +18,15 @@ public class UserJpaEntity extends BaseJpaEntity { @Column(name = "user_id") private Long userId; - @Column(name = "email", length = 100, nullable = false) - private String email; - @Column(length = 60, nullable = false) private String nickname; @Column(name = "image_url", columnDefinition = "TEXT", nullable = false) private String imageUrl; + @Column(name = "oauth2_id", length = 50, nullable = false) + private String oauth2Id; + @Enumerated(EnumType.STRING) @Column(nullable = false) private UserRole role; diff --git a/src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java b/src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java index 38780e9ef..40ce8d0c3 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java +++ b/src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java @@ -11,10 +11,10 @@ public class UserMapper { public UserJpaEntity toJpaEntity(User user, AliasJpaEntity aliasJpaEntity) { return UserJpaEntity.builder() - .email(user.getEmail()) .nickname(user.getNickname()) .imageUrl(user.getImageUrl()) .role(UserRole.from(user.getUserRole())) + .oauth2Id(user.getOauth2Id()) .aliasForUserJpaEntity(aliasJpaEntity) .build(); } @@ -22,11 +22,11 @@ public UserJpaEntity toJpaEntity(User user, AliasJpaEntity aliasJpaEntity) { public User toDomainEntity(UserJpaEntity userJpaEntity) { return User.builder() .id(userJpaEntity.getUserId()) - .email(userJpaEntity.getEmail()) .nickname(userJpaEntity.getNickname()) .imageUrl(userJpaEntity.getImageUrl()) .userRole(userJpaEntity.getRole().getType()) .aliasId(userJpaEntity.getAliasForUserJpaEntity().getAliasId()) + .oauth2Id(userJpaEntity.getOauth2Id()) .createdAt(userJpaEntity.getCreatedAt()) .modifiedAt(userJpaEntity.getModifiedAt()) .status(userJpaEntity.getStatus()) diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java index 5f4e01dd4..0fe88291d 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java @@ -1,6 +1,6 @@ package konkuk.thip.user.adapter.out.persistence; -import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; +import konkuk.thip.user.application.port.in.dto.UserViewAliasChoiceResult; import konkuk.thip.user.application.port.out.AliasQueryPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -12,7 +12,7 @@ public class AliasQueryPersistenceAdapter implements AliasQueryPort { private final AliasJpaRepository aliasJpaRepository; @Override - public AliasChoiceViewResult getAllAliasesAndCategories() { + public UserViewAliasChoiceResult getAllAliasesAndCategories() { return aliasJpaRepository.getAllAliasesAndCategories(); } } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java index 25ce90252..43cf50e37 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java @@ -1,8 +1,8 @@ package konkuk.thip.user.adapter.out.persistence; -import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; +import konkuk.thip.user.application.port.in.dto.UserViewAliasChoiceResult; public interface AliasQueryRepository { - AliasChoiceViewResult getAllAliasesAndCategories(); + UserViewAliasChoiceResult getAllAliasesAndCategories(); } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java index 9cabe24cf..a3791cd11 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java @@ -4,7 +4,7 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import konkuk.thip.room.adapter.out.jpa.QCategoryJpaEntity; import konkuk.thip.user.adapter.out.jpa.QAliasJpaEntity; -import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; +import konkuk.thip.user.application.port.in.dto.UserViewAliasChoiceResult; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -17,13 +17,13 @@ public class AliasQueryRepositoryImpl implements AliasQueryRepository { private final JPAQueryFactory jpaQueryFactory; @Override - public AliasChoiceViewResult getAllAliasesAndCategories() { + public UserViewAliasChoiceResult getAllAliasesAndCategories() { QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; QCategoryJpaEntity category = QCategoryJpaEntity.categoryJpaEntity; - List aliasChoices = jpaQueryFactory + List aliasChoices = jpaQueryFactory .select(Projections.constructor( - AliasChoiceViewResult.AliasChoice.class, + UserViewAliasChoiceResult.AliasChoice.class, alias.aliasId, alias.value, category.value, @@ -36,6 +36,6 @@ public AliasChoiceViewResult getAllAliasesAndCategories() { .orderBy(alias.aliasId.asc()) .fetch(); - return new AliasChoiceViewResult(aliasChoices); + return new UserViewAliasChoiceResult(aliasChoices); } } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java index 39798fe6a..691be41af 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java @@ -3,7 +3,9 @@ import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; -public interface UserJpaRepository extends JpaRepository, UserQueryRepository { +import java.util.Optional; +public interface UserJpaRepository extends JpaRepository, UserQueryRepository { + Optional findByOauth2Id(String oauth2Id); boolean existsByNickname(String nickname); } diff --git a/src/main/java/konkuk/thip/user/application/port/in/ShowAliasChoiceViewUseCase.java b/src/main/java/konkuk/thip/user/application/port/in/ShowAliasChoiceViewUseCase.java deleted file mode 100644 index 994a6cf31..000000000 --- a/src/main/java/konkuk/thip/user/application/port/in/ShowAliasChoiceViewUseCase.java +++ /dev/null @@ -1,8 +0,0 @@ -package konkuk.thip.user.application.port.in; - -import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; - -public interface ShowAliasChoiceViewUseCase { - - AliasChoiceViewResult getAllAliasesAndCategories(); -} diff --git a/src/main/java/konkuk/thip/user/application/port/in/VerifyNicknameUseCase.java b/src/main/java/konkuk/thip/user/application/port/in/UserVerifyNicknameUseCase.java similarity index 68% rename from src/main/java/konkuk/thip/user/application/port/in/VerifyNicknameUseCase.java rename to src/main/java/konkuk/thip/user/application/port/in/UserVerifyNicknameUseCase.java index 3cca5a9f3..d3af2661d 100644 --- a/src/main/java/konkuk/thip/user/application/port/in/VerifyNicknameUseCase.java +++ b/src/main/java/konkuk/thip/user/application/port/in/UserVerifyNicknameUseCase.java @@ -1,6 +1,6 @@ package konkuk.thip.user.application.port.in; -public interface VerifyNicknameUseCase { +public interface UserVerifyNicknameUseCase { boolean isNicknameUnique(String nickname); } diff --git a/src/main/java/konkuk/thip/user/application/port/in/UserViewAliasChoiceUseCase.java b/src/main/java/konkuk/thip/user/application/port/in/UserViewAliasChoiceUseCase.java new file mode 100644 index 000000000..2f925145f --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/in/UserViewAliasChoiceUseCase.java @@ -0,0 +1,8 @@ +package konkuk.thip.user.application.port.in; + +import konkuk.thip.user.application.port.in.dto.UserViewAliasChoiceResult; + +public interface UserViewAliasChoiceUseCase { + + UserViewAliasChoiceResult getAllAliasesAndCategories(); +} diff --git a/src/main/java/konkuk/thip/user/application/port/in/dto/DummyCommand.java b/src/main/java/konkuk/thip/user/application/port/in/dto/DummyCommand.java deleted file mode 100644 index c15059ace..000000000 --- a/src/main/java/konkuk/thip/user/application/port/in/dto/DummyCommand.java +++ /dev/null @@ -1,10 +0,0 @@ -package konkuk.thip.user.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Builder -@Getter -public class DummyCommand { - -} diff --git a/src/main/java/konkuk/thip/user/application/port/in/dto/DummyQuery.java b/src/main/java/konkuk/thip/user/application/port/in/dto/DummyQuery.java deleted file mode 100644 index 39c0fb70a..000000000 --- a/src/main/java/konkuk/thip/user/application/port/in/dto/DummyQuery.java +++ /dev/null @@ -1,9 +0,0 @@ -package konkuk.thip.user.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Builder -@Getter -public class DummyQuery { -} diff --git a/src/main/java/konkuk/thip/user/application/port/in/dto/UserSignupCommand.java b/src/main/java/konkuk/thip/user/application/port/in/dto/UserSignupCommand.java index 38aef5a36..0b23769e8 100644 --- a/src/main/java/konkuk/thip/user/application/port/in/dto/UserSignupCommand.java +++ b/src/main/java/konkuk/thip/user/application/port/in/dto/UserSignupCommand.java @@ -6,5 +6,5 @@ public record UserSignupCommand( Long aliasId, String nickname, - String email + String oauth2Id ) {} diff --git a/src/main/java/konkuk/thip/user/application/port/in/dto/AliasChoiceViewResult.java b/src/main/java/konkuk/thip/user/application/port/in/dto/UserViewAliasChoiceResult.java similarity index 77% rename from src/main/java/konkuk/thip/user/application/port/in/dto/AliasChoiceViewResult.java rename to src/main/java/konkuk/thip/user/application/port/in/dto/UserViewAliasChoiceResult.java index 3df6ca89e..b60820376 100644 --- a/src/main/java/konkuk/thip/user/application/port/in/dto/AliasChoiceViewResult.java +++ b/src/main/java/konkuk/thip/user/application/port/in/dto/UserViewAliasChoiceResult.java @@ -2,7 +2,7 @@ import java.util.List; -public record AliasChoiceViewResult(List aliasChoices) { +public record UserViewAliasChoiceResult(List aliasChoices) { public record AliasChoice( Long aliasId, diff --git a/src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java b/src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java index c832e863a..064428123 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java +++ b/src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java @@ -1,8 +1,8 @@ package konkuk.thip.user.application.port.out; -import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; +import konkuk.thip.user.application.port.in.dto.UserViewAliasChoiceResult; public interface AliasQueryPort { - AliasChoiceViewResult getAllAliasesAndCategories(); + UserViewAliasChoiceResult getAllAliasesAndCategories(); } diff --git a/src/main/java/konkuk/thip/user/application/service/UserSignupService.java b/src/main/java/konkuk/thip/user/application/service/UserSignupService.java index b32c2028e..478ed31e4 100644 --- a/src/main/java/konkuk/thip/user/application/service/UserSignupService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserSignupService.java @@ -25,7 +25,7 @@ public class UserSignupService implements UserSignupUseCase { public Long signup(UserSignupCommand command) { Alias alias = aliasCommandPort.findById(command.aliasId()); User user = User.withoutId( - command.email(), command.nickname(), alias.getImageUrl(), USER.getType(), alias.getId() + command.nickname(), alias.getImageUrl(), USER.getType(), alias.getId(), command.oauth2Id() ); return userCommandPort.save(user); diff --git a/src/main/java/konkuk/thip/user/application/service/VerifyNicknameService.java b/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java similarity index 73% rename from src/main/java/konkuk/thip/user/application/service/VerifyNicknameService.java rename to src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java index f8cae39a1..8650171c2 100644 --- a/src/main/java/konkuk/thip/user/application/service/VerifyNicknameService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java @@ -1,13 +1,13 @@ package konkuk.thip.user.application.service; -import konkuk.thip.user.application.port.in.VerifyNicknameUseCase; +import konkuk.thip.user.application.port.in.UserVerifyNicknameUseCase; import konkuk.thip.user.application.port.out.UserQueryPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -public class VerifyNicknameService implements VerifyNicknameUseCase { +public class UserVerifyNicknameService implements UserVerifyNicknameUseCase { private final UserQueryPort userQueryPort; diff --git a/src/main/java/konkuk/thip/user/application/service/ShowAliasChoiceViewService.java b/src/main/java/konkuk/thip/user/application/service/UserViewAliasChoiceService.java similarity index 56% rename from src/main/java/konkuk/thip/user/application/service/ShowAliasChoiceViewService.java rename to src/main/java/konkuk/thip/user/application/service/UserViewAliasChoiceService.java index bc96d2749..b04dd2df6 100644 --- a/src/main/java/konkuk/thip/user/application/service/ShowAliasChoiceViewService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserViewAliasChoiceService.java @@ -1,19 +1,19 @@ package konkuk.thip.user.application.service; -import konkuk.thip.user.application.port.in.ShowAliasChoiceViewUseCase; -import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; +import konkuk.thip.user.application.port.in.UserViewAliasChoiceUseCase; +import konkuk.thip.user.application.port.in.dto.UserViewAliasChoiceResult; import konkuk.thip.user.application.port.out.AliasQueryPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -public class ShowAliasChoiceViewService implements ShowAliasChoiceViewUseCase { +public class UserViewAliasChoiceService implements UserViewAliasChoiceUseCase { private final AliasQueryPort aliasQueryPort; @Override - public AliasChoiceViewResult getAllAliasesAndCategories() { + public UserViewAliasChoiceResult getAllAliasesAndCategories() { return aliasQueryPort.getAllAliasesAndCategories(); } } diff --git a/src/main/java/konkuk/thip/user/domain/User.java b/src/main/java/konkuk/thip/user/domain/User.java index b7d1e1ebf..b19513dd7 100644 --- a/src/main/java/konkuk/thip/user/domain/User.java +++ b/src/main/java/konkuk/thip/user/domain/User.java @@ -10,8 +10,6 @@ public class User extends BaseDomainEntity { private Long id; - private String email; - private String nickname; private String imageUrl; @@ -20,14 +18,16 @@ public class User extends BaseDomainEntity { private Long aliasId; - public static User withoutId(String email, String nickname, String imageUrl, String userRole, Long aliasId) { + private String oauth2Id; + + public static User withoutId(String nickname, String imageUrl, String userRole, Long aliasId, String oauth2Id) { return User.builder() .id(null) - .email(email) .nickname(nickname) .imageUrl(imageUrl) .userRole(userRole) .aliasId(aliasId) + .oauth2Id(oauth2Id) .build(); } diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java index 20a1e4938..b85119545 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java @@ -15,7 +15,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest -@AutoConfigureMockMvc +@AutoConfigureMockMvc(addFilters = false) @ActiveProfiles("test") class BookQueryControllerTest { diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java new file mode 100644 index 000000000..69b9895c4 --- /dev/null +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -0,0 +1,75 @@ +package konkuk.thip.common.util; + +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.record.adapter.out.jpa.RecordJpaEntity; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserRole; + +import java.time.LocalDate; + +public class TestEntityFactory { + + public static AliasJpaEntity createAlias() { + return AliasJpaEntity.builder() + .value("칭호") + .imageUrl("test-image-url") + .color("red") + .build(); + } + + public static UserJpaEntity createUser(AliasJpaEntity alias) { + return UserJpaEntity.builder() + .nickname("테스터") + .imageUrl("https://test.img") + .oauth2Id("kakao_12345678") + .aliasForUserJpaEntity(alias) + .role(UserRole.USER) + .build(); + } + + public static BookJpaEntity createBook() { + return BookJpaEntity.builder() + .title("책제목") + .authorName("저자") + .isbn("isbn") + .bestSeller(false) + .publisher("출판사") + .imageUrl("img") + .pageCount(100) + .description("설명") + .build(); + } + + public static CategoryJpaEntity createCategory(AliasJpaEntity alias) { + return CategoryJpaEntity.builder() + .value("카테고리1") + .aliasForCategoryJpaEntity(alias) + .build(); + } + + public static RoomJpaEntity createRoom(BookJpaEntity book, CategoryJpaEntity category) { + return RoomJpaEntity.builder() + .title("방이름") + .description("설명") + .isPublic(true) + .startDate(LocalDate.now()) + .endDate(LocalDate.now().plusDays(5)) + .recruitCount(3) + .bookJpaEntity(book) + .categoryJpaEntity(category) + .build(); + } + + public static RecordJpaEntity createRecord(UserJpaEntity user, RoomJpaEntity room) { + return RecordJpaEntity.builder() + .content("기록 내용") + .userJpaEntity(user) + .page(22) + .isOverview(false) + .roomJpaEntity(room) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java b/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java index 3eef4a2bf..d956cf296 100644 --- a/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java @@ -1,13 +1,12 @@ package konkuk.thip.feed.adapter.out.jpa; import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.feed.adapter.out.persistence.FeedJpaRepository; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; import org.junit.jupiter.api.DisplayName; @@ -24,7 +23,7 @@ @Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import class FeedJpaEntityTest { - @PersistenceContext + @Autowired private EntityManager em; @Autowired @@ -39,58 +38,31 @@ class FeedJpaEntityTest { @Autowired private FeedJpaRepository feedRepository; - private UserJpaEntity createUser() { - AliasJpaEntity alias = AliasJpaEntity.builder() - .value("칭호") - .imageUrl("test-image-url") - .color("red") - .build(); - - aliasRepository.save(alias); - - UserJpaEntity user = UserJpaEntity.builder() - .email("test@test.com") - .nickname("테스터") - .imageUrl("https://test.img") - .aliasForUserJpaEntity(alias) - .role(UserRole.USER) - .build(); - return userRepository.save(user); - } - - private BookJpaEntity createBook() { - return bookRepository.save(BookJpaEntity.builder() - .title("책제목") - .authorName("저자") - .isbn("isbn") - .bestSeller(false) - .publisher("출판사") - .imageUrl("img") - .pageCount(100) - .description("설명") - .build()); - } - @Test @DisplayName("FeedJpaEntity 저장 및 조회 테스트") void saveAndFindFeed() { - UserJpaEntity user = createUser(); - BookJpaEntity book = createBook(); + // given + AliasJpaEntity alias = aliasRepository.save(TestEntityFactory.createAlias()); + UserJpaEntity user = userRepository.save(TestEntityFactory.createUser(alias)); + BookJpaEntity book = bookRepository.save(TestEntityFactory.createBook()); - FeedJpaEntity feed = FeedJpaEntity.builder() + FeedJpaEntity feed = feedRepository.save(FeedJpaEntity.builder() .content("피드 내용") .userJpaEntity(user) .isPublic(true) .bookJpaEntity(book) - .build(); + .build()); - feedRepository.save(feed); em.flush(); em.clear(); + // when FeedJpaEntity found = feedRepository.findById(feed.getPostId()).orElseThrow(); + + // then assertThat(found).isNotNull(); assertThat(found.getContent()).isEqualTo("피드 내용"); + assertThat(found.getUserJpaEntity().getNickname()).isEqualTo("테스터"); assertThat(found.getBookJpaEntity().getTitle()).isEqualTo("책제목"); } } \ No newline at end of file diff --git a/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java b/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java index cdfee771e..4a807fe95 100644 --- a/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java +++ b/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java @@ -3,13 +3,13 @@ import jakarta.persistence.EntityManager; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.record.adapter.out.jpa.RecordJpaEntity; import konkuk.thip.record.adapter.out.persistence.RecordJpaRepository; import konkuk.thip.room.adapter.out.persistence.CategoryJpaRepository; import konkuk.thip.room.adapter.out.persistence.RoomJpaRepository; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; import org.junit.jupiter.api.DisplayName; @@ -19,8 +19,6 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; -import java.time.LocalDate; - import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @@ -49,87 +47,27 @@ class RecordJpaEntityTest { @Autowired private CategoryJpaRepository categoryRepository; - private UserJpaEntity createUser() { - AliasJpaEntity alias = AliasJpaEntity.builder() - .value("칭호") - .imageUrl("test-image-url") - .color("red") - .build(); - - aliasRepository.save(alias); - - UserJpaEntity user = UserJpaEntity.builder() - .email("test@test.com") - .nickname("테스터") - .imageUrl("https://test.img") - .aliasForUserJpaEntity(alias) - .role(UserRole.USER) - .build(); - return userRepository.save(user); - } - - private BookJpaEntity createBook() { - return bookRepository.save(BookJpaEntity.builder() - .title("책제목") - .authorName("저자") - .isbn("isbn") - .bestSeller(false) - .publisher("출판사") - .imageUrl("img") - .pageCount(100) - .description("설명") - .build()); - } - - private RoomJpaEntity createRoom(BookJpaEntity book, CategoryJpaEntity category) { - return roomRepository.save(RoomJpaEntity.builder() - .title("방이름") - .description("설명") - .isPublic(true) - .startDate(LocalDate.now()) - .endDate(LocalDate.now().plusDays(5)) - .recruitCount(3) - .bookJpaEntity(book) - .categoryJpaEntity(category) - .build()); - } - - private CategoryJpaEntity createCategory() { - AliasJpaEntity alias = AliasJpaEntity.builder() - .value("칭호") - .imageUrl("test-image-url") - .color("red") - .build(); - - aliasRepository.save(alias); - - return categoryRepository.save(CategoryJpaEntity.builder() - .value("카테고리1") - .aliasForCategoryJpaEntity(alias) - .build()); - } - @Test @DisplayName("RecordJpaEntity 저장 및 조회 테스트") void saveAndFindRecord() { - UserJpaEntity user = createUser(); - RoomJpaEntity room = createRoom(createBook(), createCategory()); - - RecordJpaEntity record = RecordJpaEntity.builder() - .content("기록 내용") - .userJpaEntity(user) - .page(22) - .isOverview(false) - .roomJpaEntity(room) - .build(); + // given + AliasJpaEntity alias = aliasRepository.save(TestEntityFactory.createAlias()); + UserJpaEntity user = userRepository.save(TestEntityFactory.createUser(alias)); + BookJpaEntity book = bookRepository.save(TestEntityFactory.createBook()); + CategoryJpaEntity category = categoryRepository.save(TestEntityFactory.createCategory(alias)); + RoomJpaEntity room = roomRepository.save(TestEntityFactory.createRoom(book, category)); + RecordJpaEntity record = recordRepository.save(TestEntityFactory.createRecord(user, room)); - recordRepository.save(record); em.flush(); em.clear(); + // when RecordJpaEntity found = recordRepository.findById(record.getPostId()).orElseThrow(); + + // then assertThat(found).isNotNull(); assertThat(found.getPage()).isEqualTo(22); assertThat(found.getRoomJpaEntity().getTitle()).isEqualTo("방이름"); + assertThat(found.getUserJpaEntity().getNickname()).isEqualTo("테스터"); } } \ No newline at end of file diff --git a/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java b/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java index ecb06c6a3..97610553e 100644 --- a/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java +++ b/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java @@ -3,10 +3,11 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; -import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.persistence.CategoryJpaRepository; import konkuk.thip.room.adapter.out.persistence.RoomJpaRepository; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -15,12 +16,13 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; -import java.time.LocalDate; +import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @ActiveProfiles("test") @Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import class RoomJpaEntityTest { + @PersistenceContext private EntityManager em; @@ -39,65 +41,25 @@ class RoomJpaEntityTest { @Test @DisplayName("RoomJpaEntity 저장 및 조회 테스트") void saveAndFindRoom() { - //given - BookJpaEntity book = BookJpaEntity.builder() - .title("테스트 책") - .authorName("테스트 저자") - .isbn("1234567890") - .bestSeller(false) - .publisher("테스트 출판사") - .imageUrl("http://test.image.url") - .pageCount(300) - .description("테스트 책 설명") - .build(); - - bookRepository.save(book); - - AliasJpaEntity alias = AliasJpaEntity.builder() - .value("칭호") - .imageUrl("test-image-url") - .color("red") - .build(); - - aliasRepository.save(alias); + // given + BookJpaEntity book = bookRepository.save(TestEntityFactory.createBook()); + AliasJpaEntity alias = aliasRepository.save(TestEntityFactory.createAlias()); + CategoryJpaEntity category = categoryRepository.save(TestEntityFactory.createCategory(alias)); + RoomJpaEntity room = roomRepository.save(TestEntityFactory.createRoom(book, category)); - CategoryJpaEntity category = CategoryJpaEntity.builder() - .value("카테고리1") - .aliasForCategoryJpaEntity(alias) - .build(); - - categoryRepository.save(category); - - RoomJpaEntity room = RoomJpaEntity.builder() - .title("테스트 방") - .description("테스트 방 설명") - .isPublic(true) - .startDate(LocalDate.of(2025, 6, 20)) - .endDate(LocalDate.of(2025, 6, 30)) - .recruitCount(5) - .bookJpaEntity(book) - .categoryJpaEntity(category) - .build(); - - //when - roomRepository.save(room); + // when em.flush(); em.clear(); RoomJpaEntity foundRoom = roomRepository.findById(room.getRoomId()).orElseThrow(); - //then - assert foundRoom.getTitle().equals("테스트 방"); - assert foundRoom.getDescription().equals("테스트 방 설명"); - assert foundRoom.isPublic(); - assert foundRoom.getStartDate().equals(LocalDate.of(2025, 6, 20)); - assert foundRoom.getEndDate().equals(LocalDate.of(2025, 6, 30)); - assert foundRoom.getRecruitCount() == 5; - assert foundRoom.getBookJpaEntity().getTitle().equals("테스트 책"); - assert foundRoom.getBookJpaEntity().getAuthorName().equals("테스트 저자"); - assert foundRoom.getBookJpaEntity().getIsbn().equals("1234567890"); + // then + assertThat(foundRoom.getTitle()).isEqualTo("방이름"); + assertThat(foundRoom.getDescription()).isEqualTo("설명"); + assertThat(foundRoom.isPublic()).isTrue(); + assertThat(foundRoom.getRecruitCount()).isEqualTo(3); + assertThat(foundRoom.getBookJpaEntity().getTitle()).isEqualTo("책제목"); + assertThat(foundRoom.getBookJpaEntity().getAuthorName()).isEqualTo("저자"); + assertThat(foundRoom.getBookJpaEntity().getIsbn()).isEqualTo("isbn"); } - - - } \ No newline at end of file diff --git a/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java b/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java index e28fee521..692dfe4ef 100644 --- a/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java +++ b/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java @@ -1,14 +1,13 @@ package konkuk.thip.room.adapter.out.jpa; import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.room.adapter.out.persistence.CategoryJpaRepository; import konkuk.thip.room.adapter.out.persistence.RoomJpaRepository; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserRole; import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity; @@ -20,8 +19,6 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; -import java.time.LocalDate; - import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @@ -29,7 +26,7 @@ @Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import class VoteJpaEntityTest { - @PersistenceContext + @Autowired private EntityManager em; @Autowired @@ -50,86 +47,35 @@ class VoteJpaEntityTest { @Autowired private CategoryJpaRepository categoryRepository; - private UserJpaEntity createUser() { - AliasJpaEntity alias = AliasJpaEntity.builder() - .value("칭호") - .imageUrl("test-image-url") - .color("red") - .build(); - aliasRepository.save(alias); - - UserJpaEntity user = UserJpaEntity.builder() - .email("test@test.com") - .nickname("테스터") - .imageUrl("https://test.img") - .aliasForUserJpaEntity(alias) - .role(UserRole.USER) - .build(); - return userRepository.save(user); - } - - private BookJpaEntity createBook() { - return bookRepository.save(BookJpaEntity.builder() - .title("책제목") - .authorName("저자") - .isbn("isbn") - .bestSeller(false) - .publisher("출판사") - .imageUrl("img") - .pageCount(100) - .description("설명") - .build()); - } - - private RoomJpaEntity createRoom(BookJpaEntity book, CategoryJpaEntity category) { - return roomRepository.save(RoomJpaEntity.builder() - .title("방이름") - .description("설명") - .isPublic(true) - .startDate(LocalDate.now()) - .endDate(LocalDate.now().plusDays(5)) - .recruitCount(3) - .bookJpaEntity(book) - .categoryJpaEntity(category) - .build()); - } - - private CategoryJpaEntity createCategory() { - AliasJpaEntity alias = AliasJpaEntity.builder() - .value("칭호") - .imageUrl("test-image-url") - .color("red") - .build(); - - aliasRepository.save(alias); - - return categoryRepository.save(CategoryJpaEntity.builder() - .value("카테고리1") - .aliasForCategoryJpaEntity(alias) - .build()); - } - @Test @DisplayName("VoteJpaEntity 저장 및 조회 테스트") void saveAndFindVote() { - UserJpaEntity user = createUser(); - RoomJpaEntity room = createRoom(createBook(), createCategory()); - - VoteJpaEntity vote = VoteJpaEntity.builder() + // given + AliasJpaEntity alias = aliasRepository.save(TestEntityFactory.createAlias()); + UserJpaEntity user = userRepository.save(TestEntityFactory.createUser(alias)); + BookJpaEntity book = bookRepository.save(TestEntityFactory.createBook()); + CategoryJpaEntity category = categoryRepository.save(TestEntityFactory.createCategory(alias)); + RoomJpaEntity room = roomRepository.save(TestEntityFactory.createRoom(book, category)); + + VoteJpaEntity vote = voteRepository.save(VoteJpaEntity.builder() .content("투표 내용") .userJpaEntity(user) .page(10) .isOverview(true) .roomJpaEntity(room) - .build(); + .build()); - voteRepository.save(vote); em.flush(); em.clear(); + // when VoteJpaEntity found = voteRepository.findById(vote.getPostId()).orElseThrow(); + + // then assertThat(found).isNotNull(); assertThat(found.getPage()).isEqualTo(10); + assertThat(found.getContent()).isEqualTo("투표 내용"); + assertThat(found.getUserJpaEntity().getNickname()).isEqualTo("테스터"); assertThat(found.getRoomJpaEntity().getTitle()).isEqualTo("방이름"); } } \ No newline at end of file diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java index cc7d92e83..9947800a1 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java @@ -2,7 +2,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import konkuk.thip.user.adapter.in.web.request.PostUserSignupRequest; +import konkuk.thip.common.security.util.JwtUtil; +import konkuk.thip.user.adapter.in.web.request.UserSignupRequest; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; @@ -19,6 +20,7 @@ import org.springframework.test.web.servlet.ResultActions; import static konkuk.thip.common.exception.code.ErrorCode.API_INVALID_PARAM; +import static konkuk.thip.common.exception.code.ErrorCode.AUTH_TOKEN_NOT_FOUND; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -42,6 +44,9 @@ class UserSignupControllerTest { @Autowired private UserJpaRepository userJpaRepository; + @Autowired + private JwtUtil jwtUtil; + @AfterEach void tearDown() { userJpaRepository.deleteAll(); @@ -49,7 +54,7 @@ void tearDown() { } @Test - @DisplayName("[칭호id, 닉네임, 이메일] 정보를 바탕으로 회원가입을 진행한다.") + @DisplayName("[칭호id, 닉네임] 정보를 바탕으로 회원가입을 진행한다.") void signup_success() throws Exception { //given : alias 생성, 회원가입 request 생성 AliasJpaEntity aliasJpaEntity = AliasJpaEntity.builder() @@ -59,14 +64,15 @@ void signup_success() throws Exception { .build(); aliasJpaRepository.save(aliasJpaEntity); - PostUserSignupRequest request = new PostUserSignupRequest( + UserSignupRequest request = new UserSignupRequest( aliasJpaEntity.getAliasId(), - "테스트유저", - "test@test.com" + "테스트유저" ); - //when : 회원가입 api 호출 + //when : 회원가입 api 호출 + 임시 토큰 발급 + String testToken = jwtUtil.createSignupToken("kakao_12345678"); ResultActions result = mockMvc.perform(post("/users/signup") + .header("Authorization", "Bearer " + testToken) //헤더 추가 .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -82,23 +88,23 @@ void signup_success() throws Exception { assertThat(userJpaEntity.getAliasForUserJpaEntity().getAliasId()).isEqualTo(request.aliasId()); assertThat(userJpaEntity.getNickname()).isEqualTo(request.nickname()); - assertThat(userJpaEntity.getEmail()).isEqualTo(request.email()); } @Test @DisplayName("[칭호id]값이 null일 경우, 400 error가 발생한다.") void signup_alias_id_null() throws Exception { //given: aliasId null - PostUserSignupRequest request = new PostUserSignupRequest( + UserSignupRequest request = new UserSignupRequest( null, - "테스트유저", - "test@test.com" + "테스트유저" ); //when //then + String testToken = jwtUtil.createSignupToken("kakao_12345678"); mockMvc.perform(post("/users/signup") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) + .header("Authorization", "Bearer " + testToken) //헤더 추가 + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) .andExpect(jsonPath("$.message", containsString("aliasId는 필수입니다."))); @@ -108,16 +114,17 @@ void signup_alias_id_null() throws Exception { @DisplayName("[닉네임]값이 공백일 경우, 400 error가 발생한다.") void signup_nickname_blank() throws Exception { //given: nickname blank - PostUserSignupRequest request = new PostUserSignupRequest( + UserSignupRequest request = new UserSignupRequest( 1L, - "", - "test@test.com" + "" ); //when //then + String testToken = jwtUtil.createSignupToken("kakao_12345678"); mockMvc.perform(post("/users/signup") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) + .header("Authorization", "Bearer " + testToken) //헤더 추가 + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) .andExpect(jsonPath("$.message", containsString("닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)"))); @@ -127,14 +134,15 @@ void signup_nickname_blank() throws Exception { @DisplayName("[닉네임]값이 한글, 영어, 숫자 외의 문자를 포함할 경우, 400 error가 발생한다.") void signup_nickname_invalid_pattern() throws Exception { //given: nickname with invalid characters - PostUserSignupRequest request = new PostUserSignupRequest( + UserSignupRequest request = new UserSignupRequest( 1L, - "닉네임!!", - "test@test.com" + "닉네임!!" ); //when //then + String testToken = jwtUtil.createSignupToken("kakao_12345678"); mockMvc.perform(post("/users/signup") + .header("Authorization", "Bearer " + testToken) //헤더 추가 .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) @@ -146,56 +154,81 @@ void signup_nickname_invalid_pattern() throws Exception { @DisplayName("[닉네임]값이 11자 이상일 경우, 400 error가 발생한다.") void signup_nickname_too_long() throws Exception { //given: 11글자 nickname - PostUserSignupRequest request = new PostUserSignupRequest( + UserSignupRequest request = new UserSignupRequest( 1L, - "11글자닉네임입니다아", - "test@test.com" + "11글자닉네임입니다아" ); //when //then + String testToken = jwtUtil.createSignupToken("kakao_12345678"); mockMvc.perform(post("/users/signup") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) + .header("Authorization", "Bearer " + testToken) //헤더 추가 + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) .andExpect(jsonPath("$.message", containsString("닉네임은 최대 10자 입니다."))); } @Test - @DisplayName("[이메일]값이 공백일 경우, 400 error가 발생한다.") - void signup_email_blank() throws Exception { - //given - PostUserSignupRequest request = new PostUserSignupRequest( - 1L, - "테스트유저", - "" + @DisplayName("임시 토큰을 통해 @Oauth2Id로 oauth2Id를 정확히 추출하여 회원가입에 성공한다.") + void signup_whenValidSignupToken_thenExtractOauth2IdCorrectly() throws Exception { + //given : alias 데이터 저장 + AliasJpaEntity aliasJpaEntity = AliasJpaEntity.builder() + .value("테스트칭호") + .color("green") + .imageUrl("http://image.url") + .build(); + aliasJpaRepository.save(aliasJpaEntity); + + //회원가입 request 생성 + UserSignupRequest request = new UserSignupRequest( + aliasJpaEntity.getAliasId(), + "테스트유저" ); - //when //then - mockMvc.perform(post("/users/signup") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) - .andExpect(jsonPath("$.message", containsString("이메일은 공백일 수 없습니다."))); + //when : 임시 토큰 생성 + String expectedOauth2Id = "kakao_12345678"; + String testToken = jwtUtil.createSignupToken(expectedOauth2Id); + + //when : 회원가입 API 호출 + ResultActions result = mockMvc.perform(post("/users/signup") + .header("Authorization", "Bearer " + testToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + //then : 응답 검증 + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.userId").exists()); + + //등록된 사용자 oauth2Id 검증 + String responseBody = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(responseBody); + Long savedUserId = jsonNode.path("data").path("userId").asLong(); + + UserJpaEntity savedUser = userJpaRepository.findById(savedUserId).orElseThrow(); + + assertThat(savedUser.getOauth2Id()).isEqualTo(expectedOauth2Id); + assertThat(savedUser.getNickname()).isEqualTo("테스트유저"); } @Test - @DisplayName("[이메일]값이 유효한 이메일 형식이 아닐 경우, 400 error가 발생한다.") - void signup_email_invalid_format() throws Exception { - //given - PostUserSignupRequest request = new PostUserSignupRequest( + @DisplayName("헤더에 토큰을 넣지 않고 요청시에 401 error가 발생한다.") + void signup_whenNoToken_thenUnauthorized() throws Exception { + //given: aliasId null + UserSignupRequest request = new UserSignupRequest( 1L, - "테스트유저", - "invalid-email-format" + "테스트유저" ); //when //then mockMvc.perform(post("/users/signup") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value("40002")) - .andExpect(jsonPath("$.message", containsString("이메일 형식이 올바르지 않습니다."))); + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(AUTH_TOKEN_NOT_FOUND.getCode())) + .andExpect(jsonPath("$.message", containsString(AUTH_TOKEN_NOT_FOUND.getMessage()))); } + + } diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/VerifyNicknameControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java similarity index 89% rename from src/test/java/konkuk/thip/user/adapter/in/web/VerifyNicknameControllerTest.java rename to src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java index a2c04a7f3..e56c806cc 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/VerifyNicknameControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import konkuk.thip.user.adapter.in.web.request.PostUserVerifyNicknameRequest; +import konkuk.thip.user.adapter.in.web.request.UserVerifyNicknameRequest; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; @@ -28,8 +28,8 @@ @SpringBootTest @ActiveProfiles("test") -@AutoConfigureMockMvc -class VerifyNicknameControllerTest { +@AutoConfigureMockMvc(addFilters = false) +class UserVerifyNicknameControllerTest { @Autowired private MockMvc mockMvc; @@ -53,7 +53,7 @@ void tearDown() { @DisplayName("[닉네임]값이 unique 할 경우, true를 반환한다.") void verify_nickname_true() throws Exception { //given - PostUserVerifyNicknameRequest request = new PostUserVerifyNicknameRequest("테스트유저"); + UserVerifyNicknameRequest request = new UserVerifyNicknameRequest("테스트유저"); //when ResultActions result = mockMvc.perform(post("/users/nickname") @@ -83,15 +83,15 @@ void verify_nickname_false() throws Exception { aliasJpaRepository.save(aliasJpaEntity); UserJpaEntity userJpaEntity = UserJpaEntity.builder() - .email("test@test.com") .nickname("테스트유저") .imageUrl("http://image.url") .role(USER) + .oauth2Id("kakao_12345678") .aliasForUserJpaEntity(aliasJpaEntity) .build(); userJpaRepository.save(userJpaEntity); - PostUserVerifyNicknameRequest request = new PostUserVerifyNicknameRequest("테스트유저"); + UserVerifyNicknameRequest request = new UserVerifyNicknameRequest("테스트유저"); //when ResultActions result = mockMvc.perform(post("/users/nickname") @@ -113,7 +113,7 @@ void verify_nickname_false() throws Exception { @DisplayName("[닉네임]값이 공백일 경우, 400 error가 발생한다.") void nickname_blank() throws Exception { //given: nickname blank - PostUserVerifyNicknameRequest request = new PostUserVerifyNicknameRequest(""); + UserVerifyNicknameRequest request = new UserVerifyNicknameRequest(""); //when //then mockMvc.perform(post("/users/nickname") @@ -128,7 +128,7 @@ void nickname_blank() throws Exception { @DisplayName("[닉네임]값이 한글, 영어, 숫자 외의 문자를 포함할 경우, 400 error가 발생한다.") void nickname_invalid_pattern() throws Exception { //given: nickname with invalid characters - PostUserVerifyNicknameRequest request = new PostUserVerifyNicknameRequest("닉네임!!"); + UserVerifyNicknameRequest request = new UserVerifyNicknameRequest("닉네임!!"); //when //then mockMvc.perform(post("/users/nickname") @@ -143,7 +143,7 @@ void nickname_invalid_pattern() throws Exception { @DisplayName("[닉네임]값이 11자 이상일 경우, 400 error가 발생한다.") void nickname_too_long() throws Exception { //given: 11글자 nickname - PostUserVerifyNicknameRequest request = new PostUserVerifyNicknameRequest("11글자닉네임입니다아"); + UserVerifyNicknameRequest request = new UserVerifyNicknameRequest("11글자닉네임입니다아"); //when //then mockMvc.perform(post("/users/nickname") diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java similarity index 90% rename from src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java rename to src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java index 892e45555..2514b10d7 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; import konkuk.thip.room.adapter.out.persistence.CategoryJpaRepository; -import konkuk.thip.user.adapter.in.web.response.GetUserShowAliasChoiceResponse; +import konkuk.thip.user.adapter.in.web.response.UserViewAliasChoiceResponse; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; import org.junit.jupiter.api.AfterEach; @@ -28,8 +28,8 @@ @SpringBootTest @ActiveProfiles("test") -@AutoConfigureMockMvc -class ShowAliasChoiceViewControllerTest { +@AutoConfigureMockMvc(addFilters = false) +class UserViewAliasChoiceControllerTest { @Autowired private MockMvc mockMvc; @@ -65,8 +65,8 @@ void show_alias_choice_view() throws Exception { String json = result.andReturn().getResponse().getContentAsString(); JsonNode jsonNode = objectMapper.readTree(json); - GetUserShowAliasChoiceResponse showResponse = objectMapper.treeToValue(jsonNode.get("data"), GetUserShowAliasChoiceResponse.class); - List choices = showResponse.aliasChoices(); + UserViewAliasChoiceResponse showResponse = objectMapper.treeToValue(jsonNode.get("data"), UserViewAliasChoiceResponse.class); + List choices = showResponse.aliasChoices(); assertThat(choices).hasSize(2); assertThat(choices) diff --git a/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java b/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java index fc2056ac3..f303de831 100644 --- a/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java +++ b/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java @@ -2,6 +2,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; import org.junit.jupiter.api.DisplayName; @@ -31,23 +32,10 @@ class UserJpaEntityTest { @DisplayName("UserJpaEntity 저장 및 조회 테스트") void saveAndFindUser() { // given - AliasJpaEntity alias = AliasJpaEntity.builder() - .value("칭호") - .imageUrl("test-image-url") - .color("red") - .build(); - aliasJpaRepository.save(alias); - - UserJpaEntity user = UserJpaEntity.builder() - .email("test@test.com") - .nickname("테스터") - .imageUrl("https://test.img") - .aliasForUserJpaEntity(alias) - .role(UserRole.USER) - .build(); + AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createAlias()); + UserJpaEntity user = userRepository.save(TestEntityFactory.createUser(alias)); // when - userRepository.save(user); em.flush(); em.clear();