diff --git a/backend/build.gradle b/backend/build.gradle index 05a9fae..e99a9b9 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -43,6 +43,9 @@ dependencies { // GJson implementation 'com.google.code.gson:gson:2.7' + + //Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' } tasks.named('test') { diff --git a/backend/src/main/java/com/worlabel/global/config/SwaggerConfig.java b/backend/src/main/java/com/worlabel/global/config/SwaggerConfig.java new file mode 100644 index 0000000..1f48168 --- /dev/null +++ b/backend/src/main/java/com/worlabel/global/config/SwaggerConfig.java @@ -0,0 +1,58 @@ +package com.worlabel.global.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.springdoc.core.customizers.OperationCustomizer; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@OpenAPIDefinition( + info = @Info( + title = "auto labeling API", + description = "auto labeling API 목록입니다.", + version = "v1.0" + ) +) +@SecurityScheme( + name = "Authorization", + type = SecuritySchemeType.HTTP, + bearerFormat = "JWT", + in = SecuritySchemeIn.HEADER, + scheme = "Bearer", + description = "access token" +) +public class SwaggerConfig { + + private final String[] noRequiredTokenApi = {"/register", "/login", "/reissue","/login/oauth", "/oauth2/**", + "/login/oauth2/**", "/error", "login/oauth2/code/kakao", "/register/duplicate", "/test"}; + + private final OperationCustomizer operationCustomizer; + + public SwaggerConfig(OperationCustomizer operationCustomizer) { + this.operationCustomizer = operationCustomizer; + } + + @Bean + public GroupedOpenApi nonSecurityGroup(){ //jwt 토큰 불필요한 api + return GroupedOpenApi.builder() + .group("token 불필요 API") + .pathsToMatch(noRequiredTokenApi) + .addOperationCustomizer(operationCustomizer) + .build(); + } + + @Bean + public GroupedOpenApi securityGroup(){ //jwt 토큰 필요한 api + return GroupedOpenApi.builder() + .group("token 필요 API") + .pathsToExclude(noRequiredTokenApi) + .addOperationCustomizer(operationCustomizer) + .build(); + } +} + diff --git a/backend/src/main/java/com/worlabel/global/config/swagger/CustomOperationCustomizer.java b/backend/src/main/java/com/worlabel/global/config/swagger/CustomOperationCustomizer.java new file mode 100644 index 0000000..db07035 --- /dev/null +++ b/backend/src/main/java/com/worlabel/global/config/swagger/CustomOperationCustomizer.java @@ -0,0 +1,71 @@ +package com.worlabel.global.config.swagger; + +import com.worlabel.global.exception.CustomException; +import com.worlabel.global.response.BaseResponse; +import com.worlabel.global.response.CustomError; +import com.worlabel.global.response.ErrorResponse; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.examples.Example; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import lombok.AllArgsConstructor; + +import org.springdoc.core.customizers.OperationCustomizer; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; + +import com.worlabel.global.exception.ErrorCode; + +@Component +@AllArgsConstructor +public class CustomOperationCustomizer implements OperationCustomizer { + + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + SwaggerApiError swaggerApiError = handlerMethod.getMethodAnnotation(SwaggerApiError.class); + operation.getResponses().remove("500"); + if (swaggerApiError != null) { + generateErrorCodeResponse(operation, swaggerApiError.value()); + } + return operation; + } + + //에러 코드로 error response 만들어 ApiResponse 에 넣음 + private void generateErrorCodeResponse(Operation operation, ErrorCode[] errorCodes) { + ApiResponses responses = operation.getResponses(); + + Map>> statusWithErrorResponse = Arrays.stream(errorCodes) + .map(errorCode -> ErrorResponse.of(new CustomException(errorCode))) + .collect(Collectors.groupingBy(BaseResponse::getStatus)); + + addErrorCodesToResponse(responses, statusWithErrorResponse); + } + + //ApiResponses에 error response 추가 + private void addErrorCodesToResponse(ApiResponses apiResponses, Map>> responses) { + responses.forEach((status, value) -> { + Content content = new Content(); + MediaType mediaType = new MediaType(); + ApiResponse apiResponse = new ApiResponse(); + + value.forEach( + errorInfoResponse -> { + Example example = new Example(); + example.setValue(errorInfoResponse); + mediaType.addExamples(String.valueOf(errorInfoResponse.getCode()), example); + }); + + content.addMediaType("application/json", mediaType); + apiResponse.setContent(content); + apiResponses.addApiResponse(String.valueOf(status), apiResponse); + }); + } +} diff --git a/backend/src/main/java/com/worlabel/global/config/swagger/SwaggerApiError.java b/backend/src/main/java/com/worlabel/global/config/swagger/SwaggerApiError.java new file mode 100644 index 0000000..887775a --- /dev/null +++ b/backend/src/main/java/com/worlabel/global/config/swagger/SwaggerApiError.java @@ -0,0 +1,16 @@ +package com.worlabel.global.config.swagger; + +import com.worlabel.global.exception.ErrorCode; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SwaggerApiError { + + ErrorCode[] value(); +} + diff --git a/backend/src/main/java/com/worlabel/global/config/swagger/SwaggerApiSuccess.java b/backend/src/main/java/com/worlabel/global/config/swagger/SwaggerApiSuccess.java new file mode 100644 index 0000000..71676e5 --- /dev/null +++ b/backend/src/main/java/com/worlabel/global/config/swagger/SwaggerApiSuccess.java @@ -0,0 +1,20 @@ +package com.worlabel.global.config.swagger; + +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponses(value = { + @ApiResponse(responseCode = "200") +}) +public @interface SwaggerApiSuccess { + + String description() default ""; +} + diff --git a/backend/src/main/java/com/worlabel/global/response/BaseResponse.java b/backend/src/main/java/com/worlabel/global/response/BaseResponse.java index 5c0f7b4..ce705c4 100644 --- a/backend/src/main/java/com/worlabel/global/response/BaseResponse.java +++ b/backend/src/main/java/com/worlabel/global/response/BaseResponse.java @@ -22,6 +22,11 @@ public abstract class BaseResponse { @Getter(onMethod_ = @JsonProperty("isSuccess")) private boolean isSuccess; + /** + * 상태 코드 + */ + private int status; + /** * 응답 코드 */ @@ -42,8 +47,9 @@ public abstract class BaseResponse { */ protected List errors; - public BaseResponse(boolean isSuccess, int code, String message) { + public BaseResponse(boolean isSuccess, int status, int code, String message) { this.isSuccess = isSuccess; + this.status = status; this.code = code; this.message = message; this.data = null; diff --git a/backend/src/main/java/com/worlabel/global/response/ErrorResponse.java b/backend/src/main/java/com/worlabel/global/response/ErrorResponse.java index 97c20be..884da53 100644 --- a/backend/src/main/java/com/worlabel/global/response/ErrorResponse.java +++ b/backend/src/main/java/com/worlabel/global/response/ErrorResponse.java @@ -1,6 +1,7 @@ package com.worlabel.global.response; import com.worlabel.global.exception.CustomException; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.validation.Errors; @@ -16,19 +17,22 @@ import java.util.stream.Collectors; * 에러 발생시 리턴 할 에러 응답 객체 */ @Slf4j -public class ErrorResponse extends BaseResponse { +@Getter +public class ErrorResponse extends BaseResponse { - public ErrorResponse(boolean isSuccess, int code, String message, Errors errors) { - super(isSuccess, code, message); + + + public ErrorResponse(boolean isSuccess, int status, int code, String message, Errors errors) { + super(isSuccess, status, code, message); super.errors = parseErrors(errors); } public ErrorResponse(CustomException exception) { - this(false, exception.getErrorCode().getCode(), exception.getMessage(), exception.getErrors()); + this(false, exception.getErrorCode().getStatus().value(), exception.getErrorCode().getCode(), exception.getMessage(), exception.getErrors()); } public ErrorResponse(CustomException exception, String message) { - this(false, 1, message, exception.getErrors()); + this(false, exception.getErrorCode().getStatus().value(), 1, message, exception.getErrors()); } public static ErrorResponse of(CustomException exception) { @@ -40,7 +44,7 @@ public class ErrorResponse extends BaseResponse { } public static ErrorResponse of(Exception exception) { - return new ErrorResponse(false, HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage(), null); + return new ErrorResponse(false, HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage(), null); } /** @@ -59,8 +63,8 @@ public class ErrorResponse extends BaseResponse { // 글로벌 에러 리스트 생성 List globalErrors = errors.getGlobalErrors().stream() .map(e -> new CustomError(null, // 필드 이름이 없으므로 null - e.getCode(), - e.getDefaultMessage(), + e.getCode(), + e.getDefaultMessage(), e.getObjectName() )) .toList(); diff --git a/backend/src/main/java/com/worlabel/global/response/SuccessResponse.java b/backend/src/main/java/com/worlabel/global/response/SuccessResponse.java index 4f8d984..c8cdd15 100644 --- a/backend/src/main/java/com/worlabel/global/response/SuccessResponse.java +++ b/backend/src/main/java/com/worlabel/global/response/SuccessResponse.java @@ -17,7 +17,7 @@ public class SuccessResponse extends BaseResponse { * 성공 응답 객체 생성자 */ public SuccessResponse() { - super(true, HttpStatus.OK.value(), "success"); + super(true, HttpStatus.OK.value(), 200, "success"); } /** @@ -26,7 +26,7 @@ public class SuccessResponse extends BaseResponse { * @param data 성공시 반환하는 데이터 */ public SuccessResponse(T data) { - super(true, 200, "success"); + super(true, 200, 200, "success"); super.data = data; }