diff --git a/src/main/kotlin/com/depromeet/piki/image/controller/dto/PresignedImageUpload.kt b/src/main/kotlin/com/depromeet/piki/image/controller/dto/PresignedImageUpload.kt index dbd717b7..aa84dd2a 100644 --- a/src/main/kotlin/com/depromeet/piki/image/controller/dto/PresignedImageUpload.kt +++ b/src/main/kotlin/com/depromeet/piki/image/controller/dto/PresignedImageUpload.kt @@ -14,7 +14,7 @@ data class PresignedImageUpload( ) val imageKey: String, @field:Schema( - description = "클라가 이미지를 직접 PUT 할 presigned URL (만료 5분)", + description = "클라가 이미지를 직접 PUT 할 presigned URL (짧게 만료되므로 발급 직후 업로드)", example = "https://piki-images.s3.ap-northeast-2.amazonaws.com/items/raw/550e8400-e29b-41d4-a716-446655440000.png?X-Amz-Signature=...", ) val uploadUrl: String, diff --git a/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentApi.kt b/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentApi.kt index 789d1d16..fec3b2b1 100644 --- a/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentApi.kt +++ b/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentApi.kt @@ -47,7 +47,7 @@ interface TournamentApi { playType 파라미터로 솔로/소셜을 나눠 볼 수 있다(SOLO · SOCIAL). 생략 시 전체이며 status 와 AND 로 걸린다 (예: ?status=PENDING&status=IN_PROGRESS&playType=SOCIAL). playType 은 저장된 속성이 아니라 참가 결과로 파생된다 — 혼자면 SOLO, 참여자가 생기면 SOCIAL 이고 - 남의 토너먼트에 참여한 항목은 항상 SOCIAL 이다. 참여는 초대 코드(PENDING)와 플레이 링크(COMPLETED)로 일어나므로, + 남의 토너먼트에 참여한 항목은 항상 SOCIAL 이다. 참여는 초대 코드(PENDING)와 플레이 링크(주최자가 자기 플레이를 완주한 뒤)로 일어나므로, 그 사이 같은 토너먼트가 SOLO 목록에서 SOCIAL 목록으로 옮겨갈 수 있다. 각 항목의 thumbnailUrls 는 카드 대표 썸네일 URL 배열이다 — 최근 등록 아이템 중 이미지가 준비된(READY) 것 최대 2장. 이미지가 준비된 아이템이 없으면 빈 배열이며, limit 유무와 무관하게 항상 포함된다. @@ -245,6 +245,8 @@ interface TournamentApi { 코드가 유효하면 tournamentId·이름·아이템 수·참여자 수와 현재 유저의 참여 여부(joined)를 반환한다. 인증은 선택 — 토큰이 있으면 그 유저 기준 joined 를 계산하고, 없으면 joined=false 로 내린다. 이후 /join 또는 /join/guest 호출 시 응답의 tournamentId를 사용하면 된다. + 코드는 토너먼트 상태에 따라 다른 문을 연다. 대기실(PENDING)이면 합류 미리보기이고, 주최자가 자기 플레이를 + 완주했으면 플레이 생성(`POST /from-play-code`) 미리보기다. 완주한 토너먼트는 공유 링크가 유효해야 보인다. """, ) @ApiResponses( @@ -259,9 +261,14 @@ interface TournamentApi { description = "잘못된 요청 (코드에 해당하는 토너먼트 없음)", content = [Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = Schema(implementation = ApiResponseBody::class))], ), + ApiResponse( + responseCode = "404", + description = "주최자가 완주했지만 공유 링크를 만들지 않음 — code: TOURNAMENT-026", + content = [Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = Schema(implementation = ApiResponseBody::class))], + ), ApiResponse( responseCode = "409", - description = "상태 충돌 (PENDING이 아닌 토너먼트 · 초대 링크 만료)", + description = "상태 충돌 (시작했지만 주최자가 아직 완주하지 않음 · 초대 링크 만료 · 완주한 토너먼트의 공유 링크 만료)", content = [Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = Schema(implementation = ApiResponseBody::class))], ), ], @@ -464,7 +471,11 @@ interface TournamentApi { @Operation( summary = "토너먼트 시작", - description = "PENDING 상태의 토너먼트를 IN_PROGRESS 상태로 전환하고, 참여 아이템 목록을 가격 오름차순으로 정렬해 반환한다.", + description = """ + 참여 아이템 목록을 가격 오름차순으로 정렬해 반환한다. 부르는 사람에 따라 두 경로로 갈린다. + - 주최자: PENDING 토너먼트를 IN_PROGRESS 로 전환하고 자기 플레이를 시작한다. + - 멤버·게스트: 주최자가 시작한 뒤에만 자기 참여 행을 플레이 시작으로 전환한다. 다시 불러도 같은 결과를 돌려준다(멱등). + """, ) @ApiResponses( value = [ @@ -500,7 +511,7 @@ interface TournamentApi { ), ApiResponse( responseCode = "403", - description = "권한 없음 (토너먼트 참여자가 아님 · 토너먼트 소유자가 아님)", + description = "권한 없음 (토너먼트 참여자가 아님)", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -690,7 +701,7 @@ interface TournamentApi { 검증하지 않으므로, 뒤집혀 오거나 순서가 달라도 통과한다. 같은 조합을 같은 승자로 다시 보내면 멱등 성공(중복 기록 없이 200)이고, 다른 승자로 보내면 409 (TOURNAMENT-035) 다. - 결승 매치도 마찬가지다 - 결승을 기록하면 토너먼트가 COMPLETED 로 바뀌지만, 같은 결승 매치를 재전송하면 + 결승 매치도 마찬가지다 - 결승을 기록하면 내 참여가 COMPLETED 로 바뀌지만, 같은 결승 매치를 재전송하면 409 가 아니라 최초 응답과 같은 completed(순위 결과)를 다시 받는다. 응답을 못 받고 재전송하는 경우가 결승에서 가장 흔하다. 응답의 nullable 필드는 `null` 로 내려가지 않고 **키 자체가 생략**된다 (이 API 공통 규약). @@ -785,10 +796,10 @@ interface TournamentApi { - 만료된 링크가 있으면: 새 14일로 갱신한다. 플레이 링크는 **토너먼트당 하나**이고 주소가 `tournamentId` 로 결정된다(별도 토큰 없음) - 같은 - 토너먼트에 여러 개를 만들 수 없다. 링크를 통해 들어오는 인원 제한은 없다. + 토너먼트에 여러 개를 만들 수 없다. 링크로 들어오는 인원도 초대·코드와 같은 정원 제한을 받는다. 초대 코드(시작 전, 참여자로 합류)와는 다른 기능이며 동시에 열리는 구간이 없다 - 초대 코드는 - PENDING 상태에서만, 플레이 링크는 COMPLETED 상태에서만 쓸 수 있다. + PENDING 상태에서만, 플레이 링크는 주최자가 자기 플레이를 완주한 뒤에만 쓸 수 있다. 토너먼트 상태(status)는 주최자 완주 뒤에도 IN_PROGRESS 일 수 있어 상태가 아니라 주최자 참여 행으로 판정한다. """, ) @ApiResponses( @@ -815,7 +826,7 @@ interface TournamentApi { ), ApiResponse( responseCode = "409", - description = "상태 충돌 (COMPLETED가 아닌 토너먼트)", + description = "상태 충돌 (주최자가 아직 자기 플레이를 완주하지 않음)", content = [Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = Schema(implementation = ApiResponseBody::class))], ), ], @@ -870,12 +881,13 @@ interface TournamentApi { "완료된 토너먼트의 6자리 코드로 입장해 내 플레이를 만든다. `from-play-link` 와 같은 일을 하고 **주소만 다르다** " + "- 링크를 열 수 없는 자리(오프라인에서 코드를 불러주는 등)를 위한 경로다.\n\n" + "**코드는 초대 코드와 같은 값이다.** 토너먼트 생성 시점에 발급돼 바뀌지 않으며, 상태에 따라 다른 문을 연다.\n\n" + - "| 대상 상태 | 이 API | 게이트 |\n" + + "| 대상 | 이 API | 게이트 |\n" + "|---|---|---|\n" + "| PENDING | **409** (`TOURNAMENT-023`) — `POST /{id}/join` 을 쓴다 | - |\n" + - "| COMPLETED | 플레이 생성 | 공유 링크가 유효해야 한다 (14일) |\n\n" + + "| 시작했지만 주최자가 아직 완주하지 않음 | **409** (`TOURNAMENT-023`) | - |\n" + + "| 주최자가 완주함 | 플레이 생성 | 공유 링크가 유효해야 한다 (14일) |\n\n" + "**공유를 켜지 않은 토너먼트는 코드로도 열리지 않는다** - 주최자가 공유 링크를 만들지 않았으면 404(`TOURNAMENT-026`). " + - "주최자가 나가 링크가 무효화된 경우도 같은 게이트에 걸린다. 만료 후 재발급하면 코드도 함께 살아난다.\n\n" + + "주최자가 나가 링크가 무효화되면 만료와 같이 409(`TOURNAMENT-027`)다. 만료 후 재발급하면 코드도 함께 살아난다.\n\n" + "**멱등하다.** 이미 참여 중이면 기존 토너먼트 id 를 그대로 200 으로 돌려준다(링크 만료와 무관). " + "그래서 201 이 아니라 200 이다.\n\n" + "**정원은 8명이다**(주최자 포함, 참여 행 기준). 초대·링크·코드가 같은 제한을 공유한다.", @@ -962,7 +974,12 @@ interface TournamentApi { @Operation( summary = "토너먼트 삭제", - description = "토너먼트를 소유자 목록에서 제거한다. 소유자만 호출 가능하며, IN_PROGRESS 상태에서는 불가. 토너먼트 데이터(아이템·히스토리)는 유지되어 다른 참여자들은 계속 접근할 수 있다.", + description = """ + 소유자만 호출 가능하다. 주최자 본인의 플레이 진행에 따라 갈린다. + - 아직 플레이 전: 토너먼트·아이템·참여 행을 모두 지운다. + - 플레이 중: 삭제할 수 없다(409). + - 플레이 완주 뒤: 주최자만 목록에서 빠지고 플레이 링크가 무효화된다. 토너먼트 데이터(아이템·히스토리)는 유지되어 다른 참여자들은 계속 접근할 수 있다. + """, ) @ApiResponses( value = [ @@ -1008,7 +1025,7 @@ interface TournamentApi { ), ApiResponse( responseCode = "409", - description = "상태 충돌 (IN_PROGRESS 토너먼트는 삭제 불가)", + description = "상태 충돌 (주최자 본인 플레이가 진행 중)", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, diff --git a/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentApiExamples.kt b/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentApiExamples.kt index 0b8391a2..c2e865fe 100644 --- a/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentApiExamples.kt +++ b/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentApiExamples.kt @@ -232,7 +232,7 @@ class TournamentApiExamples( name = "살로몬 XT-6", price = 279_000, currency = "USD", - imageUrl = null, + imageUrl = "https://cdn.example.com/items/4.jpg", ), ), ), @@ -317,7 +317,7 @@ class TournamentApiExamples( name = "살로몬 XT-6", price = 279_000, currency = "USD", - imageUrl = null, + imageUrl = "https://cdn.example.com/items/4.jpg", ), ), hasGroupResult = true, @@ -508,7 +508,7 @@ class TournamentApiExamples( name = "살로몬 XT-6", price = 279_000, currency = "KRW", - imageUrl = null, + imageUrl = "https://cdn.example.com/items/4.jpg", status = ItemStatus.READY, ), ), @@ -570,7 +570,7 @@ class TournamentApiExamples( name = "살로몬 XT-6", price = 279_000, currency = "KRW", - imageUrl = null, + imageUrl = "https://cdn.example.com/items/4.jpg", ), ), hasGroupResult = true, @@ -638,6 +638,7 @@ class TournamentApiExamples( ), ) add(TournamentException.notFoundTournament(), name = "토너먼트를 찾을 수 없음") + add(TournamentException.notPendingTournament(), name = "PENDING 상태 아님") add(TournamentException.inviteExpired(), name = "초대 링크 만료") } @@ -657,7 +658,10 @@ class TournamentApiExamples( ), ) add(TournamentException.invalidInviteCode(), name = "코드에 해당하는 토너먼트 없음") + add(TournamentException.playLinkNotCreated(), name = "완주했지만 공유 링크 없음") + add(TournamentException.notPendingTournament(), name = "시작했지만 주최자가 아직 완주하지 않음") add(TournamentException.inviteExpired(), name = "초대 링크 만료") + add(TournamentException.playLinkExpired(), name = "완주한 토너먼트의 공유 링크 만료") } handlerMethod.binds(TournamentController::createPlayLink) -> @@ -939,7 +943,7 @@ class TournamentApiExamples( name = "살로몬 XT-6", price = 279_000, currency = "KRW", - imageUrl = null, + imageUrl = "https://cdn.example.com/items/4.jpg", status = ItemStatus.READY, ), ) diff --git a/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentItemApi.kt b/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentItemApi.kt index 012ab177..f42f4520 100644 --- a/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentItemApi.kt +++ b/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentItemApi.kt @@ -26,7 +26,7 @@ import java.util.UUID // 참여자는 자기가 처음 담는 경우에도 429 를 받을 수 있다. 클라이언트가 그 상황을 오해하지 않게 명시한다. // (응답 문구 자체는 오너의 사용량을 드러내지 않는다 — 남의 사용량은 요청자에게 알릴 정보가 아니다.) private const val TOURNAMENT_RATE_LIMIT_DESCRIPTION = - "아이템 등록 한도 초과 (code: TOURNAMENT-037). 한도는 요청자가 아니라 토너먼트 오너의 몫에서 차감되므로 " + + "아이템 등록 한도 초과 (code: ITEM-006). 한도는 요청자가 아니라 토너먼트 오너의 몫에서 차감되므로 " + "참여자가 처음 담는 경우에도 받을 수 있다. 그 몫은 토너먼트 전용이 아니라 오너 계정 하나의 몫이라 " + "오너의 위시 등록과도 공유된다. 요청 수가 아니라 등록하는 item 수로 세며(이미지 5장 = 5), " + "남은 몫이 있으면 그보다 큰 요청도 통과하므로 이 응답은 몫을 이미 다 쓴 뒤부터 나온다. " + @@ -113,7 +113,6 @@ interface TournamentItemApi { summary = "위시에서 토너먼트 아이템 추가", description = """ PENDING 상태의 토너먼트에 위시리스트에 있는 아이템을 추가한다. 토너먼트 참여자(회원)만 호출 가능. - 플레이 링크로 생성된 복제 토너먼트에는 추가 불가. itemIds 중 하나라도 조건에 맞지 않으면 요청 전체가 실패한다(부분 성공 없음). 응답의 tournamentItemIds 는 요청 itemIds 와 동일한 순서로 대응된다. """, @@ -152,7 +151,7 @@ interface TournamentItemApi { ), ApiResponse( responseCode = "403", - description = "권한 없음 (토너먼트 참여자가 아님 · 위시리스트에 없는 아이템 포함 · 플레이 링크로 생성된 복제 토너먼트)", + description = "권한 없음 (토너먼트 참여자가 아님 · 위시리스트에 없는 아이템 포함 · 옛 클론 토너먼트 id 로 요청)", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -194,7 +193,7 @@ interface TournamentItemApi { summary = "URL 링크로 토너먼트 아이템 추가", description = """ PENDING 상태의 토너먼트에 URL 링크를 통해 아이템을 추가한다. - 플레이 링크로 생성된 복제 토너먼트에는 추가 불가. 토너먼트 참여자만 추가할 수 있다. + 토너먼트 참여자만 추가할 수 있다. 아이템이 PENDING 상태로 즉시 생성되어 tournamentItemId 가 반환된다. 파싱은 비동기로 진행되며, 디스패처가 PENDING 을 집어 PROCESSING 으로 전이한 뒤 READY 또는 FAILED 상태로 전환된다. 클라이언트는 SSE(`/api/v1/notifications/subscribe`)로 파싱 완료·실패를 통보받고, tournamentItemId 로 GET /tournaments/{id}/items/{tournamentItemId} 를 조회해 결과를 확인한다. @@ -237,7 +236,7 @@ interface TournamentItemApi { ), ApiResponse( responseCode = "403", - description = "권한 없음 (토너먼트 참여자가 아님 · 플레이 링크로 생성된 복제 토너먼트)", + description = "권한 없음 (토너먼트 참여자가 아님 · 옛 클론 토너먼트 id 로 요청)", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -314,7 +313,7 @@ interface TournamentItemApi { description = """ 이미지 등록의 1단계. 올릴 이미지들의 content-type·바이트 수(1~5개)를 받아, 클라가 S3 에 직접 PUT 할 presigned URL 을 발급한다. 원본 바이트가 서버를 경유하지 않아 서버 대역·메모리를 쓰지 않는다. - 참여자·PENDING·비복제 권한을 사전 검증하며, 정원(최대 32개) 최종 판정은 저장이 일어나는 2단계(/images/confirm)로 미룬다. + 참여자·PENDING 권한을 사전 검증하며, 정원(최대 32개) 최종 판정은 저장이 일어나는 2단계(/images/confirm)로 미룬다. content-type 과 contentLength 는 서명에 묶인다 — 클라는 각 uploadUrl 로 같은 Content-Type·Content-Length 로만 PUT 할 수 있고(S3 가 강제), imageKey 들을 confirm 으로 되돌려준다. 한 장의 상한은 5MB 다. """, @@ -356,7 +355,7 @@ interface TournamentItemApi { ), ApiResponse( responseCode = "403", - description = "권한 없음 (토너먼트 참여자가 아님 · 플레이 링크로 생성된 복제 토너먼트)", + description = "권한 없음 (토너먼트 참여자가 아님 · 옛 클론 토너먼트 id 로 요청)", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -480,7 +479,7 @@ interface TournamentItemApi { ), ApiResponse( responseCode = "403", - description = "권한 없음 (토너먼트 참여자가 아님 · 플레이 링크로 생성된 복제 토너먼트)", + description = "권한 없음 (토너먼트 참여자가 아님 · 옛 클론 토너먼트 id 로 요청)", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -555,7 +554,8 @@ interface TournamentItemApi { "잘못된 요청 (이름 없이 수정 시도 — code: ITEM-003 · 가격 없이 READY 전환 — code: ITEM-004 · " + "이미지 없이 READY 전환 — code: ITEM-005 · 이름 1자 미만 512자 초과 · 가격 음수 · 가격단위 8자 초과 · " + "빈 이미지 — code: PRODUCTIMAGE-001 · 이미지 타입 미지정 — code: PRODUCTIMAGE-002 · " + - "지원하지 않는 이미지 형식(png/jpeg/webp/heic/heif만 허용) — code: PRODUCTIMAGE-003)", + "지원하지 않는 이미지 형식(png/jpeg/webp/heic/heif만 허용) — code: PRODUCTIMAGE-003 · " + + "요청 값 형식 검증 실패 — COMMON-INVALID-INPUT)", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -575,7 +575,7 @@ interface TournamentItemApi { ), ApiResponse( responseCode = "403", - description = "권한 없음 (토너먼트 참여자가 아님 · 아이템을 등록한 본인이 아님 · 플레이 링크로 만든 클론 토너먼트에서는 아이템 수정 불가)", + description = "권한 없음 (토너먼트 참여자가 아님 · 아이템을 등록한 본인이 아님 · 옛 클론 토너먼트 id 로 요청)", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, @@ -604,20 +604,6 @@ interface TournamentItemApi { ), ], ), - ApiResponse( - responseCode = "400", - description = - "잘못된 요청 (빈 이미지 파일 — code: PRODUCTIMAGE-001 · " + - "이미지 형식 확인 불가 — PRODUCTIMAGE-002 · " + - "지원하지 않는 이미지 형식(png/jpeg/webp/heic/heif 만) — PRODUCTIMAGE-003 · " + - "요청 값 형식 검증 실패 — COMMON-INVALID-INPUT)", - content = [ - Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = Schema(implementation = ApiResponseBody::class), - ), - ], - ), ApiResponse( responseCode = "502", description = "외부 의존성 실패 (이미지 저장소(S3) 업로드 실패 — 재시도 가능) — code: STORAGE-001", @@ -665,7 +651,7 @@ interface TournamentItemApi { ), ApiResponse( responseCode = "403", - description = "권한 없음 (아이템을 추가한 본인도 아니고 토너먼트 소유자도 아님 · 플레이 링크로 만든 클론 토너먼트에서는 아이템 삭제 불가)", + description = "권한 없음 (아이템을 추가한 본인도 아니고 토너먼트 소유자도 아님 · 옛 클론 토너먼트 id 로 요청)", content = [ Content( mediaType = MediaType.APPLICATION_JSON_VALUE, diff --git a/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentItemApiExamples.kt b/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentItemApiExamples.kt index 1165fe09..d6f7db15 100644 --- a/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentItemApiExamples.kt +++ b/src/main/kotlin/com/depromeet/piki/tournament/controller/TournamentItemApiExamples.kt @@ -63,7 +63,7 @@ class TournamentItemApiExamples( add(TournamentException.tooManyTournamentItems(), name = "아이템 최대 32개 초과") unauthorized() add(TournamentException.forbiddenTournament(), name = "토너먼트 권한 없음") - add(TournamentException.clonedTournamentCannotAddItems(), name = "플레이링크 복제 토너먼트에는 아이템 추가 불가") + add(TournamentException.clonedTournamentCannotAddItems(), name = "옛 클론 토너먼트 id 로 요청 (아이템 추가 불가)") add(TournamentException.itemNotInWishlist(), name = "위시리스트에 없는 아이템 포함") add(TournamentException.notFoundTournament(), name = "토너먼트를 찾을 수 없음") add(TournamentException.notFoundItems(), name = "존재하지 않는 아이템 포함") @@ -90,7 +90,7 @@ class TournamentItemApiExamples( add(TournamentException.tooManyTournamentItems(), name = "아이템 최대 32개 초과") unauthorized() add(TournamentException.forbiddenTournament(), name = "토너먼트 권한 없음") - add(TournamentException.clonedTournamentCannotAddItems(), name = "플레이링크 복제 토너먼트에는 아이템 추가 불가") + add(TournamentException.clonedTournamentCannotAddItems(), name = "옛 클론 토너먼트 id 로 요청 (아이템 추가 불가)") add(TournamentException.notFoundTournament(), name = "토너먼트를 찾을 수 없음") add(TournamentException.notPendingTournament(), name = "PENDING 상태 아님") // 단건 경로라 겹친 아이템을 특정할 수 있다 — data 로 그 tournament_item id 를 함께 내린다(#973). @@ -113,12 +113,13 @@ class TournamentItemApiExamples( payload = ApiResponseBody.ok(presignedUploadsSample), ) add(TournamentException.invalidImageCount(), name = "이미지 개수 위반 (1~5개)") + add(ProductImageException.unknownType(), name = "content-type 미지정") add(ProductImageException.unsupportedType(), name = "지원하지 않는 이미지 형식") add(ImageUploadException.tooLarge(), name = "contentLength 5MB 초과") add(ImageUploadException.invalidSize(), name = "contentLength 미지정 또는 0 이하") unauthorized() add(TournamentException.forbiddenTournament(), name = "토너먼트 권한 없음") - add(TournamentException.clonedTournamentCannotAddItems(), name = "플레이링크 복제 토너먼트에는 아이템 추가 불가") + add(TournamentException.clonedTournamentCannotAddItems(), name = "옛 클론 토너먼트 id 로 요청 (아이템 추가 불가)") add(TournamentException.notFoundTournament(), name = "토너먼트를 찾을 수 없음") add(TournamentException.notPendingTournament(), name = "PENDING 상태 아님") add(ImageStorageException.presignFailed(), name = "presigned URL 발급 실패 (스토리지 장애)") @@ -144,7 +145,7 @@ class TournamentItemApiExamples( add(TournamentException.tooManyTournamentItems(), name = "아이템 최대 32개 초과") unauthorized() add(TournamentException.forbiddenTournament(), name = "토너먼트 권한 없음") - add(TournamentException.clonedTournamentCannotAddItems(), name = "플레이링크 복제 토너먼트에는 아이템 추가 불가") + add(TournamentException.clonedTournamentCannotAddItems(), name = "옛 클론 토너먼트 id 로 요청 (아이템 추가 불가)") add(TournamentException.notFoundTournament(), name = "토너먼트를 찾을 수 없음") add(TournamentException.notPendingTournament(), name = "PENDING 상태 아님") add(ImageStorageException.existsCheckFailed(), name = "이미지 존재 확인 실패 (스토리지 장애)") @@ -166,7 +167,7 @@ class TournamentItemApiExamples( add(ProductImageException.unsupportedType(), name = "지원하지 않는 이미지 형식") unauthorized() add(TournamentException.forbiddenTournament(), name = "토너먼트 권한 없음") - add(TournamentException.clonedTournamentCannotModifyItems(), name = "플레이 링크 클론은 아이템 수정 불가") + add(TournamentException.clonedTournamentCannotModifyItems(), name = "옛 클론 토너먼트 id 로 요청 (아이템 수정 불가)") add(TournamentException.notFoundTournament(), name = "토너먼트를 찾을 수 없음") add(TournamentException.notFoundTournamentItem(), name = "토너먼트 아이템을 찾을 수 없음") add(TournamentException.notPendingTournament(), name = "PENDING 상태 아님") @@ -182,7 +183,7 @@ class TournamentItemApiExamples( ) unauthorized() add(TournamentException.forbiddenTournament(), name = "토너먼트 권한 없음") - add(TournamentException.clonedTournamentCannotModifyItems(), name = "플레이 링크 클론은 아이템 삭제 불가") + add(TournamentException.clonedTournamentCannotModifyItems(), name = "옛 클론 토너먼트 id 로 요청 (아이템 삭제 불가)") add(TournamentException.notFoundTournament(), name = "토너먼트를 찾을 수 없음") add(TournamentException.notFoundTournamentItem(), name = "토너먼트 아이템을 찾을 수 없음") add(TournamentException.notPendingTournament(), name = "PENDING 상태 아님") diff --git a/src/main/kotlin/com/depromeet/piki/tournament/controller/dto/TournamentSummaryResponse.kt b/src/main/kotlin/com/depromeet/piki/tournament/controller/dto/TournamentSummaryResponse.kt index ee65401e..388de4eb 100644 --- a/src/main/kotlin/com/depromeet/piki/tournament/controller/dto/TournamentSummaryResponse.kt +++ b/src/main/kotlin/com/depromeet/piki/tournament/controller/dto/TournamentSummaryResponse.kt @@ -16,7 +16,7 @@ data class TournamentSummaryResponse( val participantCount: Int, // 카드의 "플레이한 N" — 플레이를 끝까지 마친 사람 수(완주 기준). 아직 아무도 완주 안 했으면 0. val playedCount: Int, - // 카드 대표 썸네일 URL — 최근 등록 아이템 중 이미지 있는 것 최대 2장 (없으면 빈 배열). + // 카드 대표 썸네일 URL — 최근 등록 아이템 중 READY 인 것의 이미지 최대 2장 (없으면 빈 배열). val thumbnailUrls: List, ) { companion object { diff --git a/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistApi.kt b/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistApi.kt index c0ba0cd0..cad391d1 100644 --- a/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistApi.kt +++ b/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistApi.kt @@ -26,7 +26,7 @@ import java.util.UUID // 세는 단위가 요청 수가 아니라 item 수라는 점, 그리고 몫이 위시 전용이 아니라 계정 전체 몫이라는 점이 // 클라이언트 계약의 핵심이라 명시한다. private const val RATE_LIMIT_DESCRIPTION = - "아이템 등록 한도 초과 (code: WISH-010). 몫은 위시 전용이 아니라 계정 하나의 몫이라, 이 사용자가 소유한 " + + "아이템 등록 한도 초과 (code: ITEM-006). 몫은 위시 전용이 아니라 계정 하나의 몫이라, 이 사용자가 소유한 " + "토너먼트에 아이템이 추가된 양(참여 게스트가 넣은 것 포함)도 같은 몫에서 빠진다. 한도는 요청 수가 아니라 " + "등록하는 item 수로 센다 — 이미지 5장 등록은 5 를 소모하고, 새로고침도 파싱이 다시 돌므로 1 을 소모한다. " + "남은 몫이 있으면 그보다 큰 요청도 통과하므로(마지막 한 번은 성공) 이 응답은 몫을 이미 다 쓴 뒤부터 나온다. " + @@ -347,7 +347,7 @@ interface WishlistApi { description = "잘못된 요청 (보정 후에도 상품명 없음 — code: ITEM-003 · 가격 없이 READY 전환 — code: ITEM-004 · " + "이미지 없이 READY 전환 — code: ITEM-005 · price 음수 · name/currency/memo 길이 초과 · " + - "빈 이미지 — code: PRODUCTIMAGE-001 · " + + "빈 이미지 — code: PRODUCTIMAGE-001 · 이미지 Content-Type 없음 — code: PRODUCTIMAGE-002 · " + "지원하지 않는 이미지 형식(png/jpeg/webp/heic/heif만 허용) — code: PRODUCTIMAGE-003)", content = [ Content( diff --git a/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistApiExamples.kt b/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistApiExamples.kt index 8a1df0e4..8f6788b3 100644 --- a/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistApiExamples.kt +++ b/src/main/kotlin/com/depromeet/piki/wishlist/controller/WishlistApiExamples.kt @@ -231,6 +231,7 @@ class WishlistApiExamples( payload = ApiResponseBody.ok(presignedUploadsSample), ) add(WishException.invalidImageCount(), name = "이미지 개수 위반 (1~5개 아님)") + add(ProductImageException.unknownType(), name = "content-type 미지정") add(ProductImageException.unsupportedType(), name = "지원하지 않는 이미지 형식") add(ImageUploadException.tooLarge(), name = "contentLength 5MB 초과") add(ImageUploadException.invalidSize(), name = "contentLength 미지정 또는 0 이하")