diff --git a/src/main/java/com/dduru/gildongmu/common/annotation/ApiErrorResponses.java b/src/main/java/com/dduru/gildongmu/common/annotation/ApiErrorResponses.java new file mode 100644 index 0000000..99dcebf --- /dev/null +++ b/src/main/java/com/dduru/gildongmu/common/annotation/ApiErrorResponses.java @@ -0,0 +1,28 @@ +package com.dduru.gildongmu.common.annotation; + +import com.dduru.gildongmu.common.exception.ErrorCode; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * API 엔드포인트에 자동으로 에러 응답을 추가하기 위한 어노테이션 + * ErrorCode enum을 지정하면 해당 ErrorCode의 HTTP 상태 코드가 자동으로 사용됩니다. + * + * 사용 예시: + *
+ * {@code @ApiErrorResponses({ErrorCode.NICKNAME_ALREADY_TAKEN, ErrorCode.PROFILE_NOT_FOUND})}
+ * ResponseEntity> method();
+ *
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiErrorResponses {
+ /**
+ * 추가할 ErrorCode enum 목록 (예: ErrorCode.NICKNAME_ALREADY_TAKEN, ErrorCode.PROFILE_NOT_FOUND)
+ * 각 ErrorCode의 HTTP 상태 코드가 자동으로 사용됩니다.
+ */
+ ErrorCode[] value();
+}
diff --git a/src/main/java/com/dduru/gildongmu/common/annotation/CommonApiResponses.java b/src/main/java/com/dduru/gildongmu/common/annotation/CommonApiResponses.java
deleted file mode 100644
index df4e592..0000000
--- a/src/main/java/com/dduru/gildongmu/common/annotation/CommonApiResponses.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package com.dduru.gildongmu.common.annotation;
-
-import com.dduru.gildongmu.common.exception.ErrorResponse;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.ExampleObject;
-import io.swagger.v3.oas.annotations.media.Schema;
-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, ElementType.TYPE})
-@Retention(RetentionPolicy.RUNTIME)
-@ApiResponses({
- @ApiResponse(
- responseCode = "400",
- description = "잘못된 요청",
- content = @Content(
- mediaType = "application/json",
- schema = @Schema(implementation = ErrorResponse.class),
- examples = @ExampleObject(
- name = "InvalidInput",
- value = """
- {
- "status": 400,
- "data": {
- "errorCode": "INVALID_INPUT_VALUE",
- "field": null,
- "message": "잘못된 입력 값입니다."
- }
- }
- """
- )
- )
- ),
- @ApiResponse(
- responseCode = "401",
- description = "인증 실패",
- content = @Content(
- mediaType = "application/json",
- schema = @Schema(implementation = ErrorResponse.class),
- examples = @ExampleObject(
- name = "Unauthorized",
- value = """
- {
- "status": 401,
- "data": {
- "errorCode": "UNAUTHORIZED",
- "field": null,
- "message": "인증되지 않은 사용자입니다."
- }
- }
- """
- )
- )
- ),
- @ApiResponse(
- responseCode = "404",
- description = "리소스를 찾을 수 없음",
- content = @Content(
- mediaType = "application/json",
- schema = @Schema(implementation = ErrorResponse.class),
- examples = @ExampleObject(
- name = "NotFound",
- value = """
- {
- "status": 404,
- "data": {
- "errorCode": "NOT_FOUND",
- "field": null,
- "message": "요청한 리소스를 찾을 수 없습니다."
- }
- }
- """
- )
- )
- ),
- @ApiResponse(
- responseCode = "500",
- description = "서버 오류",
- content = @Content(
- mediaType = "application/json",
- schema = @Schema(implementation = ErrorResponse.class),
- examples = @ExampleObject(
- name = "InternalServerError",
- value = """
- {
- "status": 500,
- "data": {
- "errorCode": "INTERNAL_SERVER_ERROR",
- "field": null,
- "message": "서버 오류가 발생했습니다."
- }
- }
- """
- )
- )
- )
-})
-public @interface CommonApiResponses {
-}
diff --git a/src/main/java/com/dduru/gildongmu/common/exception/ApiErrorResponseDocsCustomizer.java b/src/main/java/com/dduru/gildongmu/common/exception/ApiErrorResponseDocsCustomizer.java
new file mode 100644
index 0000000..5e7e8bf
--- /dev/null
+++ b/src/main/java/com/dduru/gildongmu/common/exception/ApiErrorResponseDocsCustomizer.java
@@ -0,0 +1,175 @@
+package com.dduru.gildongmu.common.exception;
+
+import com.dduru.gildongmu.common.annotation.ApiErrorResponses;
+import io.swagger.v3.oas.models.Components;
+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.media.Schema;
+import io.swagger.v3.oas.models.responses.ApiResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springdoc.core.customizers.OperationCustomizer;
+import org.springdoc.core.customizers.OpenApiCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * OpenAPI 에러 응답 자동 추가 커스터마이저
+ *
+ * @ApiErrorResponses 어노테이션에 ErrorCode enum을 지정하면,
+ * 해당 ErrorCode의 HTTP 상태 코드와 메시지가 자동으로 사용됩니다.
+ */
+@Slf4j
+@Configuration
+public class ApiErrorResponseDocsCustomizer {
+
+ /**
+ * ErrorResponse 스키마를 Components에 등록
+ */
+ @Bean
+ public OpenApiCustomizer errorResponseSchemaCustomizer() {
+ return openApi -> {
+ Components components = openApi.getComponents();
+ if (components == null) {
+ components = new Components();
+ openApi.setComponents(components);
+ }
+
+ // ErrorResponse 스키마 정의
+ Schema> errorResponseSchema = new Schema<>()
+ .type("object")
+ .addProperty("status", new Schema<>().type("integer").format("int32").example(400))
+ .addProperty("data", new Schema<>()
+ .type("object")
+ .addProperty("errorCode", new Schema<>().type("string").example("INVALID_INPUT_VALUE"))
+ .addProperty("field", new Schema<>().type("string").nullable(true))
+ .addProperty("message", new Schema<>().type("string").example("잘못된 입력 값입니다.")));
+
+ components.addSchemas("ErrorResponse", errorResponseSchema);
+ };
+ }
+
+ /**
+ * @ApiErrorResponses 어노테이션에 지정된 ErrorCode enum을 기반으로 에러 응답 자동 추가
+ */
+ @Bean
+ public OperationCustomizer operationCustomizer() {
+ return (operation, handlerMethod) -> {
+ ErrorCode[] errorCodes = new ErrorCode[0];
+
+ Method method = handlerMethod.getMethod();
+ Class> declaringClass = method.getDeclaringClass();
+ String methodName = method.getName();
+
+ // 모든 인터페이스에서 메서드를 찾아 어노테이션 확인
+ for (Class> iface : declaringClass.getInterfaces()) {
+ // 인터페이스의 모든 메서드 검색 (파라미터 타입이 다를 수 있으므로)
+ for (Method interfaceMethod : iface.getMethods()) {
+ // 메서드 이름이 같고 어노테이션이 있으면 사용
+ if (interfaceMethod.getName().equals(methodName)
+ && interfaceMethod.isAnnotationPresent(ApiErrorResponses.class)) {
+ errorCodes = interfaceMethod.getAnnotation(ApiErrorResponses.class).value();
+ break;
+ }
+ }
+ if (errorCodes.length > 0) {
+ break;
+ }
+
+ // 인터페이스 레벨 어노테이션 확인
+ if (iface.isAnnotationPresent(ApiErrorResponses.class)) {
+ errorCodes = iface.getAnnotation(ApiErrorResponses.class).value();
+ break;
+ }
+ }
+
+ // 클래스 레벨 어노테이션 확인
+ if (errorCodes.length == 0 && declaringClass.isAnnotationPresent(ApiErrorResponses.class)) {
+ errorCodes = declaringClass.getAnnotation(ApiErrorResponses.class).value();
+ }
+
+ // 메서드 레벨 어노테이션 확인 (메서드 레벨이 우선)
+ if (method.isAnnotationPresent(ApiErrorResponses.class)) {
+ errorCodes = method.getAnnotation(ApiErrorResponses.class).value();
+ }
+
+ if (errorCodes.length > 0) {
+ addErrorResponses(operation, errorCodes);
+ }
+
+ return operation;
+ };
+ }
+
+ /**
+ * ErrorCode enum 배열을 기반으로 에러 응답 추가
+ * 각 ErrorCode에 대해 해당하는 HTTP 상태 코드의 응답을 추가하고, 예시를 포함합니다.
+ * 같은 HTTP 상태 코드를 가진 여러 ErrorCode가 있으면 모두 examples에 추가합니다.
+ */
+ private void addErrorResponses(io.swagger.v3.oas.models.Operation operation, ErrorCode[] errorCodes) {
+ // ErrorResponse 스키마 참조
+ Schema> errorResponseSchema = new Schema<>()
+ .$ref("#/components/schemas/ErrorResponse");
+
+ // HTTP 상태 코드별로 ErrorCode들을 그룹화
+ Map