Skip to content

docs: 스웨거 문서 정리 및 업로드 문제 해결 - #167

Merged
chazy-d merged 12 commits into
mainfrom
docs/swagger-api-descriptions
Aug 11, 2026
Merged

chazy-d merged 12 commits into
mainfrom
docs/swagger-api-descriptions

Conversation

@chazy-d

@chazy-d chazy-d commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🔗 관련 이슈 (Related Issue)

📝 작업 내용

Swagger 문서가 실제 API 동작을 그대로 보여주도록 정비하고, 그 과정에서 드러난 업로드 관련 버그 두 개를 함께 고쳤습니다.

문서 쪽 공백은 세 가지였습니다.

첫째, 실패 응답이 통째로 빠져 있었습니다. 에러는 GlobalExceptionHandler가 전역에서 처리하기 때문에 컨트롤러에는 흔적이 남지 않고, 그래서 문서에는 성공 응답만 실렸습니다. 108개 엔드포인트 중 실패 응답이 문서화된 곳은 0개였습니다.

둘째, 인증 방식이 구분되지 않았습니다. 문서 전체에 인증 요구가 걸려 있어서 모든 엔드포인트가 토큰 필수로 읽혔습니다. 실제로는 SecurityConfigpermitAll 경로 19개가 토큰 없이 열려 있고, 그중 8개는 토큰이 있어도 되고 없어도 되는 게스트 참여 경로입니다.

셋째, 엔드포인트 설명이 있는 곳과 없는 곳이 섞여 있었습니다. 108개 중 69개만 설명(description)을 갖고 있었고, 나머지 39개는 이름(summary)만 있었습니다.

문서를 맞추는 과정에서 업로드 경로의 버그 두 개를 찾았습니다.

앱과 프론트는 100MB로 맞춰져 있는데 운영에서는 1MB만 넘어도 업로드가 막히고 있었습니다. nginx에 client_max_body_size가 설정된 적이 없어 기본값 1MB로 동작하고 있었습니다.

그리고 nginx를 열고 보니, 앱 한도를 넘긴 요청은 500(COMMON500)으로 나가고 있었습니다. multipart 한도 초과 예외가 전역 핸들러에 등록되어 있지 않았습니다.

그리고 지금 main이 깨져 있는 문제(마이그레이션 버전 016 중복)를 7번에서 함께 고쳤습니다. 급하면 그 커밋만 따로 떼어 먼저 머지해도 됩니다.

런타임 동작 변경 범위: 14번은 문서만 바꿉니다. 56번은 동작이 바뀝니다 — nginx가 105MB까지 통과시키고, 한도 초과 응답이 500에서 413으로 바뀝니다. 7번은 파일 이름만 바꿉니다.

1. 공통 에러 응답 전역 문서화

모든 엔드포인트에 공통 실패 응답을 붙였습니다. 다만 실제로 발생할 수 있는 상태 코드만 붙입니다.

상태 붙이는 조건
400 파싱할 본문이나 파라미터가 있는 경우
401 토큰이 없으면 막히는 경우
404 경로 변수로 리소스를 찾는 경우
500 전부

조건을 두지 않고 일괄로 붙이면, 문서에는 있지만 실제로는 나지 않는 상태 코드가 생깁니다. 그건 명세와 구현이 어긋난 것과 같아서 없느니만 못하다고 판단했습니다. 파라미터도 본문도 없는 엔드포인트에 400이 붙지 않는 이유가 그것입니다.

엔드포인트가 @ApiResponse로 직접 선언한 응답이 있으면 그쪽이 우선합니다. 여기서 덮어쓰면 개별 문서화가 무의미해집니다.

400은 두 가지 예시를 함께 보여줍니다.

// 검증 실패
{
  "isSuccess": false,
  "code": "COMMON400",
  "message": "요청 값이 올바르지 않습니다.",
  "result": { "errors": [ { "field": "title", "reason": "공백일 수 없습니다" } ] }
}

// 잘못된 요청 (본문 파싱 실패, 타입 불일치, 필수 파라미터 누락)
{
  "isSuccess": false,
  "code": "COMMON400",
  "message": "요청 값이 올바르지 않습니다.",
  "result": null
}

2. 스키마와 예시를 코드에서 뽑아내기

문서에 값을 손으로 적어두면, 응답 클래스나 에러 코드가 바뀌었을 때 코드는 정상인데 문서만 조용히 낡습니다. 그래서 문서에 들어가는 값을 전부 코드에서 가져옵니다.

  • 실패 응답 스키마는 ApiResponse 클래스에서 추출합니다.
  • 예시의 codemessageCommonErrorCode enum에서 그대로 가져옵니다.
  • 예시의 필드 구성은 실제 ApiResponse.failure(...)를 직렬화해서 만듭니다.

ApiResponse에 붙은 예시는 성공 기준(COMMON200)이라 실패 스키마에 그대로 쓰면 문서가 거짓말을 합니다. 실패 스키마에서는 실제 실패 응답 값으로 덮어씁니다.

검증 실패 본문(ValidationErrorResponse)은 실패 응답의 result가 직접 참조하도록 연결했습니다. 참조되지 않는 스키마는 springdoc이 문서에서 걷어내기 때문에, 이 연결이 곧 등록 조건이기도 합니다.


3. 인증 방식 명시

SecurityConfigpermitAll 경로를 문서에 그대로 옮겼습니다. 세 가지로 나뉩니다.

인증 불필요 (11개)@SecurityRequirements로 자물쇠를 뗍니다.

로그인/회원가입/토큰 재발급 등 인증 관련 7개는 이미 표시되어 있었고, 이번에 GET /api/v1/health, GET /api/v1/project-invitations/{token}, GET /api/v1/share-links/{token}, POST /api/v1/share-links/{token}/guests 4개를 추가했습니다.

인증 선택 (8개) — 게스트 피드백/답글 참여 경로입니다.

이 경로들은 토큰 없이 열려 있지만, 토큰을 함께 보내면 로그인 사용자로 처리됩니다. 문서에서 이 둘은 구분되지 않았습니다. 자물쇠만 보면 토큰이 필수처럼 읽히고, 자물쇠를 떼면 로그인 사용자로 호출할 방법이 없는 것처럼 읽힙니다.

