diff --git a/server/src/main/java/com/ticketing/server/global/exception/AlreadyDeletedException.java b/server/src/main/java/com/ticketing/server/global/exception/AlreadyDeletedException.java deleted file mode 100644 index 7c0e004..0000000 --- a/server/src/main/java/com/ticketing/server/global/exception/AlreadyDeletedException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ticketing.server.global.exception; - -public class AlreadyDeletedException extends RuntimeException { - - public AlreadyDeletedException(String message) { - super(message); - } - -} diff --git a/server/src/main/java/com/ticketing/server/global/exception/EmailNotFoundException.java b/server/src/main/java/com/ticketing/server/global/exception/EmailNotFoundException.java deleted file mode 100644 index 3f621fc..0000000 --- a/server/src/main/java/com/ticketing/server/global/exception/EmailNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.ticketing.server.global.exception; - -public class EmailNotFoundException extends IllegalArgumentException { - - private static final String MESSAGE = "존재하지 않는 이메일 입니다."; - - public EmailNotFoundException() { - super(MESSAGE); - } - -} diff --git a/server/src/main/java/com/ticketing/server/global/exception/ErrorCode.java b/server/src/main/java/com/ticketing/server/global/exception/ErrorCode.java new file mode 100644 index 0000000..f281c48 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/global/exception/ErrorCode.java @@ -0,0 +1,31 @@ +package com.ticketing.server.global.exception; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CONFLICT; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + + /* 400 BAD_REQUEST : 잘못된 요청 */ + MISMATCH_PASSWORD(BAD_REQUEST, "비밀번호가 일치하지 않습니다."), + TOKEN_TYPE(BAD_REQUEST, "토큰 타입이 올바르지 않습니다."), + UNAVAILABLE_REFRESH_TOKEN(BAD_REQUEST, "사용할 수 없는 토큰 입니다."), + + /* 404 NOT_FOUND : Resource 를 찾을 수 없음 */ + EMAIL_NOT_FOUND(NOT_FOUND, "해당 이메일을 찾을 수 없습니다."), + REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "리프레쉬 토큰을 찾을 수 없습니다."), + + /* 409 CONFLICT : Resource 의 현재 상태와 충돌. 보통 중복된 데이터 존재 */ + DUPLICATE_EMAIL(CONFLICT, "이메일이 이미 존재합니다."), + DELETED_EMAIL(CONFLICT, "이미 삭제된 이메일 입니다."); + + private final HttpStatus httpStatus; + private final String detail; + +} diff --git a/server/src/main/java/com/ticketing/server/global/exception/ErrorResponse.java b/server/src/main/java/com/ticketing/server/global/exception/ErrorResponse.java new file mode 100644 index 0000000..f1adc2a --- /dev/null +++ b/server/src/main/java/com/ticketing/server/global/exception/ErrorResponse.java @@ -0,0 +1,35 @@ +package com.ticketing.server.global.exception; + +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@EqualsAndHashCode +public class ErrorResponse { + + private final HttpStatus status; + private final String message; + private final List errors; + + public ErrorResponse(HttpStatus status, String message, List errors) { + this.status = status; + this.message = message; + this.errors = errors; + } + + public ErrorResponse(HttpStatus status, String message, String error) { + this.status = status; + this.message = message; + this.errors = List.of(error); + } + + public static ErrorResponse toErrorResponse(ErrorCode errorCode) { + return new ErrorResponse( + errorCode.getHttpStatus(), + errorCode.name(), + errorCode.getDetail()); + } + +} diff --git a/server/src/main/java/com/ticketing/server/global/exception/GlobalExceptionHandler.java b/server/src/main/java/com/ticketing/server/global/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..ab0207a --- /dev/null +++ b/server/src/main/java/com/ticketing/server/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,246 @@ +package com.ticketing.server.global.exception; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.METHOD_NOT_ALLOWED; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import static org.springframework.http.HttpStatus.UNSUPPORTED_MEDIA_TYPE; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.TypeMismatchException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + /* 400 START */ + + /** + * Valid 유효성 검사 실패 + */ + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + log.error("MethodArgumentNotValidException :: ", ex); + + List errors = generateErrors(ex); + ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), errors); + return handleExceptionInternal(ex, response, headers, response.getStatus(), request); + } + + /*** + * ModelAttribute 으로 binding error 발생 + */ + @Override + protected ResponseEntity handleBindException( + BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + log.error("BindException :: ", ex); + + List errors = generateErrors(ex); + ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), errors); + return handleExceptionInternal(ex, response, headers, response.getStatus(), request); + } + + /** + * 잘못된 유형으로 Bean 속성 설정 + */ + @Override + protected ResponseEntity handleTypeMismatch( + TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + log.error("TypeMismatchException :: ", ex); + + String error = ex.getValue() + " value for " + ex.getPropertyName() + " should be of type " + ex.getRequiredType(); + ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), error); + return ResponseEntity.status(response.getStatus()).headers(headers).body(response); + } + + /** + * multipart/form-data 요청 실패 + */ + @Override + protected ResponseEntity handleMissingServletRequestPart( + MissingServletRequestPartException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + log.error("MissingServletRequestPartException :: ", ex); + + String error = ex.getRequestPartName() + " part is missing"; + ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), error); + return ResponseEntity.status(response.getStatus()).headers(headers).body(response); + } + + /** + * 필수 인수 누락 + */ + @Override + protected ResponseEntity handleMissingServletRequestParameter( + MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + log.error("MissingServletRequestParameterException :: ", ex); + + String error = ex.getParameterName() + " parameter is missing"; + ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), error); + return ResponseEntity.status(response.getStatus()).headers(headers).body(response); + } + + /** + * 인수가 예상한 형식이 아닐 시 + */ + @ExceptionHandler(value = MethodArgumentTypeMismatchException.class) + protected ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex) { + log.error("MethodArgumentTypeMismatchException :: ", ex); + + String error = ex.getName() + " should be of type " + Objects.requireNonNull(ex.getRequiredType()).getName(); + ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), error); + return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response); + } + + /** + * 제약 조건 위반 + */ + @ExceptionHandler(value = ConstraintViolationException.class) + protected ResponseEntity handleConstraintViolation(ConstraintViolationException ex) { + log.error("ConstraintViolationException :: ", ex); + + List errors = new ArrayList<>(); + for (ConstraintViolation violation : ex.getConstraintViolations()) { + errors.add(violation.getRootBeanClass().getName() + " " + violation.getPropertyPath() + ": " + violation.getMessage()); + } + + ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), errors); + return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response); + } + + /* 400 END */ + + /** + * 404 발생 + */ + @Override + protected ResponseEntity handleNoHandlerFoundException( + NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + log.error("NoHandlerFoundException :: ", ex); + + String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL(); + ErrorResponse response = new ErrorResponse(NOT_FOUND, ex.getLocalizedMessage(), error); + return ResponseEntity.status(response.getStatus()).headers(headers).body(response); + } + + /** + * 지원하지 않는 HTTP 메서드로 요청 405 + */ + @Override + protected ResponseEntity handleHttpRequestMethodNotSupported( + HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + log.error("HttpRequestMethodNotSupportedException :: ", ex); + + StringBuilder builder = new StringBuilder(); + builder.append(ex.getMethod()); + builder.append(" method is not supported for this request. Supported methods are "); + + Set supportedHttpMethods = ex.getSupportedHttpMethods(); + if (supportedHttpMethods != null) { + supportedHttpMethods.forEach(t -> builder.append(t).append(" ")); + } + + ErrorResponse response = new ErrorResponse(METHOD_NOT_ALLOWED, ex.getLocalizedMessage(), builder.toString()); + return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response); + } + + /** + * 지원되지 않는 미디어 유형으로 요청 415 + */ + @Override + protected ResponseEntity handleHttpMediaTypeNotSupported( + HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + log.error("HttpMediaTypeNotSupportedException :: ", ex); + + StringBuilder builder = new StringBuilder(); + builder.append(ex.getContentType()); + builder.append(" media type is not supported. Supported media types are "); + ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(" ")); + + ErrorResponse response = new ErrorResponse(UNSUPPORTED_MEDIA_TYPE, ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2)); + return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response); + } + + /** + * 특정 핸들러 없는 모든 예외 500 + */ + @ExceptionHandler(value = Exception.class) + protected ResponseEntity handleAll(Exception ex) { + log.error("Exception :: ", ex); + + ErrorResponse response = new ErrorResponse(INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "오류가 발생했습니다."); + return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response); + } + + /** + * 접근 권한이 없을 때 + */ + @ExceptionHandler(value = AccessDeniedException.class) + protected ResponseEntity handleAccessDeniedException(Exception ex) { + log.error("AccessDeniedException :: ", ex); + + ErrorResponse response = new ErrorResponse(FORBIDDEN, ex.getLocalizedMessage(), "접근 권한이 없습니다."); + return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response); + } + + /** + * 인증 정보가 없을 때 + */ + @ExceptionHandler(value = AuthenticationException.class) + protected ResponseEntity handleAuthenticationException(Exception ex) { + log.error("AuthenticationException :: ", ex); + + ErrorResponse response = new ErrorResponse(UNAUTHORIZED, ex.getLocalizedMessage(), "로그인 후 이용하실 수 있습니다."); + return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response); + } + + /** + * 커스텀 예외 발생 시 + */ + @ExceptionHandler(value = TicketingException.class) + protected ResponseEntity ticketingException(TicketingException ex) { + log.error("TicketingException :: ", ex); + + ErrorCode errorCode = ex.getErrorCode(); + return ResponseEntity.status(errorCode.getHttpStatus()).body(ErrorResponse.toErrorResponse(errorCode)); + } + + private List generateErrors(BindException ex) { + List errors = new ArrayList<>(); + List allErrors = ex.getBindingResult().getAllErrors(); + + for (ObjectError error : allErrors) { + errors.add(((FieldError) error).getField() + ": " + error.getDefaultMessage()); + } + return errors; + } + +} diff --git a/server/src/main/java/com/ticketing/server/global/exception/PasswordMismatchException.java b/server/src/main/java/com/ticketing/server/global/exception/PasswordMismatchException.java deleted file mode 100644 index d2a2a46..0000000 --- a/server/src/main/java/com/ticketing/server/global/exception/PasswordMismatchException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.ticketing.server.global.exception; - -public class PasswordMismatchException extends RuntimeException { - - private static final String MESSAGE = "패스워드가 일치하지 않습니다"; - - public PasswordMismatchException() { - super(MESSAGE); - } - -} diff --git a/server/src/main/java/com/ticketing/server/global/exception/TicketingException.java b/server/src/main/java/com/ticketing/server/global/exception/TicketingException.java new file mode 100644 index 0000000..3810ae4 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/global/exception/TicketingException.java @@ -0,0 +1,12 @@ +package com.ticketing.server.global.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketingException extends RuntimeException { + + private final ErrorCode errorCode; + +} diff --git a/server/src/main/java/com/ticketing/server/global/exception/token/TokenException.java b/server/src/main/java/com/ticketing/server/global/exception/token/TokenException.java deleted file mode 100644 index 7baa754..0000000 --- a/server/src/main/java/com/ticketing/server/global/exception/token/TokenException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ticketing.server.global.exception.token; - -public class TokenException extends RuntimeException { - - public TokenException(String message) { - super(message); - } - -} diff --git a/server/src/main/java/com/ticketing/server/global/exception/token/TokenNotFindException.java b/server/src/main/java/com/ticketing/server/global/exception/token/TokenNotFindException.java deleted file mode 100644 index 72aff4d..0000000 --- a/server/src/main/java/com/ticketing/server/global/exception/token/TokenNotFindException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.ticketing.server.global.exception.token; - -public class TokenNotFindException extends TokenException { - - private static final String MESSAGE = "일치하는 토큰을 찾지 못하였습니다."; - - public TokenNotFindException() { - super(MESSAGE); - } -} diff --git a/server/src/main/java/com/ticketing/server/global/exception/token/TokenTypeException.java b/server/src/main/java/com/ticketing/server/global/exception/token/TokenTypeException.java deleted file mode 100644 index cea8003..0000000 --- a/server/src/main/java/com/ticketing/server/global/exception/token/TokenTypeException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.ticketing.server.global.exception.token; - -public class TokenTypeException extends TokenException { - - private static final String MESSAGE = "토큰 타입이 일치하지 않습니다."; - - public TokenTypeException() { - super(MESSAGE); - } -} diff --git a/server/src/main/java/com/ticketing/server/global/exception/token/UnavailableRefreshTokenException.java b/server/src/main/java/com/ticketing/server/global/exception/token/UnavailableRefreshTokenException.java deleted file mode 100644 index 23c370e..0000000 --- a/server/src/main/java/com/ticketing/server/global/exception/token/UnavailableRefreshTokenException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.ticketing.server.global.exception.token; - -public class UnavailableRefreshTokenException extends TokenException { - - private static final String MESSAGE = "사용할 수 없는 refresh Token 입니다."; - - public UnavailableRefreshTokenException() { - super(MESSAGE); - } - -} diff --git a/server/src/main/java/com/ticketing/server/global/security/jwt/handle/JwtAccessDeniedHandler.java b/server/src/main/java/com/ticketing/server/global/security/jwt/handle/JwtAccessDeniedHandler.java index 9cad636..4436b87 100644 --- a/server/src/main/java/com/ticketing/server/global/security/jwt/handle/JwtAccessDeniedHandler.java +++ b/server/src/main/java/com/ticketing/server/global/security/jwt/handle/JwtAccessDeniedHandler.java @@ -3,15 +3,24 @@ package com.ticketing.server.global.security.jwt.handle; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerExceptionResolver; -@Component +@Component("JwtAccessDeniedHandler") public class JwtAccessDeniedHandler implements AccessDeniedHandler { + private final HandlerExceptionResolver resolver; + + public JwtAccessDeniedHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) { + this.resolver = resolver; + } + @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { response.sendError(HttpServletResponse.SC_FORBIDDEN); + resolver.resolveException(request, response, null, accessDeniedException); } } diff --git a/server/src/main/java/com/ticketing/server/global/security/jwt/handle/JwtAuthenticationEntryPoint.java b/server/src/main/java/com/ticketing/server/global/security/jwt/handle/JwtAuthenticationEntryPoint.java index aef6c0d..a3f5bc0 100644 --- a/server/src/main/java/com/ticketing/server/global/security/jwt/handle/JwtAuthenticationEntryPoint.java +++ b/server/src/main/java/com/ticketing/server/global/security/jwt/handle/JwtAuthenticationEntryPoint.java @@ -3,15 +3,24 @@ package com.ticketing.server.global.security.jwt.handle; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +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; -@Component +@Component("JwtAuthenticationEntryPoint") 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 { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + resolver.resolveException(request, response, null, authException); } } diff --git a/server/src/main/java/com/ticketing/server/user/domain/User.java b/server/src/main/java/com/ticketing/server/user/domain/User.java index df74ef4..d6475d2 100644 --- a/server/src/main/java/com/ticketing/server/user/domain/User.java +++ b/server/src/main/java/com/ticketing/server/user/domain/User.java @@ -1,8 +1,10 @@ package com.ticketing.server.user.domain; +import static com.ticketing.server.global.exception.ErrorCode.DELETED_EMAIL; +import static com.ticketing.server.global.exception.ErrorCode.MISMATCH_PASSWORD; + import com.ticketing.server.global.dto.repository.AbstractEntity; -import com.ticketing.server.global.exception.AlreadyDeletedException; -import com.ticketing.server.global.exception.PasswordMismatchException; +import com.ticketing.server.global.exception.TicketingException; import com.ticketing.server.global.validator.constraints.Phone; import com.ticketing.server.user.service.dto.ChangePasswordDTO; import com.ticketing.server.user.service.dto.DeleteUserDTO; @@ -60,7 +62,7 @@ public class User extends AbstractEntity { public User delete(DeleteUserDTO deleteUser) { if (isDeleted) { - throw new AlreadyDeletedException("이미 탈퇴된 회원 입니다."); + throw new TicketingException(DELETED_EMAIL); } checkPassword(deleteUser); @@ -79,7 +81,7 @@ public class User extends AbstractEntity { public void checkPassword(PasswordMatches passwordMatches) { if (!passwordMatches.passwordMatches(password)) { - throw new PasswordMismatchException(); + throw new TicketingException(MISMATCH_PASSWORD); } } diff --git a/server/src/main/java/com/ticketing/server/user/service/AuthenticationServiceImpl.java b/server/src/main/java/com/ticketing/server/user/service/AuthenticationServiceImpl.java index 5f0df8a..4915754 100644 --- a/server/src/main/java/com/ticketing/server/user/service/AuthenticationServiceImpl.java +++ b/server/src/main/java/com/ticketing/server/user/service/AuthenticationServiceImpl.java @@ -1,8 +1,10 @@ package com.ticketing.server.user.service; -import com.ticketing.server.global.exception.token.TokenNotFindException; -import com.ticketing.server.global.exception.token.TokenTypeException; -import com.ticketing.server.global.exception.token.UnavailableRefreshTokenException; +import static com.ticketing.server.global.exception.ErrorCode.REFRESH_TOKEN_NOT_FOUND; +import static com.ticketing.server.global.exception.ErrorCode.TOKEN_TYPE; +import static com.ticketing.server.global.exception.ErrorCode.UNAVAILABLE_REFRESH_TOKEN; + +import com.ticketing.server.global.exception.TicketingException; import com.ticketing.server.global.redis.RefreshRedisRepository; import com.ticketing.server.global.redis.RefreshToken; import com.ticketing.server.global.security.jwt.JwtProperties; @@ -60,11 +62,11 @@ public class AuthenticationServiceImpl implements AuthenticationService { // Redis 에 토큰이 있는지 검증 RefreshToken findTokenEntity = refreshRedisRepository.findByEmail(authentication.getName()) - .orElseThrow(TokenNotFindException::new); + .orElseThrow(() -> new TicketingException(REFRESH_TOKEN_NOT_FOUND)); // redis 토큰과 input 토큰이 일치한지 확인 if (!refreshToken.equals(findTokenEntity.getToken())) { - throw new UnavailableRefreshTokenException(); + throw new TicketingException(UNAVAILABLE_REFRESH_TOKEN); } // 토큰 발급 @@ -81,7 +83,7 @@ public class AuthenticationServiceImpl implements AuthenticationService { if (StringUtils.hasText(bearerToken) && jwtProperties.hasTokenStartsWith(bearerToken)) { return bearerToken.substring(7); } - throw new TokenTypeException(); + throw new TicketingException(TOKEN_TYPE); } } diff --git a/server/src/main/java/com/ticketing/server/user/service/CustomUserDetailsService.java b/server/src/main/java/com/ticketing/server/user/service/CustomUserDetailsService.java index a54b0de..bd90e5d 100644 --- a/server/src/main/java/com/ticketing/server/user/service/CustomUserDetailsService.java +++ b/server/src/main/java/com/ticketing/server/user/service/CustomUserDetailsService.java @@ -1,5 +1,8 @@ package com.ticketing.server.user.service; +import static com.ticketing.server.global.exception.ErrorCode.EMAIL_NOT_FOUND; + +import com.ticketing.server.global.exception.TicketingException; import com.ticketing.server.user.domain.User; import com.ticketing.server.user.domain.repository.UserRepository; import java.util.Collections; @@ -20,7 +23,7 @@ public class CustomUserDetailsService implements UserDetailsService { public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return userRepository.findByEmailAndIsDeletedFalse(email) .map(this::createUserDetails) - .orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 email 입니다. :: " + email)); + .orElseThrow(() -> new TicketingException(EMAIL_NOT_FOUND)); } private UserDetails createUserDetails(User user) { diff --git a/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java b/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java index d507cbb..0dddf86 100644 --- a/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java +++ b/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java @@ -1,6 +1,9 @@ package com.ticketing.server.user.service; -import com.ticketing.server.global.exception.EmailNotFoundException; +import static com.ticketing.server.global.exception.ErrorCode.DUPLICATE_EMAIL; +import static com.ticketing.server.global.exception.ErrorCode.EMAIL_NOT_FOUND; + +import com.ticketing.server.global.exception.TicketingException; import com.ticketing.server.user.domain.User; import com.ticketing.server.user.domain.repository.UserRepository; import com.ticketing.server.user.service.dto.ChangePasswordDTO; @@ -28,18 +31,17 @@ public class UserServiceImpl implements UserService { @Transactional public User register(@Valid SignUpDTO signUpDto) { Optional user = userRepository.findByEmail(signUpDto.getEmail()); - if (user.isPresent()) { - log.error("이미 존재하는 이메일이기 때문에 신규 회원가입을 진행할 수 없습니다. :: {}", signUpDto); - throw new IllegalArgumentException("이미 존재하는 이메일이기 때문에 신규 회원가입을 진행할 수 없습니다."); + if (user.isEmpty()) { + return userRepository.save(signUpDto.toUser()); } - return userRepository.save(signUpDto.toUser()); + throw new TicketingException(DUPLICATE_EMAIL); } @Override @Transactional public User delete(@Valid DeleteUserDTO deleteUserDto) { - User user = findByEmail(deleteUserDto.getEmail()); + User user = findNotDeletedUserByEmail(deleteUserDto.getEmail()); return user.delete(deleteUserDto); } @@ -53,19 +55,16 @@ public class UserServiceImpl implements UserService { @Override public User findByEmail(String email) { return userRepository.findByEmail(email) - .orElseThrow(() -> { - log.error("존재하지 않는 이메일 입니다. :: {}", email); - throw new EmailNotFoundException(); - } - ); + .orElseThrow(UserServiceImpl::throwEmailNotFound); } private User findNotDeletedUserByEmail(String email) { return userRepository.findByEmailAndIsDeletedFalse(email) - .orElseThrow(() -> { - log.error("존재하지 않는 이메일 입니다. :: {}", email); - throw new EmailNotFoundException(); - }); + .orElseThrow(UserServiceImpl::throwEmailNotFound); + } + + private static RuntimeException throwEmailNotFound() { + throw new TicketingException(EMAIL_NOT_FOUND); } } diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index a2bffb9..b84227a 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -21,6 +21,7 @@ spring: mvc: pathmatch: matching-strategy: ant_path_matcher + throw-exception-if-no-handler-found: true jasypt: encryptor: diff --git a/server/src/main/resources/i18n/messages.properties b/server/src/main/resources/i18n/messages.properties index d356a7f..0f011da 100644 --- a/server/src/main/resources/i18n/messages.properties +++ b/server/src/main/resources/i18n/messages.properties @@ -1,10 +1,10 @@ -validation.not.empty.name="\uC774\uB984\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.email="\uC774\uBA54\uC77C\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.password="\uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.oldpassword="\uD604\uC7AC \uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.newpassword="\uBCC0\uACBD\uD560 \uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.grade="\uC0AC\uC6A9\uC790 \uB4F1\uAE09\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.phone="\uD734\uB300\uBC88\uD638\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.email="\uC774\uBA54\uC77C\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." -validation.phone="\uD734\uB300\uBC88\uD638\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." -validation.password.not.change="\uB3D9\uC77C\uD55C \uD328\uC2A4\uC6CC\uB4DC\uB85C \uBCC0\uACBD\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." +validation.not.empty.name=\uC774\uB984\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.email=\uC774\uBA54\uC77C\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.password=\uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.oldpassword=\uD604\uC7AC \uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.newpassword=\uBCC0\uACBD\uD560 \uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.grade=\uC0AC\uC6A9\uC790 \uB4F1\uAE09\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.phone=\uD734\uB300\uBC88\uD638\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.email=\uC774\uBA54\uC77C\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. +validation.phone=\uD734\uB300\uBC88\uD638\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. +validation.password.not.change=\uB3D9\uC77C\uD55C \uD328\uC2A4\uC6CC\uB4DC\uB85C \uBCC0\uACBD\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. diff --git a/server/src/main/resources/i18n/messages_en.properties b/server/src/main/resources/i18n/messages_en.properties index 6538fff..1972374 100644 --- a/server/src/main/resources/i18n/messages_en.properties +++ b/server/src/main/resources/i18n/messages_en.properties @@ -1,10 +1,10 @@ -validation.not.empty.name="name is required." -validation.not.empty.email="email is required." -validation.not.empty.password="password is required." -validation.not.empty.oldpassword="Old Password is required." -validation.not.empty.newpassword="New Password is required." -validation.not.empty.grade="user grade is required." -validation.not.empty.phone="phone is required." -validation.email="email is not valid." -validation.phone="phone is not valid." -validation.password.not.change="password not change." +validation.not.empty.name=name is required. +validation.not.empty.email=email is required. +validation.not.empty.password=password is required. +validation.not.empty.oldpassword=Old Password is required. +validation.not.empty.newpassword=New Password is required. +validation.not.empty.grade=user grade is required. +validation.not.empty.phone=phone is required. +validation.email=email is not valid. +validation.phone=phone is not valid. +validation.password.not.change=password not change. diff --git a/server/src/main/resources/i18n/messages_ko.properties b/server/src/main/resources/i18n/messages_ko.properties index d356a7f..0f011da 100644 --- a/server/src/main/resources/i18n/messages_ko.properties +++ b/server/src/main/resources/i18n/messages_ko.properties @@ -1,10 +1,10 @@ -validation.not.empty.name="\uC774\uB984\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.email="\uC774\uBA54\uC77C\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.password="\uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.oldpassword="\uD604\uC7AC \uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.newpassword="\uBCC0\uACBD\uD560 \uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.grade="\uC0AC\uC6A9\uC790 \uB4F1\uAE09\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.not.empty.phone="\uD734\uB300\uBC88\uD638\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4." -validation.email="\uC774\uBA54\uC77C\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." -validation.phone="\uD734\uB300\uBC88\uD638\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." -validation.password.not.change="\uB3D9\uC77C\uD55C \uD328\uC2A4\uC6CC\uB4DC\uB85C \uBCC0\uACBD\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4." +validation.not.empty.name=\uC774\uB984\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.email=\uC774\uBA54\uC77C\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.password=\uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.oldpassword=\uD604\uC7AC \uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.newpassword=\uBCC0\uACBD\uD560 \uD328\uC2A4\uC6CC\uB4DC\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.grade=\uC0AC\uC6A9\uC790 \uB4F1\uAE09\uC740 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.not.empty.phone=\uD734\uB300\uBC88\uD638\uB294 \uD544\uC218 \uC785\uB2C8\uB2E4. +validation.email=\uC774\uBA54\uC77C\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. +validation.phone=\uD734\uB300\uBC88\uD638\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. +validation.password.not.change=\uB3D9\uC77C\uD55C \uD328\uC2A4\uC6CC\uB4DC\uB85C \uBCC0\uACBD\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. diff --git a/server/src/test/java/com/ticketing/server/user/domain/UserTest.java b/server/src/test/java/com/ticketing/server/user/domain/UserTest.java index c8c7fa3..9a2aed6 100644 --- a/server/src/test/java/com/ticketing/server/user/domain/UserTest.java +++ b/server/src/test/java/com/ticketing/server/user/domain/UserTest.java @@ -4,8 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -import com.ticketing.server.global.exception.AlreadyDeletedException; -import com.ticketing.server.global.exception.PasswordMismatchException; +import com.ticketing.server.global.exception.TicketingException; import com.ticketing.server.user.service.dto.ChangePasswordDTO; import com.ticketing.server.user.service.dto.DeleteUserDTO; import com.ticketing.server.user.service.dto.DeleteUserDtoTest; @@ -45,7 +44,7 @@ class UserTest { // when // then assertThatThrownBy(() -> user.delete(deleteUser)) - .isInstanceOf(PasswordMismatchException.class); + .isInstanceOf(TicketingException.class); } @ParameterizedTest @@ -60,7 +59,8 @@ class UserTest { // then assertThatThrownBy(() -> user.delete(deleteUserDto)) - .isInstanceOf(AlreadyDeletedException.class); + .isInstanceOf(TicketingException.class); + } @ParameterizedTest @@ -90,7 +90,7 @@ class UserTest { // when // then assertThatThrownBy(() -> user.changePassword(changePasswordDto)) - .isInstanceOf(PasswordMismatchException.class); + .isInstanceOf(TicketingException.class); } @Test diff --git a/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java b/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java index 0ce2878..14668a8 100644 --- a/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java +++ b/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java @@ -6,7 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import com.ticketing.server.global.exception.EmailNotFoundException; +import com.ticketing.server.global.exception.TicketingException; import com.ticketing.server.user.domain.User; import com.ticketing.server.user.domain.UserGrade; import com.ticketing.server.user.domain.repository.UserRepository; @@ -54,7 +54,7 @@ class UserServiceImplTest { // when // then assertThatThrownBy(() -> userService.register(signUpDto)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(TicketingException.class); } @Test @@ -75,19 +75,19 @@ class UserServiceImplTest { @DisplayName("회원탈퇴 시 이메일이 존재하지 않을 경우") void deleteFail() { // given - when(userRepository.findByEmail("ticketing@gmail.com")).thenReturn(Optional.empty()); + when(userRepository.findByEmailAndIsDeletedFalse("ticketing@gmail.com")).thenReturn(Optional.empty()); // when // then assertThatThrownBy(() -> userService.delete(deleteUserDto)) - .isInstanceOf(EmailNotFoundException.class); + .isInstanceOf(TicketingException.class); } @Test @DisplayName("회원탈퇴 성공했을 경우") void deleteSuccess() { // given - when(userRepository.findByEmail("ticketing@gmail.com")).thenReturn(Optional.of(user)); + when(userRepository.findByEmailAndIsDeletedFalse("ticketing@gmail.com")).thenReturn(Optional.of(user)); // when User user = userService.delete(deleteUserDto); @@ -108,7 +108,7 @@ class UserServiceImplTest { // when // then assertThatThrownBy(() -> userService.changePassword(changePasswordDto)) - .isInstanceOf(EmailNotFoundException.class); + .isInstanceOf(TicketingException.class); } @Test