Skip to content

SpringDoc + Stoplight Elements API 문서 자동화 도입#62

Merged
sevineleven merged 5 commits intomainfrom
chore/api-docs
Apr 26, 2026
Merged

SpringDoc + Stoplight Elements API 문서 자동화 도입#62
sevineleven merged 5 commits intomainfrom
chore/api-docs

Conversation

@sevineleven
Copy link
Copy Markdown
Collaborator

@sevineleven sevineleven commented Apr 26, 2026

Situation

  • API 문서가 없어 프론트엔드 팀과의 명세 공유 수단이 필요한 상황
  • Swagger UI, Stoplight Platform(SaaS), Stoplight Elements 중 어떤 방식을 선택할지 검토
  • 직접 OpenAPI YAML을 작성하는 방식은 관리 비용이 크다는 점에서 배제

Task

  • 코드 변경 시 API 문서가 자동으로 반영되는 문서 자동화 구축
  • 렌더러는 Swagger UI보다 UI/UX가 우수한 Stoplight Elements 채택

Action

  • springdoc-openapi-starter-webmvc-api:3.0.3 의존성 추가
    • -webmvc-ui(Swagger UI 번들) 대신 -webmvc-api(API 엔드포인트만) 사용 — Stoplight Elements가 UI 역할을 하므로 Swagger UI 불필요
  • OpenApiConfig: API 제목, 설명, 연락처 등 메타데이터 설정
  • GuestController, WishlistController@Tag, @Operation, @ApiResponse 어노테이션은 @m-a-kingchore/swagger-openapi 브랜치 작업을 cherry-pick
  • /docs/index.html: Stoplight Elements CDN 로드 후 /v3/api-docs 를 읽어 렌더링

Result

  • 컨트롤러 코드 배포 시 /docs/index.html 문서 자동 반영
  • 어노테이션 없어도 @RestController 만 있으면 기본 문서 생성, 어노테이션으로 설명 보강 가능

연관 이슈

m-a-king and others added 3 commits April 27, 2026 00:53
- 컨트롤러에 @Tag/@Operation/@apiresponse 부착 및 성공·에러 예시 JSON 명시
- 위시리스트 등록 API 의 400/409 응답을 ProblemDetail 스키마로 문서화
- DTO 필드별 @Schema(description, example, nullable, format) 부착으로 클라이언트 가이드 강화
- Swagger UI 없이 /v3/api-docs 엔드포인트만 제공하는 webmvc-api 모듈 사용
- OpenApiConfig: API 제목, 설명, 연락처, 라이선스, 서버 설정
- application.yml: springdoc api-docs 경로 및 기본 미디어 타입 설정
- /v3/api-docs 를 읽어 Stoplight Elements UI로 렌더링
- sidebar 레이아웃, hash 라우터 적용
@github-actions
Copy link
Copy Markdown

No description provided.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Warning

Rate limit exceeded

@sevineleven has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 29 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 48 minutes and 29 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: da1bb5f1-c2f1-49f7-bf9e-2f9f2072524f

📥 Commits

Reviewing files that changed from the base of the PR and between f83df30 and 01c1cf1.

📒 Files selected for processing (2)
  • src/main/kotlin/com/depromeet/team3/common/config/OpenApiConfig.kt
  • src/main/resources/static/docs/index.html

Walkthrough

SpringDoc OpenAPI 라이브러리를 Gradle 의존성에 추가하고, OpenAPI 설정 클래스를 작성하여 /v3/api-docs 엔드포인트를 통한 API 문서 자동 생성을 활성화했습니다. Guest 및 Wishlist 컨트롤러와 관련 DTO에 OpenAPI 어노테이션을 추가하여 메타데이터를 정의하고, Stoplight Elements를 활용한 HTML 문서 페이지를 제공합니다.

Changes