OpenAPI는 security 목록에 빈 요구사항을 함께 넣으면 둘 다 허용이라는 뜻이 됩니다. 그 표현을 쓰고, 토큰 유무에 따라 무엇이 달라지는지 설명으로 덧붙였습니다.

"security": [ { "bearerAuth": [] }, {} ]
인증은 선택입니다. 토큰을 보내면 로그인 사용자로, 보내지 않으면 게스트로 처리됩니다.

대상은 POST/GET /videos/{videoId}/feedbacks, PATCH/DELETE /feedbacks/{feedbackId}, POST/GET /feedbacks/{feedbackId}/replies, PATCH/DELETE /replies/{replyId} 입니다.

PATCH /feedbacks/{feedbackId}/statusPATCH /replies/{replyId}/statuspermitAll에 없어서 인증 필수로 두었습니다.

나머지 (89개) — 401을 문서화합니다.

자물쇠 표시와 401 문서화가 서로 어긋나지 않도록, 두 판정을 EndpointAuthentication 한 곳에서 가져다 씁니다.


4. 비어 있던 엔드포인트 설명 39개

설명이 빠져 있던 곳은 프로젝트 계열에 몰려 있었습니다.

영역 빠져 있던 개수
Project 7
Project File 7
Project Notice 6
Project Member 5
Project Invitation 3
Recent Activity 3
Notification 3
Recruitment 2
Feedback / Reply 상태 변경 2
Health 1

비어 있던 39개를 채워 108개 전부가 설명을 갖습니다.

summary를 풀어 쓴 문장은 쓰지 않았습니다.

설명을 채우는 가장 쉬운 방법은 이름을 문장으로 늘리는 것입니다.

summary: 프로젝트 삭제
description: 프로젝트를 삭제한다.   ← 이러면 커버리지만 100%가 되고 공백은 그대로입니다

이건 읽는 사람에게 아무것도 주지 않으면서 문서만 길어집니다. 그래서 호출하는 쪽이 지금까지 코드를 열어봐야 알 수 있던 것만 적었습니다.

summary: 프로젝트 삭제
description: ADMIN 만 삭제할 수 있다. 실제로 지우지 않고 삭제 표시만 남긴다.

서비스 코드와 쿼리를 읽고 확인한 것만 적었습니다. 크게 여섯 갈래입니다.

권한 — 누가 호출할 수 있는지가 엔드포인트마다 다릅니다.

  • 프로젝트 수정/삭제, 초대 링크 생성: ADMIN만
  • 파일 수정/삭제/고정: 업로더 본인 또는 ADMIN
  • 공지 수정/삭제: 작성자 본인 또는 ADMIN
  • 멤버 역할 수정: ADMIN 또는 본인
  • 멤버 내보내기: ADMIN만, 자기 자신은 불가 (나가기를 써야 함)
  • 프로젝트 나가기: ADMIN은 불가

덮어쓰기 여부 — 같은 PATCH인데 동작이 반대인 곳이 있습니다.

  • PATCH /projects/{projectId}: 보내지 않은 필드가 null로 덮어써집니다
  • PATCH /projects/{projectId}/files/{fileId}: 보내지 않은 필드는 기존 값이 유지됩니다

둘 다 PATCH라서 문서만 봐서는 구분할 수 없었습니다. 프로젝트 수정 쪽은 "바꾸지 않을 값도 함께 보내야 한다"를 명시했습니다.

적용 범위 — 같은 "고정"인데 보이는 범위가 다릅니다.

  • 프로젝트 고정: 호출한 사람에게만 적용 (다른 멤버 목록 순서에 영향 없음)
  • 파일 고정: 프로젝트 멤버 모두에게 함께 보임

제한값

  • 프로젝트 생성 5개 (삭제한 것은 미포함)
  • 파일 업로드 100MB, pdf/jpg/jpeg/png/doc/docx만, 확장자와 Content-Type 일치 필요
  • 초대 링크 기본 유효기간 72시간
  • 목록 조회 size 기본 20 / 최대 50

조회 범위

  • GET /notifications: 최근 24시간 이내 갱신된 알림만 반환, 읽지 않은 것 우선 정렬
  • PATCH /notifications/read-all: 24시간 제한이 없어 목록에 보이지 않는 오래된 알림도 함께 처리됩니다

목록에 3개만 보이는데 전체 읽음을 누르면 그보다 많이 처리되는 이유입니다.

응답 형태

  • GET /projects/{projectId}/files/{fileId}/download만 공통 응답 래퍼를 쓰지 않고 파일 본문을 그대로 반환합니다 (Content-Disposition: attachment)

그 외에 초대 링크는 원본 토큰이 응답의 inviteUrl에만 담기고 서버에는 해시로 저장되어 나중에 같은 링크를 다시 조회할 수 없다는 점, 구인 공고 상세는 조회수를 먼저 올리고 읽어서 응답의 viewCount에 이번 조회가 반영된다는 점, 멤버 상세의 memberId사용자 ID가 아니라 프로젝트 멤버 ID라는 점을 적었습니다.

이름만 보고 적었으면 틀렸을 것 두 가지

  • 프로젝트 목록 정렬을 "최근 활동순"으로 적으려 했는데, 쿼리는 p.id desc최근에 만들어진 순이었습니다.
  • 공지 읽음 처리를 "처음 읽은 시각이 유지된다"고 적으려 했는데, ON DUPLICATE KEY UPDATE read_at = :now읽은 시각이 갱신됩니다.

둘 다 코드를 안 봤으면 그대로 나갔을 내용입니다.

문서 예시에 실제 팀원 이름이 들어가 있던 것도 함께 정리했습니다. 닉네임 필드는 규칙 문서 예시와 같은 "그린", 실명 필드는 제 이름으로 바꿨습니다.


5. nginx 업로드 한도 미설정 (운영 반영 완료)

파일 업로드 한도를 문서에 적으려고 확인하다가 발견했습니다. 앱은 100MB, 프론트도 100MB로 안내하는데 운영에서는 1MB를 넘으면 업로드가 실패하고 있었습니다.

nginx client_max_body_size의 기본값이 1MB인데, 이 설정을 어디에도 두지 않았습니다. 운영에서 크기를 바꿔가며 확인한 결과입니다.

요청 크기 결과
900KB 앱까지 도달, 400 PROJECT_FILE_INVALID_TYPE400 (공통 응답 JSON)
1.07MB 413, nginx 기본 HTML
3MB 413, 본문 0바이트 전송 후 차단

