From 5139aa13a4947ae518417c35954cc11d10e964f6 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:06:10 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20Swagger=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=EA=B0=80=20arg0=EB=A1=9C?= =?UTF-8?q?=20=EB=85=B8=EC=B6=9C=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stream/api/admin/welfare/fee/AdminFeeController.java | 3 ++- .../stream/api/app/event/archive/AppArchiveController.java | 3 ++- .../stream/api/app/event/event/AppEventController.java | 5 +++-- .../ac/kookmin/stream/api/common/config/OpenApiConfig.java | 7 +++++++ build.gradle.kts | 6 ++++++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeController.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeController.java index 735ea965..bc775cd4 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeController.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeController.java @@ -24,6 +24,7 @@ import kr.ac.kookmin.stream.welfare.domain.fee.domain.StudentTransferStatus; import kr.ac.kookmin.stream.welfare.domain.fee.domain.TransferStatus; import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; @@ -50,7 +51,7 @@ public ApiResponse> getRequests( AdminApiUser apiUser, @RequestParam(required = false) String status, @RequestParam(required = false) String keyword, - @Valid @ModelAttribute PageParams pageParams + @Valid @ParameterObject @ModelAttribute PageParams pageParams ) { TransferStatus transferStatus = TransferStatus.from(status); PageResult result = adminFeeSearchUseCase.search(transferStatus, keyword, pageParams.toOffset()); diff --git a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/archive/AppArchiveController.java b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/archive/AppArchiveController.java index 6c635e65..0b154977 100644 --- a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/archive/AppArchiveController.java +++ b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/archive/AppArchiveController.java @@ -7,6 +7,7 @@ import kr.ac.kookmin.stream.api.app.event.archive.response.ArchiveListResponse; import kr.ac.kookmin.stream.event.domain.archive.service.ArchiveService; import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; @@ -25,7 +26,7 @@ public class AppArchiveController implements AppArchiveApi { */ @Override @GetMapping - public ApiResponse getArchives(@Valid @ModelAttribute ArchiveListParams params) { + public ApiResponse getArchives(@Valid @ParameterObject @ModelAttribute ArchiveListParams params) { return ApiResponse.success(ArchiveListResponse.of( archiveService.getArchives(params.year()), archiveService.getYears() diff --git a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventController.java b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventController.java index a958acf0..20bd17b4 100644 --- a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventController.java +++ b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventController.java @@ -19,6 +19,7 @@ import kr.ac.kookmin.stream.event.domain.event.service.EventApplicationService; import kr.ac.kookmin.stream.event.domain.event.service.EventService; import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; @@ -47,7 +48,7 @@ public class AppEventController implements AppEventApi { @Override @GetMapping public ApiResponse> getEvents( - @Valid @ModelAttribute EventListParams params + @Valid @ParameterObject @ModelAttribute EventListParams params ) { CursorSliceResult result = eventService.getPublishedEvents( params.toRecruitStatus(), params.toCursor(), params.sizeOrDefault()); @@ -58,7 +59,7 @@ public ApiResponse> getEvents( @GetMapping("/applications") public ApiResponse> getMyApplications( AppApiUser apiUser, - @Valid @ModelAttribute EventApplicationListParams params + @Valid @ParameterObject @ModelAttribute EventApplicationListParams params ) { CursorSliceResult result = eventApplicationService.getMyApplications( apiUser.userId(), params.toCursor(), params.sizeOrDefault()); 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 { From 4c074b18d2b18275d593d471a26d8f24dea7010b Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:06:10 +0900 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20Swagger=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=EC=BB=A8=EB=B2=A4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conventions/coding-style.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/conventions/coding-style.md b/docs/conventions/coding-style.md index 7b1538c4..ae9a2f0d 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를 바인딩할 때는 `@ParameterObject`(springdoc)를 `@ModelAttribute`와 함께 붙인다. 없으면 Swagger에서 개별 필드로 펼쳐지지 않고 하나의 `object` 쿼리 파라미터로 뜬다. (예: `@Valid @ParameterObject @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에는 `@ParameterObject`를 함께 붙인다(2-2절). + - 파라미터 이름은 루트 빌드의 `-parameters` 컴파일 옵션으로 보존되므로 `@RequestParam("name")`처럼 이름을 중복 명시하지 않아도 된다. ```java // api:admin-api — 운영진 회원 등록 (Swagger 명세는 AdminMemberApi, error-handling.md 6절) From 5fdfe5f323944539c12a5cb75cdf43b8b42f4f57 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:48:12 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20@ParameterObject=EB=A5=BC=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=EC=97=90=EC=84=9C=20Api=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ac/kookmin/stream/api/admin/welfare/fee/AdminFeeApi.java | 3 ++- .../stream/api/admin/welfare/fee/AdminFeeController.java | 3 +-- .../kookmin/stream/api/app/event/archive/AppArchiveApi.java | 3 ++- .../stream/api/app/event/archive/AppArchiveController.java | 3 +-- .../ac/kookmin/stream/api/app/event/event/AppEventApi.java | 5 +++-- .../stream/api/app/event/event/AppEventController.java | 5 ++--- 6 files changed, 11 insertions(+), 11 deletions(-) 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/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeController.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeController.java index bc775cd4..735ea965 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeController.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/fee/AdminFeeController.java @@ -24,7 +24,6 @@ import kr.ac.kookmin.stream.welfare.domain.fee.domain.StudentTransferStatus; import kr.ac.kookmin.stream.welfare.domain.fee.domain.TransferStatus; import lombok.RequiredArgsConstructor; -import org.springdoc.core.annotations.ParameterObject; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; @@ -51,7 +50,7 @@ public ApiResponse> getRequests( AdminApiUser apiUser, @RequestParam(required = false) String status, @RequestParam(required = false) String keyword, - @Valid @ParameterObject @ModelAttribute PageParams pageParams + @Valid @ModelAttribute PageParams pageParams ) { TransferStatus transferStatus = TransferStatus.from(status); PageResult result = adminFeeSearchUseCase.search(transferStatus, keyword, pageParams.toOffset()); 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/archive/AppArchiveController.java b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/archive/AppArchiveController.java index 0b154977..6c635e65 100644 --- a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/archive/AppArchiveController.java +++ b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/archive/AppArchiveController.java @@ -7,7 +7,6 @@ import kr.ac.kookmin.stream.api.app.event.archive.response.ArchiveListResponse; import kr.ac.kookmin.stream.event.domain.archive.service.ArchiveService; import lombok.RequiredArgsConstructor; -import org.springdoc.core.annotations.ParameterObject; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; @@ -26,7 +25,7 @@ public class AppArchiveController implements AppArchiveApi { */ @Override @GetMapping - public ApiResponse getArchives(@Valid @ParameterObject @ModelAttribute ArchiveListParams params) { + public ApiResponse getArchives(@Valid @ModelAttribute ArchiveListParams params) { return ApiResponse.success(ArchiveListResponse.of( archiveService.getArchives(params.year()), archiveService.getYears() 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/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventController.java b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventController.java index 20bd17b4..a958acf0 100644 --- a/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventController.java +++ b/api/app-api/src/main/java/kr/ac/kookmin/stream/api/app/event/event/AppEventController.java @@ -19,7 +19,6 @@ import kr.ac.kookmin.stream.event.domain.event.service.EventApplicationService; import kr.ac.kookmin.stream.event.domain.event.service.EventService; import lombok.RequiredArgsConstructor; -import org.springdoc.core.annotations.ParameterObject; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; @@ -48,7 +47,7 @@ public class AppEventController implements AppEventApi { @Override @GetMapping public ApiResponse> getEvents( - @Valid @ParameterObject @ModelAttribute EventListParams params + @Valid @ModelAttribute EventListParams params ) { CursorSliceResult result = eventService.getPublishedEvents( params.toRecruitStatus(), params.toCursor(), params.sizeOrDefault()); @@ -59,7 +58,7 @@ public ApiResponse> getEvents( @GetMapping("/applications") public ApiResponse> getMyApplications( AppApiUser apiUser, - @Valid @ParameterObject @ModelAttribute EventApplicationListParams params + @Valid @ModelAttribute EventApplicationListParams params ) { CursorSliceResult result = eventApplicationService.getMyApplications( apiUser.userId(), params.toCursor(), params.sizeOrDefault()); From 01ceae5b4061d8d8e1cd80fee4f99485e1fa2c5c Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:48:12 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20@ParameterObject=20=EC=84=A0?= =?UTF-8?q?=EC=96=B8=20=EC=9C=84=EC=B9=98=EB=A5=BC=20Api=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conventions/coding-style.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conventions/coding-style.md b/docs/conventions/coding-style.md index ae9a2f0d..5d50ec5a 100644 --- a/docs/conventions/coding-style.md +++ b/docs/conventions/coding-style.md @@ -50,7 +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를 바인딩할 때는 `@ParameterObject`(springdoc)를 `@ModelAttribute`와 함께 붙인다. 없으면 Swagger에서 개별 필드로 펼쳐지지 않고 하나의 `object` 쿼리 파라미터로 뜬다. (예: `@Valid @ParameterObject @ModelAttribute EventListParams params`) +- `Params` DTO 파라미터에는 `{Client}{Domain}Api` 인터페이스 쪽에 `@ParameterObject`(springdoc)를 붙인다. 없으면 Swagger에서 개별 필드로 펼쳐지지 않고 하나의 `object` 쿼리 파라미터로 뜬다. 컨트롤러에는 바인딩 어노테이션만 남긴다. (예: 인터페이스 `@ParameterObject EventListParams params`, 컨트롤러 `@Valid @ModelAttribute EventListParams params`) ```java // api:admin-api @@ -326,7 +326,7 @@ class MemberServiceImpl implements MemberService { - Swagger 문서용 어노테이션(`@Tag`/`@Operation`/`@ApiErrorCode`)은 짝이 되는 `{Client}{Domain}Api` 인터페이스에 모으고, 컨트롤러가 이를 `implements`한다. 각 핸들러에 `@Override`를 붙이고 컨트롤러에는 라우팅·바인딩·본문만 남긴다(`error-handling.md` 6절). - Swagger 쿼리 파라미터 렌더링 규칙: - `ApiUser` 파라미터는 `OpenApiConfig`가 `SpringDocUtils.addRequestWrapperToIgnore(ApiUser.class)`로 문서에서 숨기므로 별도 처리하지 않는다. - - `@ModelAttribute` `Params` DTO에는 `@ParameterObject`를 함께 붙인다(2-2절). + - `@ModelAttribute` `Params` DTO에는 `-Api` 인터페이스의 같은 파라미터에 `@ParameterObject`를 붙인다(2-2절). - 파라미터 이름은 루트 빌드의 `-parameters` 컴파일 옵션으로 보존되므로 `@RequestParam("name")`처럼 이름을 중복 명시하지 않아도 된다. ```java