Cohort / File(s) Summary
의존성 및 기본 설정
build.gradle.kts, src/main/resources/application.yml, src/main/kotlin/com/depromeet/team3/common/config/OpenApiConfig.kt
SpringDoc OpenAPI 라이브러리 추가, API 문서 엔드포인트(/v3/api-docs) 및 미디어 타입 설정, OpenAPI 빈 구성으로 애플리케이션 정보 및 서버 메타데이터 정의.
Guest API 문서화
src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt, src/main/kotlin/com/depromeet/team3/guest/controller/dto/GuestResponse.kt
@Tag, @Operation, @ApiResponse, @Schema 어노테이션으로 게스트 ID 발급 엔드포인트 및 응답 DTO 문서화. UUID 형식과 예제 값 명시.
Wishlist API 문서화
src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt, src/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterRequest.kt, src/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterResponse.kt
@Tag, @Operation, @ApiResponses, @Schema 어노테이션으로 위시리스트 등록 엔드포인트 및 요청/응답 DTO 문서화. 201, 400, 409 응답 예제 포함.
API 문서 페이지
src/main/resources/static/docs/index.html
Stoplight Elements Web Components를 활용한 OpenAPI 문서 렌더링 페이지. /v3/api-docs에서 가져온 명세를 시각화하여 제공.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Assessment against linked issues