문제가 두 겹입니다. 요청이 스프링에 닿지 않으니 서비스의 검증 로직은 실행되지 않고, 응답도 공통 래퍼가 아니라 nginx 기본 HTML이라 프론트가 원인을 표시할 수 없습니다.

infra/nginx/conf.d/upload-limits.conf를 추가했습니다.

client_max_body_size 105M;

proxy_read_timeout 300s;
proxy_send_timeout 300s;
  • 105MB는 앱의 spring.servlet.multipart.max-request-size와 맞춘 값입니다. 개별 파일 한도(100MB)에 JSON 파트와 멀티파트 경계가 더해지므로 그보다 커야 합니다.
  • 타임아웃 300초는 큰 파일 때문입니다. 앱이 S3 업로드를 마쳐야 응답하는데 기본 60초로는 100MB 근처에서 504가 납니다. 파일은 올라갔는데 실패로 보이는 상태가 됩니다.
  • conf.d에 두어 http 레벨에 적용했습니다. certbot이 관리하는 server 블록은 건드리지 않고, 모든 server/location으로 상속됩니다.

설정을 서버에만 두면 인스턴스를 새로 띄울 때 기본값으로 돌아갑니다. 리포의 파일이 매 배포마다 반영되도록 cd-prod.yml에 단계를 넣었습니다.

  • 내용이 같으면 reload를 건너뜁니다.
  • nginx -t를 통과할 때만 reload하고, 실패하면 복사한 파일을 지우고 실행 중인 nginx는 그대로 둡니다.

운영에는 이미 수동으로 같은 설정을 반영해 두었습니다. 이 PR은 그 상태를 코드로 고정하는 쪽입니다.


6. 업로드 한도를 넘긴 요청이 500으로 나가던 문제

nginx를 열고 나니 앱 한도(100MB)를 넘긴 요청이 다음 벽입니다. 그 응답을 확인해 보니 500이었습니다.

multipart 한도 초과는 요청 본문을 읽는 단계에서 MaxUploadSizeExceededException으로 끊깁니다. 컨트롤러에 닿지 않으니 서비스의 파일 크기 검증(PROJECT_FILE_SIZE400, USER_PROFILE_IMAGE_SIZE400 등)은 실행조차 되지 않습니다. 전역 핸들러에 이 예외가 없어서 @ExceptionHandler(Exception.class)가 받았고, 결과적으로 COMMON500이 나갔습니다. 프론트는 "파일이 너무 큽니다" 대신 서버 오류를 표시하고, 서버에는 사용자 실수 때마다 ERROR 로그가 쌓입니다.

CommonErrorCodeCOMMON413을 추가하고 두 핸들러를 등록했습니다.

예외 응답
MaxUploadSizeExceededException 413 COMMON413 "업로드 용량이 허용된 한도를 초과했습니다."
MultipartException (그 외 해석 실패) 400 COMMON400

413으로 맞춘 이유는 5번과 같은 갈래로 처리되게 하기 위해서입니다. nginx도 한도를 넘기면 413을 주므로, 프론트는 105MB에서 막히든 100MB에서 막히든 413 하나만 보면 됩니다.

400을 함께 잡은 이유는 업로드 중 연결이 끊기면(본문이 잘림, boundary 불일치) 지금은 500이 찍히기 때문입니다. 클라이언트 요청 문제이므로 400으로 내렸습니다.

MaxUploadSizeExceededExceptionMultipartException의 하위 타입이고, 스프링은 더 구체적인 핸들러를 고르므로 둘을 함께 등록해도 안전합니다.

검증: MultipartUploadLimitTest를 추가했습니다. 실제 포트를 띄워(RANDOM_PORT + TestRestTemplate) 한도를 1KB로 낮추고 진짜 HTTP 요청을 보냅니다. MockMvc로는 재현되지 않습니다 — MockMvcRequestBuilders.multipart()는 테스트가 요청 객체를 직접 조립하기 때문에 파싱 자체가 일어나지 않고, 한도는 서블릿 컨테이너가 강제하는 값이라 검사도 일어나지 않습니다.

핸들러를 임시로 되돌린 상태에서 이 테스트가 실패하는 것까지 확인했습니다.


7. 마이그레이션 버전 016 중복 (현재 main 배포 불가 상태)

이 브랜치와는 별개로 지금 main이 깨져 있어 함께 고쳤습니다.

V016__guest_session_token.sqlV016__portfolio_period.sql이 같은 버전 번호를 씁니다. 두 브랜치가 각자 016을 집었는데, 파일 이름이 달라서 git은 충돌로 잡지 못합니다. 그냥 다른 파일 두 개로 보여 깨끗하게 머지됩니다. 같은 버전이라는 건 Flyway만 압니다.

각 PR의 CI는 자기 브랜치에 016이 하나뿐이라 통과했고, main에 합쳐진 뒤에야 터졌습니다.

  • CI: flywayInfo에서 Found more than one migration with version 016
  • CD: 앱 기동 중 flywayInitializer에서 같은 예외 → Health Check 실패 → 자동 롤백

포트폴리오 쪽을 V018로 옮겼습니다. 게스트 쪽 V016·V017은 이미 운영에 적용되어 이력에 남아 있어 번호를 바꿀 수 없습니다. 아직 적용되지 않은 것은 포트폴리오 쪽 하나뿐이고, 017까지 차 있어 다음 빈 번호가 018입니다.

두 파일은 건드리는 테이블이 겹치지 않아(guest vs user_portfolio) 실행 순서는 결과에 영향이 없습니다. SQL 내용은 그대로입니다.

이 브랜치가 main보다 뒤처져 있어 main을 먼저 병합했습니다. PATCH /projects/{projectId} 설명이 양쪽에서 함께 수정되어 충돌했고, 호출 규칙(ADMIN 전용, 미전송 필드 null 덮어쓰기)과 완료 전환 동작 설명을 둘 다 남겼습니다.

재발 방지 제안: main 브랜치 보호에 "Require branches to be up to date before merging"을 켜면 이 유형은 막힙니다. 이번엔 이틀 전 main 기준으로 통과한 CI 결과로 머지됐습니다. 켜두면 먼저 들어간 쪽이 있을 때 나중 브랜치가 main을 다시 당기고 CI를 돌려야 하고, 그때 flywayInfo가 잡습니다.


