From b6628dc7b1c62ccc8da81005a3453f352f3319c2 Mon Sep 17 00:00:00 2001 From: sjungwon03 Date: Fri, 24 Jul 2026 15:57:13 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EA=B3=B5=ED=86=B5=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5(ErrorResponse)=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=EC=99=80=204XX/5XX=20=EA=B8=B0=EB=B3=B8=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 성공 응답의 실제 타입(ApiResponse)과 요청 DTO는 springdoc이 이미 정확히 추론하지만, 예외 핸들러가 던지는 ErrorResponse는 컨트롤러 시그니처에 드러나지 않아 문서에서 빠져 있었다. OpenApiConfig에서 ErrorResponse 스키마를 명시적으로 등록하고 OperationCustomizer로 모든 오퍼레이션에 4XX/5XX 공통 에러 응답을 전역으로 붙인다. Closes #215 --- .../global/config/OpenApiConfig.java | 65 ++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/bop/youthpick/global/config/OpenApiConfig.java b/src/main/java/com/bop/youthpick/global/config/OpenApiConfig.java index e9d78cc..c84ee14 100644 --- a/src/main/java/com/bop/youthpick/global/config/OpenApiConfig.java +++ b/src/main/java/com/bop/youthpick/global/config/OpenApiConfig.java @@ -1,26 +1,85 @@ package com.bop.youthpick.global.config; +import com.bop.youthpick.global.error.ErrorResponse; +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.converter.ResolvedSchema; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springdoc.core.customizers.OperationCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * 성공 응답의 실제 타입은 컨트롤러 반환 타입에서 springdoc이 자동으로 뽑아내지만(예: {@code ApiResponseListPolicyCardResponse}), + * 에러 응답({@code global.error.ErrorResponse})은 예외 핸들러가 던지는 것이라 컨트롤러 시그니처에 드러나지 않는다. 그래서 스키마 등록과 + * 4XX/5XX 기본 응답 노출을 여기서 전역으로 채워준다. + */ @Configuration public class OpenApiConfig { private static final String BEARER_AUTH_SCHEME = "bearerAuth"; + private static final String ERROR_SCHEMA_NAME = "ErrorResponse"; @Bean OpenAPI openAPI() { + Components components = + new Components().addSecuritySchemes(BEARER_AUTH_SCHEME, bearerAuthScheme()); + registerErrorSchema(components); + return new OpenAPI() .info(apiInfo()) .addSecurityItem(new SecurityRequirement().addList(BEARER_AUTH_SCHEME)) - .components( - new Components() - .addSecuritySchemes(BEARER_AUTH_SCHEME, bearerAuthScheme())); + .components(components); + } + + @Bean + OperationCustomizer errorResponseCustomizer() { + Content errorContent = + new Content() + .addMediaType( + "application/json", + new MediaType() + .schema( + new Schema<>() + .$ref( + "#/components/schemas/" + + ERROR_SCHEMA_NAME))); + + return (operation, handlerMethod) -> { + ApiResponses responses = operation.getResponses(); + responses.addApiResponse( + "4XX", + errorApiResponse( + errorContent, + "클라이언트 오류 — 공통 에러 응답 형식(ErrorResponse)으로 내려간다. code 필드로 원인을 구분한다(예: C001" + + " 입력값 오류, A001 인증 실패).")); + responses.addApiResponse( + "5XX", + errorApiResponse( + errorContent, "서버 내부 오류 — 공통 에러 응답 형식(ErrorResponse)으로 내려간다(S001).")); + return operation; + }; + } + + private ApiResponse errorApiResponse(Content content, String description) { + return new ApiResponse().description(description).content(content); + } + + private void registerErrorSchema(Components components) { + ResolvedSchema resolvedSchema = + ModelConverters.getInstance() + .resolveAsResolvedSchema(new AnnotatedType(ErrorResponse.class)); + components.addSchemas(ERROR_SCHEMA_NAME, resolvedSchema.schema); + resolvedSchema.referencedSchemas.forEach(components::addSchemas); } private Info apiInfo() {