Objective Addressed Explanation
SpringDoc OpenAPI 의존성 추가 → /v3/api-docs 자동 생성 [#59]
Stoplight Elements HTML 페이지 서빙 → /docs/index.html [#59]
컨트롤러 배포 시 문서 자동 반영 [#59]

리뷰 의견

✨ 좋은 점

OpenAPI 자동화 도입이 깔끔하게 진행되었습니다! 의존성 추가부터 설정, 어노테이션 적용까지 일관성 있게 구성되었네요.

💡 제안 및 개선 사항

  1. OpenAPI 설정의 버전 관리 (OpenApiConfig.kt)
    현재 버전을 v1로 하드코딩했는데, 프로젝트의 build.gradle.kts의 버전 정보(예: version = "0.0.1-SNAPSHOT")를 활용하면 버전 동기화가 자동으로 유지됩니다.

    // 현재
    .version("v1")
    
    // 개선 제안
    .version(project.version.toString()) // build.gradle.kts의 버전 참조
  2. 외부 문서 링크 추가 검토
    Stoplight Elements 설정에 외부 문서나 인증 정보(OAuth2, API Key 등) 관련 참조 링크가 추가될 경우를 대비하세요. 향후 확장이 용이합니다.

  3. 다른 컨트롤러의 문서화
    현재 Guest와 Wishlist 컨트롤러만 어노테이션이 추가되었습니다. 프로젝트의 다른 컨트롤러들도 점진적으로 같은 방식으로 문서화하는 것을 권장합니다.

  4. 테스트 엔드포인트 접근성
    /docs/index.html 외에 Swagger UI가 필요할 경우, springdoc-openapi-starter-webmvc-ui 의존성 추가를 고려해 보세요. (현재는 API 문서만 제공하고 있습니다)

📚 참고 자료

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목이 Conventional Commits 형식을 따르지 않으며, 명령형 동사가 없고 type 접두사가 없습니다. 제목을 'chore: SpringDoc + Stoplight Elements API 문서 자동화 도입' 형식으로 변경하여 Conventional Commits 규칙을 준수하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/api-docs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt (1)

28-62: @Operation/@ApiResponses 구성 자체는 깔끔합니다 👍

성공 예시(JSON)에 실제 응답 모양 그대로 넣어주신 덕분에 Stoplight에서 그대로 "Try it" 가능해 보여요. 이런 식으로 컨트롤러에 응답 컨트랙트가 못 박혀 있으면 프론트와의 합의 비용이 확 줄어듭니다.

다만 이 long-form examples가 컨트롤러마다 반복되면 금세 노이즈가 됩니다. 컨트롤러 4~5개 넘어가기 전에 공통 에러 응답 예시(Bad Request, Conflict 등)를 OpenApiConfig에서 Components.examples로 등록해놓고 @ApiResponse(content = @content(examples = @ExampleObject(ref = "#/components/examples/InvalidInput"))) 형태로 참조하는 패턴을 권장합니다. 지금 PR에서 당장 바꿀 필요는 없어요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt`
around lines 28 - 62, The controller currently embeds long ExampleObject JSON
directly in WishlistController's `@Operation/`@ApiResponses which will create
noise when repeated; extract common response examples (e.g., Bad Request,
Conflict, and the success payload example if reused) into OpenApiConfig's
Components.examples and update the annotations in WishlistController to
reference them via ExampleObject(ref = "#/components/examples/YourExampleName")
inside `@ApiResponse`(content = `@Content`(...)) so the controller keeps a compact
annotation while reusing shared examples across controllers.
src/main/resources/application.yml (1)

31-34: 운영 환경에서 /v3/api-docs·/docs/index.html 노출 정책을 정해두면 좋겠어요.

지금 설정대로면 모든 프로파일(특히 prod)에서 /v3/api-docs가 인증 없이 그대로 열립니다. 아직 인증/시큐리티가 들어가기 전이라 당장 코드 수정이 필요한 건 아니지만, 내부 API 스펙(엔드포인트, 파라미터 시맨틱, 에러 카테고리 등)은 공격자에게 공짜 공격면이 됩니다. 한 번 외부에 인덱싱되면 회수도 어려워요.

운영 적용 전에 아래 중 한 가지를 정해두시는 걸 권장합니다.

  • prod 프로파일에서 비활성화: springdoc.api-docs.enabled: false, swagger-ui.enabled: false (또는 정적 /docs/**도 차단)
  • 또는 SecurityFilterChain에서 /v3/api-docs/**, /docs/**을 사내망/베이직 인증 뒤로 격리

참고: SpringDoc – Disabling the springdoc-openapi endpoints.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/application.yml` around lines 31 - 34, The springdoc
endpoints are currently exposed unconditionally via the springdoc.api-docs
config; for prod disable or protect them: add a prod-specific setting to
application.yml setting springdoc.api-docs.enabled: false and
swagger-ui.enabled: false (or block static /docs/**) to disable exposure in
production, or update your SecurityFilterChain (the security configuration
bean/method that registers HTTP security rules) to require internal-network or
basic auth for the "/v3/api-docs/**" and "/docs/**" patterns; implement one of
these two options and ensure the change targets the springdoc/api-docs config
and the SecurityFilterChain route matchers so prod does not serve API docs
publicly.
src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt (1)

28-44: 문서는 일관됩니다. 한 가지만 짚고 갈게요.

issueGuestId()가 새 식별자를 발급(=리소스 생성)하는데 @ResponseStatus가 없어 기본 200 OK로 응답합니다. 문서의 responseCode = "200"은 실제 동작과 정확히 맞아 떨어지므로 본 PR 범위 내에서는 OK 입니다 ✅

다만 시맨틱적으로는 201 Created가 더 정확합니다. 현재 클라이언트가 200을 가정하고 짠 코드가 없다면, 별도 PR에서 @ResponseStatus(HttpStatus.CREATED) + 문서도 201로 맞추는 걸 권장드려요. 지금 PR에서 같이 바꾸진 마시고요(범위가 커집니다).

참고: RFC 9110 §15.3.2 – 201 Created

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt`
around lines 28 - 44, The endpoint GuestController.issueGuestId issues a new
resource but currently returns 200; to make it semantically correct, add
`@ResponseStatus`(HttpStatus.CREATED) to the issueGuestId method and update the
`@ApiResponse`(responseCode = "200", ...) to responseCode = "201" (adjust the
description/example as needed) so the OpenAPI docs and runtime behavior both
reflect 201 Created; locate these changes on the issueGuestId method and the
surrounding ApiResponse/Content schema declarations referencing GuestResponse.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterRequest.kt`:
- Around line 6-22: The request body DTO lacks Bean Validation and the
controller isn't triggering validation: update the WishlistRegisterRequest data
class to add validation annotations on the url field (e.g., `@NotBlank` and a
`@Pattern` or URL-style constraint) and any necessary constraints on guestId, and
then add `@Valid` to the parameter in WishlistController.register(...) so Spring
will run validation before reaching ProductLink.parse(); ensure you use the
field-targeted annotations (e.g., `@field`:NotBlank) on url inside
WishlistRegisterRequest and keep the existing Schema metadata.

In
`@src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt`:
- Around line 87-110: Add a new duplicate/conflict category to the ErrorCategory
enum (e.g., DUPLICATE_RESOURCE or CONFLICT) and update the
WishAlreadyExistsException to use that category instead of
ErrorCategory.INVALID_INPUT; then update the OpenAPI example for the 409
response so its "category" and "detail" match the new ErrorCategory text and the
actual message produced by WishAlreadyExistsException. Ensure you modify the
ErrorCategory enum declaration and the WishAlreadyExistsException class
(overridden httpStatus/category properties) and the ApiResponse ExampleObject
for the 409 case so code, runtime response, and docs are consistent.

In `@src/main/resources/static/docs/index.html`:
- Around line 7-8: Pin the Stoplight Elements CDN to a hash-compatible release
and add SRI/crossorigin: replace the current unversioned script and stylesheet
tags with explicit v8.5.2 URLs (so router="hash" continues to work), add the
corresponding integrity attribute values and set crossorigin="anonymous" for
both <script> and <link>, and consider a local/static fallback if CDN fails; if
you instead want to use v9+, change router="hash" to router="history" and ensure
server-side routing is configured before switching.

---

Nitpick comments:
In `@src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt`:
- Around line 28-44: The endpoint GuestController.issueGuestId issues a new
resource but currently returns 200; to make it semantically correct, add
`@ResponseStatus`(HttpStatus.CREATED) to the issueGuestId method and update the
`@ApiResponse`(responseCode = "200", ...) to responseCode = "201" (adjust the
description/example as needed) so the OpenAPI docs and runtime behavior both
reflect 201 Created; locate these changes on the issueGuestId method and the
surrounding ApiResponse/Content schema declarations referencing GuestResponse.

In
`@src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt`:
- Around line 28-62: The controller currently embeds long ExampleObject JSON
directly in WishlistController's `@Operation/`@ApiResponses which will create
noise when repeated; extract common response examples (e.g., Bad Request,
Conflict, and the success payload example if reused) into OpenApiConfig's
Components.examples and update the annotations in WishlistController to
reference them via ExampleObject(ref = "#/components/examples/YourExampleName")
inside `@ApiResponse`(content = `@Content`(...)) so the controller keeps a compact
annotation while reusing shared examples across controllers.

In `@src/main/resources/application.yml`:
- Around line 31-34: The springdoc endpoints are currently exposed
unconditionally via the springdoc.api-docs config; for prod disable or protect
them: add a prod-specific setting to application.yml setting
springdoc.api-docs.enabled: false and swagger-ui.enabled: false (or block static
/docs/**) to disable exposure in production, or update your SecurityFilterChain
(the security configuration bean/method that registers HTTP security rules) to
require internal-network or basic auth for the "/v3/api-docs/**" and "/docs/**"
patterns; implement one of these two options and ensure the change targets the
springdoc/api-docs config and the SecurityFilterChain route matchers so prod
does not serve API docs publicly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 58539509-097c-4ecb-83b1-6e7ebb5bbff9

📥 Commits

Reviewing files that changed from the base of the PR and between 4f3777d and f83df30.

📒 Files selected for processing (9)
  • build.gradle.kts
  • src/main/kotlin/com/depromeet/team3/common/config/OpenApiConfig.kt
  • src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt
  • src/main/kotlin/com/depromeet/team3/guest/controller/dto/GuestResponse.kt
  • src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt
  • src/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterRequest.kt
  • src/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterResponse.kt
  • src/main/resources/application.yml
  • src/main/resources/static/docs/index.html

Comment on lines +6 to 22
@Schema(description = "위시리스트 등록 요청")
data class WishlistRegisterRequest(
@field:Schema(
description = "등록할 상품 페이지 URL",
example = "https://www.example-shop.com/products/12345",
requiredMode = Schema.RequiredMode.REQUIRED,
)
val url: String,

@field:Schema(
description = "게스트 식별자 (게스트 발급 API로 받은 UUID)",
example = "8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f",
format = "uuid",
requiredMode = Schema.RequiredMode.REQUIRED,
)
val guestId: UUID,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 컨트롤러에서 `@Valid` 사용 여부 + 기존 검증 어노테이션 사용 패턴 확인
rg -nP -C2 '@Valid\b|jakarta\.validation' --type=kotlin

Repository: depromeet/18th-team3-server

Length of output: 53


🏁 Script executed:

# WishlistRegisterRequest.kt 파일 위치와 내용 확인
fd -t f WishlistRegisterRequest.kt

Repository: depromeet/18th-team3-server

Length of output: 158


🏁 Script executed:

# Wishlist 관련 컨트롤러 찾기
fd -t f -i wishlist.*controller.kt

Repository: depromeet/18th-team3-server

Length of output: 149


🏁 Script executed:

# 프로젝트 전체 검증 패턴 확인 (다른 DTO들의 예시)
rg -t kotlin '@field:(NotBlank|NotNull|Pattern|Valid)' -c | head -20

Repository: depromeet/18th-team3-server

Length of output: 53


🏁 Script executed:

cat -n src/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterRequest.kt

Repository: depromeet/18th-team3-server

Length of output: 870


🏁 Script executed:

cat -n src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt

Repository: depromeet/18th-team3-server

Length of output: 5996


🏁 Script executed:

# 프로젝트의 일반적인 요청 DTO 패턴 확인 (다른 DTO 예시)
fd -t f -path '*/controller/dto/*.kt' | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -30 {}'

Repository: depromeet/18th-team3-server

Length of output: 240


🏁 Script executed:

# WishlistService.register 메서드에서 실제 검증 로직 확인
fd -t f WishlistService.kt | xargs -I {} sh -c 'echo "=== {} ===" && cat -n {}'

Repository: depromeet/18th-team3-server

Length of output: 2060


🏁 Script executed:

# 다른 컨트롤러 DTO 패턴 비교 (2-3개 샘플)
find src/main/kotlin -path '*/controller/dto/*.kt' -type f | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && cat -n {}'

Repository: depromeet/18th-team3-server

Length of output: 3423


컨트롤러에 @Valid 붙이고, DTO에 검증 어노테이션을 추가해야 합니다.

현재 WishlistController.register()에서 @RequestBody 아래에 @Valid가 없어서, 컨트롤러 레벨에서 자동 검증이 작동하지 않습니다. Swagger 문서상으로는 requiredMode = REQUIRED + format = "uuid"라고 선언되어 있는데, 실제로는:

  • url이 빈 문자열이면 그대로 서비스까지 흘러가 ProductLink.parse()에서 예외 발생 (컨트롤러 검증층 부재)
  • 문서와 실제 동작의 불일치 — 프론트가 Swagger를 믿고 호출했는데, 문서에 없는 500이나 예상 밖의 에러가 떨어질 수 있음

Spring Boot 표준 패턴API 안정성을 고려하면:

import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern

data class WishlistRegisterRequest(
    `@field`:Schema(/* ... */)
    `@field`:NotBlank(message = "URL은 필수입니다.")
    `@field`:Pattern(
        regexp = "^https?://.+",
        message = "지원하지 않는 URL 형식입니다."
    )
    val url: String,

    `@field`:Schema(/* ... */)
    val guestId: UUID,
)

그리고 컨트롤러에서:

fun register(
    `@Valid`
    `@RequestBody`
    request: WishlistRegisterRequest
): WishlistRegisterResponse

프로젝트 전체에서 Bean Validation을 쓰지 않는 이유가 있다면 (서비스 레이어 검증 정책 등), 그 전략을 문서화하고 일관되게 유지하세요. 하지만 현재 상태는 Swagger 문서와 실제 구현이 어긋나 있으므로 개선이 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterRequest.kt`
around lines 6 - 22, The request body DTO lacks Bean Validation and the
controller isn't triggering validation: update the WishlistRegisterRequest data
class to add validation annotations on the url field (e.g., `@NotBlank` and a
`@Pattern` or URL-style constraint) and any necessary constraints on guestId, and
then add `@Valid` to the parameter in WishlistController.register(...) so Spring
will run validation before reaching ProductLink.parse(); ensure you use the
field-targeted annotations (e.g., `@field`:NotBlank) on url inside
WishlistRegisterRequest and keep the existing Schema metadata.

Comment on lines +87 to +110
ApiResponse(
responseCode = "409",
description = "이미 위시리스트에 등록된 상품",
content = [
Content(
mediaType = MediaType.APPLICATION_PROBLEM_JSON_VALUE,
schema = Schema(implementation = ProblemDetail::class),
examples = [
ExampleObject(
name = "중복 등록",
value = """
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"detail": "입력 오류 — 요청을 수정하여 재시도",
"category": "INVALID_INPUT"
}
""",
),
],
),
],
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 실제 예외 핸들러에서 409와 함께 어떤 category 값을 쓰는지 확인
rg -nP -C3 '\bProblemDetail\b|category\s*='
rg -nP -C3 'register\s*\(' --type=kotlin

Repository: depromeet/18th-team3-server

Length of output: 12010


🏁 Script executed:

#!/bin/bash
# WishAlreadyExistsException과 ErrorCategory 정의 확인
rg -A5 'class WishAlreadyExistsException'
rg -A10 'enum class ErrorCategory'
rg -nC3 'HttpMappable'

Repository: depromeet/18th-team3-server

Length of output: 10286


🏁 Script executed:

#!/bin/bash
# ErrorCategory 전체 파일과 WishAlreadyExistsException 사용처 확인
cat src/main/kotlin/com/depromeet/team3/common/exception/ErrorCategory.kt
echo "---"
rg 'WishAlreadyExistsException' -C2

Repository: depromeet/18th-team3-server

Length of output: 2764


409 응답의 categorydetail 메시지가 실제 예외 정의와 어긋나고 있습니다.

문제: OpenAPI 예시를 수정하는 것만으로는 부족합니다. 실제로 WishAlreadyExistsExceptionErrorCategory.INVALID_INPUT으로 설정되어 있어서, 클라이언트가 받는 응답은 항상 "입력 오류 — 요청을 수정하여 재시도"가 나옵니다.

  • WishAlreadyExistsException (라인 15): category = ErrorCategory.INVALID_INPUT → 409(중복)에 400(검증 오류) 카테고리를 쓰는 것은 의미가 어긋남
  • 클라이언트가 "재시도"를 보고 무한 재시도 로직을 짤 수 있음
  • 또한 ErrorCategory enum에 DUPLICATE 또는 CONFLICT 같은 카테고리가 없음

해결 방안:

1단계: ErrorCategory enum에 새로운 카테고리 추가

enum class ErrorCategory(val description: String) {
    INVALID_INPUT("입력 오류 — 요청을 수정하여 재시도"),
    DUPLICATE_RESOURCE("이미 등록된 리소스입니다."),  // 추가
    // ... 나머지
}

2단계: WishAlreadyExistsException에서 해당 카테고리 사용

class WishAlreadyExistsException(
    guestId: UUID,
    link: ProductLink,
) : BaseException("이미 위시리스트에 등록된 상품입니다. guestId=$guestId link=$link"), HttpMappable {
    override val httpStatus: HttpStatus = HttpStatus.CONFLICT
    override val category: ErrorCategory = ErrorCategory.DUPLICATE_RESOURCE  // 변경
}

3단계: OpenAPI 예시도 함께 수정

♻️ 문서 예시 수정
                                value = """
                                    {
                                      "type": "about:blank",
                                      "title": "Conflict",
                                      "status": 409,
-                                      "detail": "입력 오류 — 요청을 수정하여 재시도",
-                                      "category": "INVALID_INPUT"
+                                      "detail": "이미 등록된 리소스입니다.",
+                                      "category": "DUPLICATE_RESOURCE"
                                    }
                                """,

이렇게 하면 실제 응답과 문서가 일치하고, 클라이언트가 상태 코드와 카테고리로 명확하게 분기 처리할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt`
around lines 87 - 110, Add a new duplicate/conflict category to the
ErrorCategory enum (e.g., DUPLICATE_RESOURCE or CONFLICT) and update the
WishAlreadyExistsException to use that category instead of
ErrorCategory.INVALID_INPUT; then update the OpenAPI example for the 409
response so its "category" and "detail" match the new ErrorCategory text and the
actual message produced by WishAlreadyExistsException. Ensure you modify the
ErrorCategory enum declaration and the WishAlreadyExistsException class
(overridden httpStatus/category properties) and the ApiResponse ExampleObject
for the 409 case so code, runtime response, and docs are consistent.

Comment thread src/main/resources/static/docs/index.html Outdated
@github-actions github-actions Bot requested review from 1o18z and m-a-king April 26, 2026 16:02
- v9.x에서 React Router v6 업그레이드로 hash 라우팅 회귀 버그 발생
- v8.5.2로 고정: hash router 정상 동작이 확인된 마지막 v8 버전
- integrity + crossorigin 속성으로 SRI 적용
- API 버전(v1)은 빌드 버전(0.0.1-SNAPSHOT)과 별개의 public 계약 버전
- project.version은 Gradle 컨텍스트 전용이라 Spring Bean에서 참조 불가
@sevineleven sevineleven merged commit 99dc358 into main Apr 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API 문서 자동화 도입 (SpringDoc + Stoplight Elements)

2 participants