8. 생성된 문서 검증 테스트

문서는 애노테이션에서 조립되기 때문에 애노테이션을 빠뜨려도 빌드가 깨지지 않습니다. 누락은 배포된 문서를 열어봐야 드러납니다. 그래서 /v3/api-docs를 실제로 호출해서 결과를 검증하는 테스트를 추가했습니다.

  • 모든 엔드포인트가 이름(summary)을 가진다
  • 모든 엔드포인트가 summary를 되풀이하지 않는 설명(description)을 가진다
  • 모든 엔드포인트가 500을 문서화한다
  • 경로 변수를 받는 엔드포인트가 404를 문서화한다
  • 401은 인증이 필수인 엔드포인트에만 문서화된다
  • 인증이 선택인 엔드포인트는 토큰 없는 호출도 허용한다고 표시한다
  • 실패 응답 스키마의 필드와 예시가 실제 응답 객체와 일치한다
  • 검증 실패 본문 스키마가 참조로 살아 있다

기대값은 실제 응답 객체를 직렬화해서 만듭니다. 필드명을 테스트에 적어두면 그 하드코딩도 코드와 같이 낡기 때문입니다.

문서에서 감추려는 엔드포인트는 @Hidden을 붙이면 검증 대상에서 빠집니다. 현재 GET /api/v1/auth/callback/google 하나가 여기 해당합니다.


✅ PR 체크리스트

  • PR 제목은 커밋 컨벤션을 따랐습니다.
  • 관련 이슈를 연결했습니다.
  • ./gradlew compileJava로 컴파일을 확인했습니다.
  • ./gradlew test로 테스트를 확인했습니다. (136개 통과, main 병합 후 기준)
  • 생성된 /v3/api-docs에서 설명 없는 엔드포인트가 0개임을 확인했습니다. (108/108)
  • 동작이 바뀌는 범위를 본문에 명시했습니다. (nginx 한도, 413 응답)
  • main을 병합해 최신 상태에서 CI를 확인했습니다.

Summary by CodeRabbit

  • 새 기능

    • 파일 업로드 용량 초과 시 명확한 오류 응답을 제공합니다.
    • 게스트 및 로그인 사용자 모두 이용 가능한 선택적 인증 API 문서를 지원합니다.
    • API 문서에 인증 조건, 페이지네이션, 권한, 오류 응답 및 요청 제한 정보를 보강했습니다.
    • 서버 상태 확인 API를 인증 없이 조회할 수 있습니다.
    • 포트폴리오 참여 기간 정보를 저장할 수 있습니다.
  • 개선 사항

    • 대용량 업로드와 장시간 파일 전송을 지원하도록 요청 제한 및 타임아웃을 조정했습니다.
    • 배포 시 Nginx 설정을 검증한 뒤 안전하게 적용합니다.

에러는 GlobalExceptionHandler 가 전역에서 처리해서 컨트롤러에 흔적이 남지 않는다.
그래서 생성된 문서에는 성공 응답만 있고 실패 응답이 통째로 빠져 있었다.

OperationCustomizer 로 모든 엔드포인트에 공통 에러 응답을 붙인다.
실제로 발생할 수 있는 상태 코드만 붙인다.
- 400: 파싱할 본문이나 파라미터가 있는 경우
- 404: 경로 변수로 리소스를 찾는 경우
- 500: 전부

응답 스키마와 예시는 ApiResponse, ValidationErrorResponse, CommonErrorCode 에서
직접 뽑아낸다. 손으로 적어두면 응답이 바뀌었을 때 문서만 조용히 낡는다.
문서 전체에 인증 요구가 걸려 있어서 모든 엔드포인트가 토큰 필수로 읽혔다.
실제로는 SecurityConfig 의 permitAll 경로 19개가 토큰 없이 열려 있다.

세 가지로 나눠 표시한다.
- 인증 불필요 11개: @SecurityRequirements 로 자물쇠를 뗀다
- 인증 선택 8개: 게스트 참여 경로. security 에 빈 요구사항을 함께 넣어
  토큰이 있어도 없어도 되는 상태를 표현하고, 무엇이 달라지는지 설명한다
- 나머지: 401 을 문서화한다

401 은 인증이 필수인 곳에만 붙인다. 열려 있는 경로에 붙이면 발생하지 않는
상태 코드가 명세에 실린다.

동작은 그대로다. 접근 제어는 SecurityConfig 가 결정하고 이 커밋은 문서만 바꾼다.
문서는 애노테이션에서 조립되기 때문에 애노테이션을 빠뜨려도 빌드가 깨지지 않는다.
누락은 배포된 문서를 열어봐야 드러난다. 생성 결과를 직접 확인해서 먼저 깨지게 한다.

- 모든 엔드포인트가 설명을 가진다
- 400/401/404/500 이 발생 조건에 맞게 문서화된다
- 실패 응답 스키마와 예시가 실제 응답 객체와 일치한다
- 검증 실패 본문 스키마가 참조로 살아 있다

기대값은 실제 응답 객체를 직렬화해서 만든다.
필드명을 테스트에 적어두면 그 하드코딩도 코드와 같이 낡는다.
설명이 있는 엔드포인트가 108개 중 69개뿐이라 명세가 덜 된 것처럼 보였다.
비어 있던 39개를 채워 108개 전부가 설명을 갖는다.

summary 를 풀어 쓴 문장은 쓰지 않았다. 이름만 다시 적으면 공백은 그대로다.
대신 호출하는 쪽이 코드를 열어봐야 알 수 있던 제약을 적었다.

- 권한: 프로젝트 수정/삭제는 ADMIN 만, 파일과 공지는 작성자 또는 ADMIN
- 덮어쓰기 여부: 프로젝트 수정은 전체 교체, 파일 수정은 부분 수정
- 적용 범위: 프로젝트 고정은 개인별, 파일 고정은 멤버 전체 공유
- 제한값: 프로젝트 5개, 파일 100MB 와 허용 확장자, 초대 기본 72시간
- 조회 범위: 알림 목록은 최근 24시간, 전체 읽음은 제한 없음
- 응답 형태: 파일 다운로드만 공통 래퍼 대신 본문을 그대로 반환

