diff --git a/src/main/java/com/alom/dorundorunbe/global/config/SecurityConfig.java b/src/main/java/com/alom/dorundorunbe/global/config/SecurityConfig.java new file mode 100644 index 0000000..561c883 --- /dev/null +++ b/src/main/java/com/alom/dorundorunbe/global/config/SecurityConfig.java @@ -0,0 +1,33 @@ +package com.alom.dorundorunbe.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) + throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) // CSRF 보호 비활성화 + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/swagger-resources/**", + "/webjars/**", + "/actuator/**", + "/doodle/**", + "/challenges/**" + ).permitAll() // Swagger 및 관련 리소스 허용 + .anyRequest().authenticated() // 나머지 요청은 인증 필요 + ); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/alom/dorundorunbe/global/config/SwaggerConfig.java b/src/main/java/com/alom/dorundorunbe/global/config/SwaggerConfig.java new file mode 100644 index 0000000..8379f23 --- /dev/null +++ b/src/main/java/com/alom/dorundorunbe/global/config/SwaggerConfig.java @@ -0,0 +1,25 @@ +package com.alom.dorundorunbe.global.config; + +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .components(new Components()) + .info(apiInfo()); + } + + private Info apiInfo() { + return new Info() + .title("두런두런 백엔드 API") + .description("두런두런과 함께 비대면 러닝을 시작해보세요") + .version("1.0.0"); + } +} \ No newline at end of file diff --git a/src/main/java/com/alom/dorundorunbe/global/exception/BusinessException.java b/src/main/java/com/alom/dorundorunbe/global/exception/BusinessException.java new file mode 100644 index 0000000..3b241f3 --- /dev/null +++ b/src/main/java/com/alom/dorundorunbe/global/exception/BusinessException.java @@ -0,0 +1,22 @@ +package com.alom.dorundorunbe.global.exception; + +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException{ + + private final ErrorCode errorCode; + private final String detail; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + this.detail = ""; + } + + public BusinessException(ErrorCode errorCode, String detail) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + this.detail = detail; + } +} \ No newline at end of file diff --git a/src/main/java/com/alom/dorundorunbe/global/exception/ErrorCode.java b/src/main/java/com/alom/dorundorunbe/global/exception/ErrorCode.java new file mode 100644 index 0000000..47a0cc4 --- /dev/null +++ b/src/main/java/com/alom/dorundorunbe/global/exception/ErrorCode.java @@ -0,0 +1,45 @@ +package com.alom.dorundorunbe.global.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + // 챌린지 + NO_SUCH_CHALLENGE(HttpStatus.NOT_FOUND, "존재하지 않는 챌린지입니다."), + // 알려지지 않은 문제 + UNKNOWN(HttpStatus.INTERNAL_SERVER_ERROR, "알려지지 않은 문제가 발생하였습니다."), + // 입력 값 오류 + BLANK_ARGUMENT(HttpStatus.BAD_REQUEST, "비어있는 값이 존재합니다."), + INVALID_EMAIL_FORMAT(HttpStatus.BAD_REQUEST, "이메일 형식이 올바르지 않습니다."), + PASSWORD_POLICY_VIOLATION(HttpStatus.BAD_REQUEST, "비밀번호 조건을 충족하지 않습니다."), + INVALID_SEARCH_CRITERIA(HttpStatus.BAD_REQUEST, "검색 조건을 잘못 설정하였습니다."), + PROFANITY_IN_NAME(HttpStatus.BAD_REQUEST, "이름에 비속어가 포함되어 있습니다."), + DUPLICATE_NAME(HttpStatus.CONFLICT, "중복된 이름입니다."), + // 계정 관련 오류 + EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 존재하는 이메일입니다."), + EMAIL_ALREADY_EXISTS_OTHER_AUTH(HttpStatus.CONFLICT, "이미 다른 방식으로 인증되어 있는 이메일입니다."), + INVALID_EMAIL_OR_PASSWORD(HttpStatus.UNAUTHORIZED, "이메일 또는 비밀번호가 틀립니다."), + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 사용자입니다."), + // OAuth 및 토큰 오류 + OAUTH_COMMUNICATION_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, "OAuth 통신에 실패하였습니다."), + INVALID_OAUTH_CODE(HttpStatus.BAD_REQUEST, "올바르지 않은 OAuth code입니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "올바르지 않은 토큰입니다."), + EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 엑세스 토큰입니다."), + EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 리프레시 토큰입니다."), + TAMPERED_TOKEN_SIGNATURE(HttpStatus.UNAUTHORIZED, "서명이 변형된 토큰입니다."), + EMPTY_TOKEN_PROVIDED(HttpStatus.BAD_REQUEST, "요청에 토큰이 비어있습니다."), + INSUFFICIENT_MEMBER_PERMISSION(HttpStatus.FORBIDDEN, "권한이 부족한 사용자입니다."), + // 메일 전송 오류 + EMAIL_SENDING_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, "메일 전송에 실패하였습니다."), + EMAIL_VERIFIED_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, "메일 인증에 실패하였습니다."), + // 프로세스 오류 + FAIL_PROCEED(HttpStatus.INTERNAL_SERVER_ERROR, "프로세스 실행중 문제가 발생하였습니다."), + // 정상 + SUCCESS(HttpStatus.OK, "SUCCESS"); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/com/alom/dorundorunbe/global/exception/RestExceptionHandler.java b/src/main/java/com/alom/dorundorunbe/global/exception/RestExceptionHandler.java new file mode 100644 index 0000000..75b84ba --- /dev/null +++ b/src/main/java/com/alom/dorundorunbe/global/exception/RestExceptionHandler.java @@ -0,0 +1,35 @@ +package com.alom.dorundorunbe.global.exception; + +import com.alom.dorundorunbe.global.util.BasicResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class RestExceptionHandler { + private final Logger LOGGER = LoggerFactory.getLogger(RestExceptionHandler.class); + @ExceptionHandler(BusinessException.class) + public ResponseEntity handleBusinessException(BusinessException exception) { + LOGGER.info("[ExceptionHandler] Message: {}, Detail: {}", exception.getMessage(), exception.getDetail()); + + return BasicResponse.to(exception); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException exception) { + LOGGER.error("[ExceptionHandler] Runtime exception occurred: ", exception); + + ErrorCode errorCode = ErrorCode.FAIL_PROCEED; + HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; + + BusinessException businessException = new BusinessException(errorCode, "서버 내부 오류가 발생했습니다."); + LOGGER.error("[ExceptionHandler] Message: {}, Detail: {}", businessException.getMessage(), exception.getMessage()); + + return ResponseEntity + .status(status) + .body(BasicResponse.of(businessException)); + } +} \ No newline at end of file diff --git a/src/main/java/com/alom/dorundorunbe/global/util/BasicResponse.java b/src/main/java/com/alom/dorundorunbe/global/util/BasicResponse.java new file mode 100644 index 0000000..85d60ec --- /dev/null +++ b/src/main/java/com/alom/dorundorunbe/global/util/BasicResponse.java @@ -0,0 +1,33 @@ +package com.alom.dorundorunbe.global.util; + +import com.alom.dorundorunbe.global.exception.BusinessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public record BasicResponse( + HttpStatus status, + String message, + String detail +) { + public static BasicResponse of(BusinessException exception) { + return new BasicResponse( + exception.getErrorCode().getHttpStatus(), + exception.getErrorCode().getMessage(), + exception.getDetail() + ); + } + + public static BasicResponse of(String message) { + return new BasicResponse( + HttpStatus.OK, + message, + "" + ); + } + + public static ResponseEntity to(BusinessException exception) { + return ResponseEntity + .status(exception.getErrorCode().getHttpStatus()) + .body(BasicResponse.of(exception)); + } +}