diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeApi.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeApi.java index 0c64a876..57bfb9fb 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeApi.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeApi.java @@ -17,6 +17,7 @@ import kr.ac.kookmin.stream.common.CommonErrorCode; import kr.ac.kookmin.stream.member.domain.member.domain.MemberErrorCode; import kr.ac.kookmin.stream.welfare.domain.fee.domain.FeeErrorCode; +import org.springdoc.core.annotations.ParameterObject; /** * 운영진 학생회비 API의 문서 명세. 구현은 {@link AdminFeeController}가 맡는다. @@ -37,7 +38,7 @@ ApiResponse> getRequests( AdminApiUser apiUser, String status, String keyword, - PageParams pageParams + @ParameterObject PageParams pageParams ); /** 납부 확인 요청 처리. 승인/반려로 상태를 바꾸고 납부자 명부에 반영한다. */ diff --git a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/archive/AppArchiveApi.java b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/archive/AppArchiveApi.java index ed25fcdb..e52e225c 100644 --- a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/archive/AppArchiveApi.java +++ b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/archive/AppArchiveApi.java @@ -9,6 +9,7 @@ import kr.ac.kookmin.stream.api.app.event.archive.response.ArchiveListResponse; import kr.ac.kookmin.stream.common.CommonErrorCode; import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveErrorCode; +import org.springdoc.core.annotations.ParameterObject; /** * 학생 앱 행사 아카이브 API의 문서 명세. 구현은 {@link AppArchiveController}가 맡는다. @@ -23,7 +24,7 @@ public interface AppArchiveApi { @Operation(summary = "행사 아카이브 목록 조회", description = "지난 행사 아카이브 목록과 필터용 연도 목록을 함께 조회한다. year를 지정하면 해당 연도만 필터링한다.") @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) - ApiResponse getArchives(ArchiveListParams params); + ApiResponse getArchives(@ParameterObject ArchiveListParams params); /** 아카이브 상세. */ @Operation(summary = "행사 아카이브 상세 조회") diff --git a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventApi.java b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventApi.java index acc455ef..d5215f3f 100644 --- a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventApi.java +++ b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventApi.java @@ -17,6 +17,7 @@ import kr.ac.kookmin.stream.api.app.event.event.response.EventListItemResponse; import kr.ac.kookmin.stream.common.CommonErrorCode; import kr.ac.kookmin.stream.event.domain.event.domain.EventErrorCode; +import org.springdoc.core.annotations.ParameterObject; /** * 학생 앱 행사 API의 문서 명세. 구현은 {@link AppEventController}가 맡는다. @@ -32,7 +33,7 @@ public interface AppEventApi { description = "게시된 행사를 커서 기반으로 조회한다. recruitStatus로 모집 상태를 필터링하고, cursor/size로 다음 페이지를 넘긴다.") @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) @ApiErrorCode(type = EventErrorCode.class, codes = {"EVENT_INVALID_RECRUIT_STATUS", "EVENT_INVALID_CURSOR"}) - ApiResponse> getEvents(EventListParams params); + ApiResponse> getEvents(@ParameterObject EventListParams params); /** 행사 상세. */ @Operation(summary = "행사 상세 조회") @@ -67,7 +68,7 @@ ApiResponse apply( @ApiErrorCode(type = EventErrorCode.class, codes = {"EVENT_INVALID_CURSOR"}) ApiResponse> getMyApplications( AppApiUser apiUser, - EventApplicationListParams params + @ParameterObject EventApplicationListParams params ); /** 내 행사 신청 상세. 행사의 질문 전체에 이 신청의 답변을 붙여 내려준다. */ diff --git a/api/common-api/src/main/java/kr/ac/kookmin/stream/api/common/config/OpenApiConfig.java b/api/common-api/src/main/java/kr/ac/kookmin/stream/api/common/config/OpenApiConfig.java index 55171850..e2f266ca 100644 --- a/api/common-api/src/main/java/kr/ac/kookmin/stream/api/common/config/OpenApiConfig.java +++ b/api/common-api/src/main/java/kr/ac/kookmin/stream/api/common/config/OpenApiConfig.java @@ -5,8 +5,10 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; +import kr.ac.kookmin.stream.api.common.ApiUser; import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCodeCustomizer; import org.springdoc.core.models.GroupedOpenApi; +import org.springdoc.core.utils.SpringDocUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -19,6 +21,11 @@ public class OpenApiConfig { private static final String JWT_SCHEME = "bearerAuth"; + static { + // ArgumentResolver로 주입되는 인증 사용자(ApiUser 구현체)를 springdoc이 쿼리 파라미터로 오인하지 않도록 무시한다. + SpringDocUtils.getConfig().addRequestWrapperToIgnore(ApiUser.class); + } + @Bean public OpenAPI openAPI() { return new OpenAPI() diff --git a/build.gradle.kts b/build.gradle.kts index 065da772..71073933 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,12 @@ subprojects { } } + // 메서드 파라미터 이름을 바이트코드에 보존한다. 미적용 시 @RequestParam 등이 + // Swagger에서 arg0/arg1로 노출된다. (Spring Boot 플러그인 미적용 모듈까지 일괄 적용) + tasks.withType { + options.compilerArgs.add("-parameters") + } + // 모듈별 build.gradle.kts에서 반복 선언하지 않도록 lombok을 공통 의존성으로 적용 // (subprojects {} 클로저 내부 리시버 기준으로는 libs 카탈로그 액세서가 아직 등록되지 않아 rootProject를 통해 참조) dependencies { diff --git a/docs/conventions/coding-style.md b/docs/conventions/coding-style.md index 7b1538c4..5d50ec5a 100644 --- a/docs/conventions/coding-style.md +++ b/docs/conventions/coding-style.md @@ -50,6 +50,7 @@ public record Member( - Request/Response DTO는 `record`(`api:{client}-api`). 각 도메인 패키지 아래 `request`/`response` 하위 패키지로 나눠 둔다(`{basePackage}.api.{client}.{팀}.{도메인}.{request|response}`, `architecture.md` 2-2절). - Request DTO를 Service로 그대로 넘기지 않는다. `toCommand()`로 Command(`core:domain`)로 변환한다. - 접미사로 요청 형태를 구분한다: **`@RequestBody` 바디 DTO는 `Request`**, **`@ModelAttribute`로 바인딩하는 쿼리 파라미터 DTO는 `Params`**(예: `EventListParams`, `ArchiveListParams`). 파일은 둘 다 `request` 하위 패키지에 둔다. 페이지네이션 공용 파라미터는 `common-api`의 `PageParams`를 쓴다. +- `Params` DTO 파라미터에는 `{Client}{Domain}Api` 인터페이스 쪽에 `@ParameterObject`(springdoc)를 붙인다. 없으면 Swagger에서 개별 필드로 펼쳐지지 않고 하나의 `object` 쿼리 파라미터로 뜬다. 컨트롤러에는 바인딩 어노테이션만 남긴다. (예: 인터페이스 `@ParameterObject EventListParams params`, 컨트롤러 `@Valid @ModelAttribute EventListParams params`) ```java // api:admin-api @@ -323,6 +324,10 @@ class MemberServiceImpl implements MemberService { - role 전용 `ApiUser`(`config-and-auth.md`)와 `{Domain}Service`를 주입받는다. 단일 도메인 흐름은 Controller가 직접 처리한다. - 클라이언트 접두사(`Admin`/`App`)로 컨트롤러를 구분하고, 각 클라이언트 모듈의 `{basePackage}.api.{client}.{팀}.{도메인}` 패키지 바로 아래 둔다(DTO는 그 아래 `request`/`response`로 분리, `architecture.md` 2-2절). - Swagger 문서용 어노테이션(`@Tag`/`@Operation`/`@ApiErrorCode`)은 짝이 되는 `{Client}{Domain}Api` 인터페이스에 모으고, 컨트롤러가 이를 `implements`한다. 각 핸들러에 `@Override`를 붙이고 컨트롤러에는 라우팅·바인딩·본문만 남긴다(`error-handling.md` 6절). +- Swagger 쿼리 파라미터 렌더링 규칙: + - `ApiUser` 파라미터는 `OpenApiConfig`가 `SpringDocUtils.addRequestWrapperToIgnore(ApiUser.class)`로 문서에서 숨기므로 별도 처리하지 않는다. + - `@ModelAttribute` `Params` DTO에는 `-Api` 인터페이스의 같은 파라미터에 `@ParameterObject`를 붙인다(2-2절). + - 파라미터 이름은 루트 빌드의 `-parameters` 컴파일 옵션으로 보존되므로 `@RequestParam("name")`처럼 이름을 중복 명시하지 않아도 된다. ```java // api:admin-api — 운영진 회원 등록 (Swagger 명세는 AdminMemberApi, error-handling.md 6절)