내용은 전부 서비스 코드에서 확인했다. 이름만 보고 지으면 문서가 거짓말을 한다.
설명이 다시 비거나 summary 를 되풀이하면 깨지도록 회귀 테스트를 함께 넣었다.
문서 예시에 실제 팀원 이름이 그대로 들어가 있었다.
닉네임 필드는 규칙 문서 예시와 같은 "그린"으로, 실명 필드는 "차태훈"으로 바꾼다.
multipart 한도(파일 100MB, 요청 105MB)를 넘긴 요청은 본문을 읽는 단계에서
MaxUploadSizeExceededException 으로 끊긴다. 컨트롤러에 닿지 않으니
서비스의 파일 크기 검증(PROJECT_FILE_SIZE400 등)은 실행조차 되지 않는다.

전역 핸들러에 이 예외가 없어서 @ExceptionHandler(Exception.class) 가 받았고,
결과적으로 COMMON500 이 나갔다. 프론트는 "파일이 너무 큽니다" 대신
서버 오류를 표시하게 되고, 서버에는 ERROR 로그가 쌓인다.

앞단 nginx 도 한도를 넘기면 413 을 주므로 상태 코드를 413 으로 맞춘다.
프론트가 nginx 차단과 앱 차단을 한 갈래로 처리할 수 있다.

한도 초과 외의 multipart 해석 실패(본문이 잘림, boundary 불일치)도 함께 잡는다.
업로드 중 연결이 끊길 때마다 500 이 찍히던 것을 400 으로 내린다.

MockMvc 는 multipart 요청을 테스트가 직접 조립해서 파싱 자체가 일어나지 않는다.
한도는 서블릿 컨테이너가 강제하므로 실제 포트를 띄워 진짜 HTTP 요청으로 검증한다.
프로젝트 수정 API 설명이 양쪽에서 함께 수정돼 충돌했다.
호출 규칙(ADMIN 전용, 미전송 필드 null 덮어쓰기)과
완료 전환 동작 설명을 둘 다 남긴다.
게스트 세션 토큰(#162)과 포트폴리오 참여 기간(#160)이 각자 V016 을 썼다.
파일명이 서로 달라 git 은 충돌로 잡지 못했고, main 에 합쳐진 뒤에야
Flyway 가 Found more than one migration with version 016 으로 기동을 막았다.
CI 는 flywayInfo 에서, CD 는 앱 기동 중 flywayInitializer 에서 실패했다.

게스트 쪽 V016·V017 은 이미 운영에 적용돼 이력에 남아 있어 번호를 바꿀 수 없다.
아직 어디에도 적용되지 않은 포트폴리오 쪽을 다음 빈 번호인 018 로 옮긴다.

두 파일은 건드리는 테이블이 겹치지 않아 실행 순서는 결과에 영향이 없다.
SQL 내용은 그대로다.
@chazy-d chazy-d self-assigned this Aug 11, 2026
@chazy-d chazy-d added documents 문서 수정 fix 버그 수정 labels Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 80f943d5-0ba8-4c1a-b624-92a6aa1fa761

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Swagger에 인증 방식과 공통 오류 응답을 반영했습니다. 주요 API 설명을 보강했습니다. multipart 업로드 오류 처리와 Nginx 배포 단계를 추가했습니다. 포트폴리오 참여 기간 컬럼을 추가했습니다.

Changes

API 인증 및 오류 문서화

Layer / File(s) Summary
선택적 인증 문서화
src/main/java/com/slatto/global/config/*, src/main/java/com/slatto/domain/feedback/controller/*, src/main/java/com/slatto/domain/project/controller/ProjectInvitationController.java, src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java, src/main/java/com/slatto/global/health/controller/HealthCheckController.java
OptionalAuthentication을 추가했습니다. 선택적·익명 인증 엔드포인트의 Swagger 보안 요구사항과 안내 문구를 설정했습니다.
공통 오류 응답 스키마 및 응답 주입
src/main/java/com/slatto/global/config/*, src/main/java/com/slatto/global/response/*, src/main/java/com/slatto/global/exception/ValidationErrorResponse.java, src/test/java/com/slatto/global/config/OpenApiDocumentationTest.java
OpenAPI에 400·401·404·500 응답과 공통 오류 스키마를 추가했습니다. 생성된 문서의 스키마, 예시, 인증 설정을 통합 검증합니다.
엔드포인트 설명 보강
src/main/java/com/slatto/domain/{notification,project,recruitment}/controller/*, src/main/java/com/slatto/domain/{feedback,schedule,video}/dto/*
알림, 프로젝트, 파일, 초대, 멤버, 공지, 구인구직 API의 권한, 범위, 페이지네이션, 동작 설명과 Swagger 예시를 보강했습니다.

multipart 업로드 및 배포

Layer / File(s) Summary
multipart 오류 및 Nginx 설정 배포
.github/workflows/cd-prod.yml, infra/nginx/conf.d/upload-limits.conf, src/main/java/com/slatto/global/exception/*, src/main/java/com/slatto/global/response/code/CommonErrorCode.java, src/test/java/com/slatto/global/exception/MultipartUploadLimitTest.java
업로드 제한 초과를 413 응답으로 처리합니다. Nginx 설정을 EC2에 복사하고 nginx -t 성공 시에만 reload합니다. 통합 테스트가 공통 오류 응답을 검증합니다.

포트폴리오 참여 기간 스키마

Layer / File(s) Summary
참여 기간 컬럼 추가
src/main/resources/db/migration/V018__portfolio_period.sql
user_portfolio 테이블에 nullable start_dateend_date 컬럼을 추가했습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant OpenAPI
  participant SwaggerAuthenticationCustomizer
  participant SwaggerErrorResponseCustomizer
  participant OpenApiDocumentationTest

  Client->>OpenAPI: API 문서 요청
  OpenAPI->>SwaggerAuthenticationCustomizer: 엔드포인트 인증 정보 전달
  SwaggerAuthenticationCustomizer-->>OpenAPI: security 요구사항 및 안내 문구 설정
  OpenAPI->>SwaggerErrorResponseCustomizer: 작업 정보 전달
  SwaggerErrorResponseCustomizer-->>OpenAPI: 조건별 오류 응답 추가
  OpenApiDocumentationTest->>OpenAPI: 생성 문서 조회
  OpenAPI-->>OpenApiDocumentationTest: 인증·오류 스키마 및 예시 반환
Loading

Possibly related PRs

Suggested labels: feature

Suggested reviewers: guingguing, young0206, sangwon02

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning [165]의 Swagger 문서화 목표는 충족하지만, 이슈가 금지한 런타임 변경과 Flyway 마이그레이션 변경을 포함합니다. Swagger 문서화 변경과 업로드·마이그레이션 변경을 분리하거나, 이슈 #165의 범위를 확장하고 관련 요구사항을 명시하세요.
Out of Scope Changes check ⚠️ Warning nginx 설정, multipart 예외 처리, 413 응답 변경과 Flyway V018 마이그레이션은 [165]의 문서화 범위를 벗어납니다. nginx·multipart 처리·Flyway 마이그레이션 변경을 별도 PR로 분리하거나, 해당 변경을 포함하는 관련 이슈를 연결하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 15.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 Swagger 문서 정비와 업로드 문제 해결이라는 PR의 주요 변경 사항을 명확하고 간결하게 요약합니다.
Description check ✅ Passed 관련 이슈, 작업 내용, 동작 변경 범위, 테스트 결과와 체크리스트를 구체적으로 작성했습니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/cd-prod.yml:
- Around line 83-89: Update the Nginx replacement flow around TARGET and the
nginx -t check to back up the existing TARGET before copying SOURCE. On
validation failure, restore that backup when TARGET originally existed; only
remove TARGET when no original file was present, preserving the current failure
exit behavior.

In
`@src/main/java/com/slatto/domain/project/controller/ProjectInvitationController.java`:
- Around line 34-39: Update the description in the `@Operation` annotation for
ProjectInvitationController so it states that users with the inviteUrl can
retrieve the invitation information again, while losing the original token
prevents the server from recovering or reissuing it.

In `@src/main/java/com/slatto/global/config/SwaggerErrorResponseCustomizer.java`:
- Around line 44-65: Update SwaggerErrorResponseCustomizer to add HTTP 413 for
multipart endpoints, using the ErrorResponse schema and PAYLOAD_TOO_LARGE
example while preserving existing response conditions. Extend
OpenApiDocumentationTest to verify the generated 413 response for all three
multipart endpoints; apply the customizer change in
src/main/java/com/slatto/global/config/SwaggerErrorResponseCustomizer.java:44-65
and the corresponding assertions in
src/test/java/com/slatto/global/config/OpenApiDocumentationTest.java:96-108.

In `@src/main/java/com/slatto/global/response/ApiResponse.java`:
- Line 24: Update the `@Schema` description on the result field in ApiResponse to
state that result is null for general failures and contains a field-error list
for validation failures, matching ApiResponse.failure(errorCode, response).
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f346306-ca4e-4404-9e21-33c4db021473

📥 Commits

Reviewing files that changed from the base of the PR and between 7caa67d and fb5a44b.

📒 Files selected for processing (29)
  • .github/workflows/cd-prod.yml
  • infra/nginx/conf.d/upload-limits.conf
  • src/main/java/com/slatto/domain/feedback/controller/FeedbackController.java
  • src/main/java/com/slatto/domain/feedback/controller/FeedbackDetailController.java
  • src/main/java/com/slatto/domain/feedback/dto/response/FeedbackResponse.java
  • src/main/java/com/slatto/domain/notification/controller/NotificationController.java
  • src/main/java/com/slatto/domain/notification/controller/RecentActivityController.java
  • src/main/java/com/slatto/domain/project/controller/ProjectController.java
  • src/main/java/com/slatto/domain/project/controller/ProjectFileController.java
  • src/main/java/com/slatto/domain/project/controller/ProjectInvitationController.java
  • src/main/java/com/slatto/domain/project/controller/ProjectMemberController.java
  • src/main/java/com/slatto/domain/project/controller/ProjectNoticeController.java
  • src/main/java/com/slatto/domain/recruitment/controller/RecruitmentController.java
  • src/main/java/com/slatto/domain/schedule/dto/ScheduleDailyResponse.java
  • src/main/java/com/slatto/domain/sharelink/controller/ShareLinkController.java
  • src/main/java/com/slatto/domain/video/dto/response/VideoResponse.java
  • src/main/java/com/slatto/global/config/EndpointAuthentication.java
  • src/main/java/com/slatto/global/config/OptionalAuthentication.java
  • src/main/java/com/slatto/global/config/SwaggerAuthenticationCustomizer.java
  • src/main/java/com/slatto/global/config/SwaggerConfig.java
  • src/main/java/com/slatto/global/config/SwaggerErrorResponseCustomizer.java
  • src/main/java/com/slatto/global/exception/GlobalExceptionHandler.java
  • src/main/java/com/slatto/global/exception/ValidationErrorResponse.java
  • src/main/java/com/slatto/global/health/controller/HealthCheckController.java
  • src/main/java/com/slatto/global/response/ApiResponse.java
  • src/main/java/com/slatto/global/response/code/CommonErrorCode.java
  • src/main/resources/db/migration/V018__portfolio_period.sql
  • src/test/java/com/slatto/global/config/OpenApiDocumentationTest.java
  • src/test/java/com/slatto/global/exception/MultipartUploadLimitTest.java

Comment on lines +83 to +89
sudo cp "$SOURCE" "$TARGET"

# 문법 검사를 통과할 때만 reload 한다. 실패하면 실행 중인 nginx 는 그대로 둔다.
if ! sudo nginx -t; then
echo "nginx 설정 검사 실패. 이전 설정으로 되돌립니다."
sudo rm -f "$TARGET"
exit 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

기존 Nginx 설정을 삭제하지 말고 복원하십시오.

Line 86의 nginx -t가 실패하면 Line 88이 현재 TARGET을 삭제합니다. 기존 upload-limits.conf가 있던 서버에서는 이전 설정이 복원되지 않습니다. 이후 Nginx가 재시작되면 업로드 제한과 타임아웃 설정이 사라집니다.

교체 전 TARGET을 백업하십시오. 검증 실패 시 백업을 다시 복원하십시오. 대상 파일이 원래 없던 경우에만 삭제하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/cd-prod.yml around lines 83 - 89, Update the Nginx
replacement flow around TARGET and the nginx -t check to back up the existing
TARGET before copying SOURCE. On validation failure, restore that backup when
TARGET originally existed; only remove TARGET when no original file was present,
preserving the current failure exit behavior.

Comment on lines +34 to +39
@Operation(
summary = "프로젝트 초대 링크 생성",
description = """
ADMIN 만 만들 수 있다. expirationPeriod 로 유효기간을 정하며 기본값은 72시간이다.
원본 토큰은 응답의 inviteUrl 에만 담기고 서버에는 해시로 저장되므로, 같은 링크를 나중에 다시 조회할 수 없다."""
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

초대 링크 재조회 제한 설명을 수정하십시오.

Line 38의 설명은 실제 동작과 다릅니다. getInvitation은 요청 토큰을 해시하여 저장된 초대를 조회하므로, inviteUrl을 가진 사용자는 링크 정보를 다시 조회할 수 있습니다.

원본 토큰을 잃으면 서버가 토큰을 복구하거나 재발급할 수 없다는 점을 설명하십시오.

수정 예시
-            원본 토큰은 응답의 inviteUrl 에만 담기고 서버에는 해시로 저장되므로, 같은 링크를 나중에 다시 조회할 수 없다."""
+            원본 토큰은 응답의 inviteUrl 에만 담기고 서버에는 해시로 저장된다.
+            응답을 잃으면 서버에서 원본 토큰을 복구하거나 같은 링크를 재발급할 수 없다."""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Operation(
summary = "프로젝트 초대 링크 생성",
description = """
ADMIN 만들 있다. expirationPeriod 유효기간을 정하며 기본값은 72시간이다.
원본 토큰은 응답의 inviteUrl 에만 담기고 서버에는 해시로 저장되므로, 같은 링크를 나중에 다시 조회할 없다."""
)
`@Operation`(
summary = "프로젝트 초대 링크 생성",
description = """
ADMIN 만들 있다. expirationPeriod 유효기간을 정하며 기본값은 72시간이다.
원본 토큰은 응답의 inviteUrl 에만 담기고 서버에는 해시로 저장된다.
응답을 잃으면 서버에서 원본 토큰을 복구하거나 같은 링크를 재발급할 없다."""
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/slatto/domain/project/controller/ProjectInvitationController.java`
around lines 34 - 39, Update the description in the `@Operation` annotation for
ProjectInvitationController so it states that users with the inviteUrl can
retrieve the invitation information again, while losing the original token
prevents the server from recovering or reissuing it.

Comment on lines +44 to +65
// 400 은 파싱할 본문이나 파라미터가 있어야 발생한다. 둘 다 없으면 날 수 없다.
if (hasParameters(operation) || operation.getRequestBody() != null) {
addIfAbsent(responses, CommonErrorCode.BAD_REQUEST, badRequestContent());
}

// 401 은 토큰이 필수인 엔드포인트에서만 발생한다.
// 게스트 참여 경로는 토큰이 없어도 통과하기 때문에 여기서 제외된다.
if (EndpointAuthentication.isRequired(handlerMethod)) {
addIfAbsent(responses, CommonErrorCode.UNAUTHORIZED, singleExampleContent(CommonErrorCode.UNAUTHORIZED));
}

// 404 는 경로로 리소스를 찾는 엔드포인트에서만 발생한다.
if (hasPathParameter(operation)) {
addIfAbsent(responses, CommonErrorCode.NOT_FOUND, singleExampleContent(CommonErrorCode.NOT_FOUND));
}

// 500 은 처리되지 않은 예외를 잡는 핸들러가 있어 모든 엔드포인트에서 가능하다.
addIfAbsent(
responses,
CommonErrorCode.INTERNAL_SERVER_ERROR,
singleExampleContent(CommonErrorCode.INTERNAL_SERVER_ERROR)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# multipart 엔드포인트와 413 Swagger 응답 선언을 확인한다.
rg -n -C 6 --type java \
  'MultipartFile|`@RequestPart`|PAYLOAD_TOO_LARGE|responseCode\s*=\s*"413"|`@ApiResponse`' \
  src/main/java

# 생성 OpenAPI 문서의 413 회귀 테스트가 있는지 확인한다.
rg -n -C 5 --type java \
  'path\("413"\)|PAYLOAD_TOO_LARGE|MultipartFile|`@RequestPart`' \
  src/test/java

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 46496


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- SwaggerErrorResponseCustomizer.java ---'
cat -n src/main/java/com/slatto/global/config/SwaggerErrorResponseCustomizer.java

printf '%s\n' '--- OpenApiDocumentationTest.java (relevant sections) ---'
sed -n '1,180p' src/test/java/com/slatto/global/config/OpenApiDocumentationTest.java

printf '%s\n' '--- related response/error helpers and annotations ---'
rg -n -C 8 --type java \
  'class ErrorResponse|record ErrorResponse|ErrorResponse|singleExampleContent|badRequestContent|addIfAbsent|OpenAPI|OperationCustomizer|SwaggerErrorResponseCustomizer|PAYLOAD_TOO_LARGE|MaxUploadSizeExceededException' \
  src/main/java src/test/java

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 50375


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- multipart controller mappings ---'
rg -n -B 8 -A 18 --type java \
  'consumes\s*=\s*MediaType\.MULTIPART_FORM_DATA_VALUE|consumes\s*=\s*"multipart/form-data"|MultipartFile' \
  src/main/java/*/ 2>/dev/null || true
rg -n -B 8 -A 18 --type java \
  'consumes\s*=\s*MediaType\.MULTIPART_FORM_DATA_VALUE|consumes\s*=\s*"multipart/form-data"' \
  src/main/java

printf '%s\n' '--- upload-size configuration ---'
rg -n -C 5 \
  'max-file-size|max-request-size|multipart|MaxUploadSizeExceededException|PAYLOAD_TOO_LARGE' \
  src/main/resources src/main/java src/test/resources 2>/dev/null || true

printf '%s\n' '--- all 413 declarations/assertions ---'
rg -n -C 4 --type java \
  '"413"|path\("413"\)|HttpStatus\.PAYLOAD_TOO_LARGE|PAYLOAD_TOO_LARGE|responseCode\s*=\s*413|responseCode\s*=\s*"413"' \
  src/main/java src/test/java || true

printf '%s\n' '--- OpenAPI test helper and remaining tests ---'
sed -n '180,380p' src/test/java/com/slatto/global/config/OpenApiDocumentationTest.java

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 50375


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

root = Path("src/main/java")
mapping = re.compile(
    r'@(?P<kind>GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)'
    r'\s*(?:\([^)]*\))?',
    re.S
)
for path in sorted(root.rglob("*.java")):
    text = path.read_text()
    if "MULTIPART_FORM_DATA_VALUE" not in text and "multipart/form-data" not in text:
        continue
    print(f"FILE {path}")
    for match in mapping.finditer(text):
        end = min(len(text), match.end() + 500)
        block = text[match.start():end]
        if "MULTIPART_FORM_DATA_VALUE" in block or "multipart/form-data" in block:
            print("  " + " ".join(block.split())[:350])

print("413 references:")
for base in (Path("src/main/java"), Path("src/test/java"), Path("src/main/resources"), Path("src/test/resources")):
    if not base.exists():
        continue
    for path in sorted(base.rglob("*")):
        if path.is_file():
            text = path.read_text(errors="ignore")
            if any(token in text for token in ("PAYLOAD_TOO_LARGE", '"413"', "path(\"413\")", "responseCode = 413")):
                for i, line in enumerate(text.splitlines(), 1):
                    if any(token in line for token in ("PAYLOAD_TOO_LARGE", '"413"', "path(\"413\")", "responseCode = 413")):
                        print(f"{path}:{i}:{line.strip()}")
PY

printf '%s\n' '--- multipart-related resource configuration ---'
rg -n -C 3 -i \
  'multipart|max-file-size|max-request-size' \
  src/main/resources src/test/resources 2>/dev/null || true

printf '%s\n' '--- OpenAPI test method names ---'
rg -n --type java \
  '^(\s*)(`@Test`|void |`@DisplayName`)' \
  src/test/java/com/slatto/global/config/OpenApiDocumentationTest.java

Repository: SLAT-TO/SLATE-TO-BE

Length of output: 5315


multipart 엔드포인트에 HTTP 413 응답을 문서화하십시오.

GlobalExceptionHandler는 업로드 한도 초과를 PAYLOAD_TOO_LARGE(COMMON413)와 HTTP 413으로 반환합니다. SwaggerErrorResponseCustomizer는 413을 등록하지 않으며, OpenApiDocumentationTest에도 검증이 없습니다. 세 multipart 엔드포인트에 ErrorResponse 스키마와 PAYLOAD_TOO_LARGE 예시를 추가하고, 생성된 OpenAPI 문서의 413 응답을 검증하십시오.

📍 Affects 2 files
  • src/main/java/com/slatto/global/config/SwaggerErrorResponseCustomizer.java#L44-L65 (this comment)
  • src/test/java/com/slatto/global/config/OpenApiDocumentationTest.java#L96-L108
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/slatto/global/config/SwaggerErrorResponseCustomizer.java`
around lines 44 - 65, Update SwaggerErrorResponseCustomizer to add HTTP 413 for
multipart endpoints, using the ErrorResponse schema and PAYLOAD_TOO_LARGE
example while preserving existing response conditions. Extend
OpenApiDocumentationTest to verify the generated 413 response for all three
multipart endpoints; apply the customizer change in
src/main/java/com/slatto/global/config/SwaggerErrorResponseCustomizer.java:44-65
and the corresponding assertions in
src/test/java/com/slatto/global/config/OpenApiDocumentationTest.java:96-108.

@Schema(description = "응답 메시지", example = "요청에 성공했습니다.")
private final String message;

@Schema(description = "응답 데이터. 실패 시 null 이며, 검증 실패에서만 필드 오류 목록이 담긴다.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

result의 실패 응답 설명을 실제 형태와 맞추십시오.

검증 실패는 ApiResponse.failure(errorCode, response)를 사용하므로 resultnull이 아닙니다. 현재 설명은 모든 실패 응답의 resultnull인 것처럼 보입니다.

일반 실패에서는 null이며, 검증 실패에서는 필드 오류 목록이 담긴다로 수정하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/slatto/global/response/ApiResponse.java` at line 24, Update
the `@Schema` description on the result field in ApiResponse to state that result
is null for general failures and contains a field-error list for validation
failures, matching ApiResponse.failure(errorCode, response).

nginx -t 가 실패하면 방금 복사한 파일을 rm 으로 지웠다. 서버에 이미
설정이 있었다면 되돌리는 게 아니라 없애는 동작이다.

실행 중인 nginx 는 설정을 메모리에 들고 있어 당장은 멀쩡하다. 문제는
다음 reload 다. certbot 갱신 훅이 reload 를 돌리는 순간 파일이 없으면
client_max_body_size 가 기본값으로 돌아가 업로드가 전부 nginx 단에서
413 이 된다. 배포 실패 시점과 장애 시점이 떨어져 있어 원인 찾기도 어렵다.

덮어쓰기 전에 원본을 남기고, 검사에 실패하면 되돌린다. 원래 파일이
없었을 때만 지운다. 백업은 include 대상이 되지 않도록 conf.d 밖에 둔다.
"같은 링크를 나중에 다시 조회할 수 없다" 고 적었는데 사실과 다르게 읽힌다.
getInvitation 은 받은 토큰을 해시해서 찾으므로 inviteUrl 을 가진 사람은
몇 번이든 다시 조회할 수 있다.

서버가 원본 토큰을 갖고 있지 않다는 뜻이었으므로, 응답을 잃으면 같은
링크를 다시 받을 수 없고 새로 만들어야 한다는 문장으로 바꾼다.
COMMON413 과 전역 핸들러는 추가했는데 문서에는 413 이 어디에도 없었다.
발생하는 실패 응답이 명세에서 빠져 있으면 프론트가 문서만 보고
처리 갈래를 만들 수 없다.

413 은 서블릿 컨테이너가 강제하는 업로드 한도에서 나므로 multipart 본문을
받는 엔드포인트에서만 발생한다. 조건 없이 붙이면 실제로 나지 않는 상태
코드가 실리므로, 400/401/404 와 같은 방식으로 조건부 주입한다.

컨트롤러마다 애노테이션을 붙이지 않는 이유는 같다. 손으로 적은 문서는
코드가 바뀔 때 조용히 낡는다.

붙는 범위가 어긋나면 드러나도록 문서 검증 테스트에 단언을 추가한다.
주입을 끈 상태에서 이 단언이 실패하는 것까지 확인했다.
@chazy-d
chazy-d merged commit b9438b0 into main Aug 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documents 문서 수정 fix 버그 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CHORE: Swagger 문서에 실패 응답·인증 방식 반영

1 participant