{"openapi":"3.1.0","info":{"title":"PIKI API","description":"피키(PIKI) 위시리스트 서비스 API 문서.\n\n- 모든 응답은 공통 래퍼(`ApiResponseBody`)로 `application/json` 으로 내려간다. 필드는 `status`·`code`·`detail`·`data`·`pageResponse` 이며, 성공·실패가 같은 형태다. 실패 시 `data` 는 null 이고 `code`·`detail` 에 사유가 담긴다.\n- 인증은 JWT 기반이다. 게스트 생성 또는 소셜 로그인으로 액세스·리프레시 토큰 쌍을 발급받고, 보호된 API 는 액세스 토큰을 `Authorization: Bearer {accessToken}` 헤더로 전달한다. 만료 시 리프레시 토큰으로 갱신한다.","contact":{"name":"PIKI","url":"https://github.com/depromeet/PIKI-Server"},"license":{"name":"Apache-2.0","url":"https://www.apache.org/licenses/LICENSE-2.0"},"version":"v1"},"servers":[{"url":"/","description":"Current host"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Auth","description":"인증 API"},{"name":"User","description":"유저 API"},{"name":"Wishlist","description":"위시리스트 등록/조회/복구/삭제 API"},{"name":"Tournament","description":"토너먼트 API"},{"name":"Tournament Item","description":"토너먼트 아이템 API"},{"name":"Notification","description":"알림 API"},{"name":"FCM","description":"FCM 푸시 토큰 API"},{"name":"Dev","description":"개발·테스트 전용 API (운영 환경 비활성화)"}],"paths":{"/api/v1/wishlists":{"get":{"tags":["Wishlist"],"summary":"위시리스트 조회 (다건)","description":"\n            로그인한 유저 본인의 위시리스트를 최신 등록순(id desc)으로 조회한다.\n            cursor 페이지네이션: 직전 응답의 pageResponse.nextCursor 를 다음 요청 cursor 로 그대로 전달한다.\n            마지막 페이지면 nextCursor 는 null, hasNext 는 false.\n            size 는 미지정 시 20, 1~50 범위를 벗어나면 양 끝으로 보정된다.\n            각 항목의 item.status 로 파싱 상태(PENDING/PROCESSING/READY/FAILED)를 구분한다 —\n            등록 직후 PENDING·PROCESSING 인 항목은 이 조회를 폴링해 READY/FAILED 로 전이되는지 확인한다.\n        ","operationId":"getWishlist","parameters":[{"name":"cursor","in":"query","description":"직전 응답의 nextCursor (없으면 첫 페이지)","required":false,"schema":{"type":"string"},"example":1010},{"name":"size","in":"query","description":"페이지 크기 (기본 20, 최대 50)","required":false,"schema":{"type":"integer","format":"int32"},"example":20}],"responses":{"200":{"description":"위시리스트 조회 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"조회 성공 (대기·담는 중 + 완성 혼재, 마지막 페이지)":{"value":{"data":[{"wish":{"id":1027,"createdAt":"2026-05-21T10:11:00Z"},"item":{"id":515,"status":"PENDING","name":null,"currentPrice":null,"currency":null,"imageUrl":null,"sourceUrl":"https://www.example-shop.com/products/67891"}},{"wish":{"id":1026,"createdAt":"2026-05-21T10:10:00Z"},"item":{"id":514,"status":"PROCESSING","name":null,"currentPrice":null,"currency":null,"imageUrl":null,"sourceUrl":"https://www.example-shop.com/products/67890"}},{"wish":{"id":1024,"createdAt":"2026-05-21T10:00:00Z"},"item":{"id":512,"status":"READY","name":"에어 조던 1 미드","currentPrice":119000,"currency":"KRW","imageUrl":"https://cdn.example.com/p/512.jpg","sourceUrl":"https://www.example-shop.com/products/12345"}}],"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"조회 성공 (다음 페이지 있음)":{"value":{"data":[{"wish":{"id":1024,"createdAt":"2026-05-21T10:00:00Z"},"item":{"id":512,"status":"READY","name":"에어 조던 1 미드","currentPrice":119000,"currency":"KRW","imageUrl":"https://cdn.example.com/p/512.jpg","sourceUrl":"https://www.example-shop.com/products/12345"}}],"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":"1024","hasNext":true}}},"빈 위시리스트":{"value":{"data":[],"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"유효하지 않은 cursor 값 (숫자로 변환 불가)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유효하지 않은 cursor":{"value":{"data":null,"detail":"유효하지 않은 cursor 입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (GUEST 권한으로 접근 불가 · MEMBER 필요)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"권한 없음 (MEMBER 필요)":{"value":{"data":null,"detail":"권한 없음 — 접근 불가","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}},"post":{"tags":["Wishlist"],"summary":"위시리스트 등록 (URL)","description":"\n            상품 페이지 URL 을 받아 위시리스트에 등록한다. 메타데이터(이름/가격/이미지) 추출은 외부 LLM 호출이라\n            오래 걸리므로 동기로 기다리지 않는다. 등록 즉시 item.status=PENDING 인 항목을 201 로 반환하고,\n            실제 파싱은 백그라운드 디스패처가 PENDING 을 집어 PROCESSING 으로 전이한 뒤 READY(완료) 또는 FAILED(파싱 실패) 로 전이한다.\n            클라이언트는 위시리스트 조회를 폴링해 status 변화(PENDING→PROCESSING→READY/FAILED)를 확인한다. URL 형식 오류는 등록 전에 400 으로 거른다.\n        ","operationId":"registerFromUrl","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WishlistRegisterRequest"}}},"required":true},"responses":{"201":{"description":"위시리스트 등록 접수 (item.status=PENDING, 파싱은 백그라운드)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"등록 접수 (파싱 대기 — PENDING)":{"value":{"data":{"wish":{"id":1027,"createdAt":"2026-05-21T10:11:00Z"},"item":{"id":515,"status":"PENDING","name":null,"currentPrice":null,"currency":null,"imageUrl":null,"sourceUrl":"https://www.example-shop.com/products/67891"}},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (URL 이 비어 있음 · 유효한 URL 형식이 아님 · https 외 스킴)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유효하지 않은 URL 형식":{"value":{"data":null,"detail":"유효한 URL 형식이 아닙니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"https 외 스킴":{"value":{"data":null,"detail":"https URL만 허용합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (GUEST 권한으로 접근 불가 · MEMBER 필요)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"권한 없음 (MEMBER 필요)":{"value":{"data":null,"detail":"권한 없음 — 접근 불가","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}},"delete":{"tags":["Wishlist"],"summary":"위시리스트 삭제 (다건)","description":"\n            위시 항목 여러 개를 한 번에 멱등 삭제한다(soft delete). 요청 목록 중 없거나 이미 삭제된 id 는\n            무시하고(목표 상태 달성) 성공으로 처리한다. 단 존재하는 항목 중 본인 소유가 아닌 위시가 하나라도\n            섞이면 소유권 경계로 403 을 주고 아무것도 삭제하지 않는다. 중복 ID 는 무시한다.\n            삭제된 항목은 조회 결과에서 제외된다.\n            id 목록은 query param 으로 받는다(예: ?ids=1024,1025,1026, 1~100개). DELETE + body 는\n            중간자(게이트웨이·LB·CDN)가 body 를 스트립/거절할 수 있어 피한다.\n        ","operationId":"deleteWishes","parameters":[{"name":"ids","in":"query","description":"삭제할 위시 ID 목록 (쉼표 구분, 1~100개)","required":false,"schema":{"type":"array","items":{"type":"integer","format":"int64"}},"example":"1024,1025,1026"}],"responses":{"200":{"description":"삭제 성공 (없거나 이미 삭제된 항목은 무시, data 없음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"다중 삭제 성공":{"value":{"data":null,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (ids 가 비어 있음 · 누락 · 100개 초과)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"ids 누락/빈 목록/100개 초과":{"value":{"data":null,"detail":"삭제할 위시 ID 는 1개 이상 100개 이하여야 합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (GUEST 권한으로 접근 불가 · MEMBER 필요, 또는 목록에 본인 위시가 아닌 항목이 섞여 있음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"본인 위시 아닌 항목 포함":{"value":{"data":null,"detail":"해당 위시 아이템에 접근할 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/wishlists/images":{"post":{"tags":["Wishlist"],"summary":"위시리스트 등록 (이미지)","description":"\n            상품 페이지를 캡처한 이미지 1~5장을 받아, 각 이미지를 PROCESSING 상태의 위시 항목으로 즉시 등록하고 목록을 반환한다.\n            실제 상품 정보 추출(Gemini Vision)은 백그라운드에서 비동기로 진행되어 각 항목을 READY 또는 FAILED 로 전이시킨다.\n            URL 등록과 결과 모양(WishItemResponse)이 같다. 이미지 등록 항목은 URL 이 없어 sourceUrl 이 null 이며,\n            추출 결과는 조회로 폴링하며, 추출 실패(FAILED) 항목은 보정 API(PATCH)로 직접 채워 복구한다.\n        ","operationId":"registerFromImages","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"images":{"type":"array","items":{"type":"string","format":"binary"}}}}}}},"responses":{"201":{"description":"이미지 등록 접수 — 각 항목이 PROCESSING 상태로 생성되고 비동기 파싱이 시작된다","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"이미지 등록 접수 (PROCESSING, 다건)":{"value":{"data":[{"wish":{"id":1025,"createdAt":"2026-05-21T10:05:00Z"},"item":{"id":513,"status":"PROCESSING","name":null,"currentPrice":null,"currency":null,"imageUrl":null,"sourceUrl":null}},{"wish":{"id":1027,"createdAt":"2026-05-21T10:05:00Z"},"item":{"id":515,"status":"PROCESSING","name":null,"currentPrice":null,"currency":null,"imageUrl":null,"sourceUrl":null}}],"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (이미지 개수 1~5 위반 · 빈 이미지 · 이미지 타입 미지정 · 지원하지 않는 이미지 형식(png/jpeg/webp/heic/heif만 허용))","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"이미지 개수 위반 (1~5개 아님)":{"value":{"data":null,"detail":"이미지는 최소 1개, 최대 5개까지 전송할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"지원하지 않는 이미지 형식":{"value":{"data":null,"detail":"지원하지 않는 이미지 형식입니다: image/gif (지원: image/png, image/jpeg, image/webp, image/heic, image/heif)","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (GUEST 권한으로 접근 불가 · MEMBER 필요)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"권한 없음 (MEMBER 필요)":{"value":{"data":null,"detail":"권한 없음 — 접근 불가","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/tournaments":{"get":{"tags":["Tournament"],"summary":"토너먼트 목록 조회","description":"\n            내 토너먼트 목록을 최근 생성 순으로 조회한다.\n            status 파라미터로 상태 필터링 가능하며 여러 값을 중복 전달할 수 있다(예: ?status=PENDING&status=IN_PROGRESS).\n            생략 시 전체 반환. status 값은 대문자(PENDING/IN_PROGRESS/COMPLETED)로 전달해야 한다.\n        ","operationId":"getTournaments","parameters":[{"name":"status","in":"query","description":"상태 필터 (복수 전달 가능, 생략 시 전체)","required":false,"schema":{"type":"array","items":{"type":"string","enum":["PENDING","IN_PROGRESS","COMPLETED"]}},"example":"PENDING"}],"responses":{"200":{"description":"목록 조회 성공 (참여 토너먼트 없으면 빈 배열 반환)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"목록 조회 성공":{"value":{"data":[{"tournamentId":1,"name":"내 토너먼트","status":"PENDING","createdAt":"2026-05-22T12:00:00Z","participantProfileImages":["https://cdn.example.com/profiles/user1.jpg","https://cdn.example.com/profiles/user2.jpg"]}],"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}},"post":{"tags":["Tournament"],"summary":"토너먼트 생성","description":"\n            이름으로 PENDING 상태의 토너먼트를 생성한다.\n            응답의 inviteCode 와 inviteExpiresAt 을 친구에게 공유하면 초대 링크가 만료되기 전까지 친구가 참여할 수 있다.\n            inviteDurationMinutes 를 생략하면 기본 30분, 최대 1440분(24시간)까지 1분 단위로 설정 가능하다.\n        ","operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTournamentRequest"}}},"required":true},"responses":{"201":{"description":"토너먼트 생성 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"생성 성공":{"value":{"data":{"tournamentId":1,"inviteCode":"ABC123","inviteExpiresAt":"2026-05-30T15:00:00Z"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (name 미입력)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}}}}},"/api/v1/tournaments/{tournamentId}/start":{"post":{"tags":["Tournament"],"summary":"토너먼트 시작","description":"PENDING 상태의 토너먼트를 IN_PROGRESS 상태로 전환하고, 참여 아이템 목록을 가격 오름차순으로 정렬해 반환한다.","operationId":"start","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"responses":{"200":{"description":"토너먼트 시작 성공 (가격 오름차순 정렬 아이템 목록 반환)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"시작 성공":{"value":{"data":{"tournamentId":1,"items":[{"tournamentItemId":1,"name":"나이키 에어맥스","price":129000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/1.jpg"},{"tournamentItemId":2,"name":"아디다스 울트라부스트","price":189000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/2.jpg"},{"tournamentItemId":3,"name":"뉴발란스 993","price":259000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/3.jpg"},{"tournamentItemId":4,"name":"살로몬 XT-6","price":279000,"currency":"USD","imageUrl":null}]},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"아이템 수 미충족 (최소 2개, 최대 32개)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"아이템 수 미충족 (2~32개)":{"value":{"data":null,"detail":"토너먼트 아이템은 최소 2개, 최대 32개여야 합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님 · 토너먼트 소유자가 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트 권한 없음":{"value":{"data":null,"detail":"해당 토너먼트에 대한 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음 · 존재하지 않는 아이템 포함","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (PENDING이 아닌 토너먼트 · PROCESSING/FAILED 상품 포함 · 가격 정보 없는 상품 포함)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"PENDING 상태 아님":{"value":{"data":null,"detail":"PENDING 상태인 토너먼트에만 수행할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/tournaments/{tournamentId}/play-link":{"post":{"tags":["Tournament"],"summary":"플레이 링크 생성","description":"\n            완료된 토너먼트의 플레이 링크를 생성한다. 토너먼트 소유자만 호출 가능.\n            플레이 링크를 통해 친구들이 동일한 아이템 구성으로 토너먼트를 진행할 수 있다.\n            만료 기간은 생성 시점 + 14일로 고정이며 변경 불가.\n            이미 링크가 생성된 경우 409.\n        ","operationId":"createPlayLink","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"responses":{"200":{"description":"플레이 링크 생성 성공 (playLinkExpiresAt 반환)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"플레이 링크 생성 성공":{"value":{"data":"2026-06-09T22:00:00Z","detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님 · 소유자가 아님 · 플레이 링크로 참여한 토너먼트는 플레이 링크 생성 불가)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"플레이 링크로 참여한 토너먼트 (재공유 불가)":{"value":{"data":null,"detail":"플레이 링크로 참여한 토너먼트는 플레이 링크를 생성할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}},"409":{"description":"상태 충돌 (COMPLETED가 아닌 토너먼트 · 플레이 링크가 이미 생성됨)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"플레이 링크 이미 생성됨":{"value":{"data":null,"detail":"플레이 링크가 이미 생성된 토너먼트입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/tournaments/{tournamentId}/matches":{"post":{"tags":["Tournament"],"summary":"매치 결과 기록","description":"\n            IN_PROGRESS 상태의 토너먼트에서 한 매치의 결과(승자)를 기록한다.\n            currentRound 는 해당 시점에 서버가 기대하는 라운드와 일치해야 한다.\n            결승(currentRound=2) 결과 기록 시 본인의 순위 결과(1위~최대 4위)가 즉시 반환된다.\n            소셜 토너먼트라도 각 인스턴스(ROOT·CLONE)는 해당 인스턴스의 결승이 완료되는 즉시 COMPLETED 로 전환된다.\n            다른 참여자의 진행 여부와 무관하게 내 결과는 바로 확인할 수 있으며, 전체 그룹 결과는 2명 이상이 완료한 뒤 hasGroupResult=true 로 활성화된다.\n            결승이 아닌 라운드는 data=null 을 반환한다.\n        ","operationId":"recordMatch","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordMatchRequest"}}},"required":true},"responses":{"200":{"description":"매치 결과 기록 성공 (결승이 아닌 라운드: data=null · 결승 라운드: data.result에 순위 아이템 목록)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"기록 성공 (결승 아닌 라운드) — data=null":{"value":{"data":null,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"기록 성공 (결승 라운드) — 순위 결과 포함":{"value":{"data":{"result":[{"rank":1,"tournamentItemId":1,"itemId":10,"name":"나이키 에어맥스","price":129000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/1.jpg"},{"rank":2,"tournamentItemId":2,"itemId":20,"name":"아디다스 울트라부스트","price":189000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/2.jpg"},{"rank":3,"tournamentItemId":3,"itemId":30,"name":"뉴발란스 993","price":259000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/3.jpg"},{"rank":4,"tournamentItemId":4,"itemId":40,"name":"살로몬 XT-6","price":279000,"currency":"USD"}],"hasGroupResult":true,"playLinkExpiresAt":"2026-06-20T22:00:00Z"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (승자가 대결 두 아이템 중 하나가 아님 · 해당 토너먼트에 속하지 않는 아이템 · 현재 진행해야 할 라운드가 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"승자가 대결 아이템이 아님":{"value":{"data":null,"detail":"승자는 대결한 두 아이템 중 하나여야 합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트 권한 없음":{"value":{"data":null,"detail":"해당 토너먼트에 대한 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (IN_PROGRESS가 아닌 토너먼트 · 이미 탈락한 아이템)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"진행 중 토너먼트 아님":{"value":{"data":null,"detail":"진행 중인 토너먼트에만 수행할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/tournaments/{tournamentId}/join":{"post":{"tags":["Tournament"],"summary":"소셜 토너먼트 참여 (인증된 사용자)","description":"\n            PENDING 상태의 토너먼트에 참여한다. JWT 인증이 필요하다 (GUEST·MEMBER 모두 허용).\n            - 링크 직접 접근: inviteCode 생략 가능. 만료 여부만 확인 후 바로 참여.\n            - 코드 입력 경로: inviteCode 전달 시 코드 일치 여부도 검증.\n            초대 링크가 만료됐거나 이미 참여 중이면 실패한다.\n            계정이 없는 비회원은 /join/guest 를 사용한다.\n        ","operationId":"join","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JoinTournamentRequest"}}},"required":true},"responses":{"200":{"description":"참여 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"참여 성공":{"value":{"data":null,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (초대 코드 형식 오류 · 코드 불일치 — inviteCode 전달 시에만 발생)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}},"404":{"description":"토너먼트를 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}},"409":{"description":"상태 충돌 (PENDING이 아닌 토너먼트 · 초대 링크 만료 · 이미 참여 중 · 참여 인원 초과(최대 8명))","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}}}}},"/api/v1/tournaments/{tournamentId}/join/guest":{"post":{"tags":["Tournament"],"summary":"소셜 토너먼트 참여 (비회원 게스트)","description":"\n            닉네임을 입력해 게스트 계정을 생성하고 토너먼트에 참여한다. 인증 불필요.\n            - 링크 직접 접근: inviteCode 생략 가능. 만료 여부만 확인.\n            - 코드 입력 경로: inviteCode 전달 시 코드 일치 여부도 검증.\n            성공 시 JWT 토큰 쌍과 생성된 사용자 정보가 반환된다.\n            토큰 전달 방식은 클라이언트 타입에 따라 다르다 (X-Client-Type 헤더):\n            - WEB(기본·미설정): accessToken·refreshToken 은 HttpOnly 쿠키로 전달, body 값은 null.\n            - APP(app 명시): accessToken·refreshToken 을 body 로 전달, 쿠키 없음.\n        ","operationId":"joinAsGuest","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JoinTournamentAsGuestRequest"}}},"required":true},"responses":{"201":{"description":"게스트 계정 생성 및 참여 성공 (WEB=쿠키 전달·body null / APP=body 전달)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"게스트 참여 성공 (APP — body 토큰)":{"value":{"data":{"userId":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","nickname":"멋진친구","profileImage":"https://piki-assets.s3.ap-northeast-2.amazonaws.com/defaults/user-profile-3.png","tournamentId":1,"accessToken":"eyJhbGciOiJIUzI1NiJ9.example","refreshToken":"eyJhbGciOiJIUzI1NiJ9.refresh"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (초대 코드 형식 오류 · 코드 불일치 — inviteCode 전달 시에만 발생 · 닉네임 미입력 · 닉네임 10자 초과 · 닉네임이 '탈퇴' 예약 prefix 로 시작)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"닉네임 미입력":{"value":{"data":null,"detail":"nickname: 닉네임은 비어 있을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}},"409":{"description":"상태 충돌 (PENDING이 아닌 토너먼트 · 초대 링크 만료 · 닉네임 중복 · 참여 인원 초과(최대 8명))","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}}}}},"/api/v1/tournaments/{tournamentId}/items/wish":{"post":{"tags":["Tournament Item"],"summary":"위시에서 토너먼트 아이템 추가","description":"\n            PENDING 상태의 토너먼트에 위시리스트에 있는 아이템을 추가한다. 토너먼트 참여자(회원)만 호출 가능.\n            플레이 링크로 생성된 복제 토너먼트에는 추가 불가.\n            itemIds 중 하나라도 조건에 맞지 않으면 요청 전체가 실패한다(부분 성공 없음).\n            응답의 tournamentItemIds 는 요청 itemIds 와 동일한 순서로 대응된다.\n        ","operationId":"addItemsFromWish","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddTournamentItemsRequest"}}},"required":true},"responses":{"200":{"description":"아이템 추가 성공 (tournamentItemIds: 요청 itemIds 순서와 동일하게 대응)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"위시 아이템 추가 성공":{"value":{"data":{"tournamentItemIds":[10,11]},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (itemIds 1~32개 범위 초과 · 아이템 최대 32개 초과)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"itemIds 개수 위반 (1~32개)":{"value":{"data":null,"detail":"itemIds: 아이템은 1개 이상 32개 이하여야 합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"아이템 최대 32개 초과":{"value":{"data":null,"detail":"토너먼트 아이템은 최대 32개까지 추가할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님 · 위시리스트에 없는 아이템 포함 · 플레이 링크로 생성된 복제 토너먼트)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트 권한 없음":{"value":{"data":null,"detail":"해당 토너먼트에 대한 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"플레이링크 복제 토너먼트에는 아이템 추가 불가":{"value":{"data":null,"detail":"플레이 링크로 생성된 토너먼트에는 아이템을 추가할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"위시리스트에 없는 아이템 포함":{"value":{"data":null,"detail":"위시리스트에 없는 아이템은 토너먼트에 추가할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음 · 존재하지 않는 아이템 포함","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"존재하지 않는 아이템 포함":{"value":{"data":null,"detail":"존재하지 않는 아이템이 포함되어 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (PENDING이 아닌 토너먼트 · 이미 등록된 아이템 · 요청 내 중복 아이템 · PENDING/PROCESSING/FAILED 등 미완료 상품 포함)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"PENDING 상태 아님":{"value":{"data":null,"detail":"PENDING 상태인 토너먼트에만 수행할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"이미 등록된/중복 아이템":{"value":{"data":null,"detail":"이미 토너먼트에 등록된 아이템입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"PENDING/PROCESSING/FAILED 등 미완료 상품 포함":{"value":{"data":null,"detail":"아직 준비되지 않은 상품은 토너먼트에 추가할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/tournaments/{tournamentId}/items/link":{"post":{"tags":["Tournament Item"],"summary":"URL 링크로 토너먼트 아이템 추가","description":"\n            PENDING 상태의 토너먼트에 URL 링크를 통해 아이템을 추가한다.\n            플레이 링크로 생성된 복제 토너먼트에는 추가 불가. 토너먼트 참여자만 추가할 수 있다.\n            아이템이 PENDING 상태로 즉시 생성되어 tournamentItemId 가 반환된다.\n            파싱은 비동기로 진행되며, 디스패처가 PENDING 을 집어 PROCESSING 으로 전이한 뒤 READY 또는 FAILED 상태로 전환된다.\n            클라이언트는 tournamentItemId 로 GET /tournaments/{id}/items/{tournamentItemId} 를 폴링한다.\n        ","operationId":"addItemFromLink","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddTournamentItemFromLinkRequest"}}},"required":true},"responses":{"200":{"description":"아이템 추가 성공 (item.status=PENDING, 파싱은 백그라운드)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"링크 아이템 추가 성공":{"value":{"data":{"tournamentItemId":1},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (URL 미입력 · URL 형식 오류(비어 있음/유효하지 않음/https 외 스킴) · URL 2048자 초과 · 아이템 최대 32개 초과)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유효하지 않은 URL 형식":{"value":{"data":null,"detail":"유효한 URL 형식이 아닙니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"https 외 스킴":{"value":{"data":null,"detail":"https URL만 허용합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"아이템 최대 32개 초과":{"value":{"data":null,"detail":"토너먼트 아이템은 최대 32개까지 추가할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님 · 플레이 링크로 생성된 복제 토너먼트)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트 권한 없음":{"value":{"data":null,"detail":"해당 토너먼트에 대한 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"플레이링크 복제 토너먼트에는 아이템 추가 불가":{"value":{"data":null,"detail":"플레이 링크로 생성된 토너먼트에는 아이템을 추가할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (PENDING이 아닌 토너먼트)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"PENDING 상태 아님":{"value":{"data":null,"detail":"PENDING 상태인 토너먼트에만 수행할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/tournaments/{tournamentId}/items/images":{"post":{"tags":["Tournament Item"],"summary":"이미지로 토너먼트 아이템 추가","description":"\n            PENDING 상태의 토너먼트에 이미지 추출을 통해 아이템을 추가한다.\n            플레이 링크로 생성된 복제 토너먼트에는 추가 불가. 토너먼트 참여자만 추가할 수 있다.\n            이미지 1~5장을 전달하면 아이템이 PROCESSING 상태로 즉시 생성되어 tournamentItemIds 가 반환된다.\n            이미지 파싱은 비동기로 진행되며 완료 시 READY 또는 FAILED 상태로 전환된다.\n            클라이언트는 tournamentItemId 로 GET /tournaments/{id}/items/{tournamentItemId} 를 폴링한다.\n        ","operationId":"addItemsFromImages","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"images":{"type":"array","items":{"type":"string","format":"binary"}}}}}}},"responses":{"200":{"description":"아이템 추가 성공 (item.status=PROCESSING, 파싱은 백그라운드)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"이미지 아이템 추가 성공":{"value":{"data":{"tournamentItemIds":[1,2,3]},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (이미지 1~5장 범위 초과 · 빈 이미지 · 이미지 타입 미지정 · 지원하지 않는 이미지 형식(png/jpeg/webp/heic/heif만 허용) · 아이템 최대 32개 초과)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"이미지 개수 위반 (1~5개)":{"value":{"data":null,"detail":"이미지는 최소 1개, 최대 5개까지 전송할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"아이템 최대 32개 초과":{"value":{"data":null,"detail":"토너먼트 아이템은 최대 32개까지 추가할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님 · 플레이 링크로 생성된 복제 토너먼트)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트 권한 없음":{"value":{"data":null,"detail":"해당 토너먼트에 대한 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"플레이링크 복제 토너먼트에는 아이템 추가 불가":{"value":{"data":null,"detail":"플레이 링크로 생성된 토너먼트에는 아이템을 추가할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (PENDING이 아닌 토너먼트)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"PENDING 상태 아님":{"value":{"data":null,"detail":"PENDING 상태인 토너먼트에만 수행할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/tournaments/{sourceTournamentId}/from-play-link":{"post":{"tags":["Tournament"],"summary":"플레이 링크로 토너먼트 진행","description":"\n            플레이 링크가 유효한 토너먼트와 동일한 아이템 구성으로 새 토너먼트를 생성한다.\n            생성된 토너먼트는 PENDING 상태이며 아이템이 미리 복사되어 있어 바로 시작할 수 있다.\n            idempotent: 같은 사용자가 같은 원본의 플레이 링크를 다시 호출하면 새로 만들지 않고\n            기존 본인 클론의 tournamentId 를 그대로 반환한다 (원본 링크 만료와 무관하게 이어서 진행 가능).\n        ","operationId":"createFromPlayLink","parameters":[{"name":"sourceTournamentId","in":"path","description":"원본 토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"responses":{"200":{"description":"복제 토너먼트 생성 또는 기존 본인 클론 반환 (tournamentId 반환)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"복제 토너먼트 생성 또는 기존 본인 클론 반환":{"value":{"data":42,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음 · 플레이 링크가 생성되지 않은 토너먼트","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"플레이 링크가 생성되지 않은 토너먼트":{"value":{"data":null,"detail":"플레이 링크가 생성되지 않은 토너먼트입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (플레이 링크 만료)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"플레이 링크 만료":{"value":{"data":null,"detail":"플레이 링크가 만료되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/notifications/read":{"post":{"tags":["Notification"],"summary":"알림 읽음 처리","description":"알림을 읽음 처리한다 (**GUEST·MEMBER 모두, 본인 알림만**). 요청 body 는 두 방식 중 **정확히 하나**:\n\n| 방식 | 동작 |\n|---|---|\n| `all=true` | 본인 안읽음 알림 전부 읽음 (전체 읽음 버튼, 화면 이동 없음) |\n| `ids=[...]` | 지정한 알림만 읽음 (단건 클릭은 `[id]` 1개, 클릭 후 FE 가 딥링크로 이동) |\n\n- 둘 다 보내거나 둘 다 비우면(빈 `ids` 포함) **400**.\n- `ids` 는 본인 소유만 반영되고 타인·없는 id 는 무시된다. **멱등**(이미 읽음도 성공).\n- 응답 `data` 의 `unreadCount`(앱 badge) · `unreadCountByCategory`(탭 badge)로 처리 후 안읽음 수를 **서버 권위 값**으로 내려준다 — 클라는 이 값들을 그대로 badge 로 미러링한다(별도 카운트 조회 불필요).","operationId":"read","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationReadRequest"}}},"required":true},"responses":{"200":{"description":"읽음 처리 성공 (처리 후 unreadCount 동봉, 멱등)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"읽음 처리 성공 (처리 후 unreadCount 동봉)":{"value":{"data":{"unreadCount":2,"unreadCountByCategory":{"ACTIVITY":1,"SYSTEM":1}},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (all 과 ids 를 함께 보냄 · 둘 다 없음 · 빈 ids)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"all 과 ids 동시 전송 / 둘 다 없음 / 빈 ids":{"value":{"data":null,"detail":"validSelection: all=true 또는 ids 중 정확히 하나만 보내야 합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/fcm/tokens":{"post":{"tags":["FCM"],"summary":"FCM 토큰 등록/갱신","description":"현재 인증 유저의 이 기기 FCM 토큰을 등록한다. **upsert(멱등)** 이며 앱 진입·토큰 갱신 시 호출한다.\n\n- 같은 기기(`deviceId`)에서 토큰이 회전하면 그 기기 row 의 토큰만 교체한다.\n- 다른 사용자가 같은 토큰을 등록하면 이전 소유자 row 를 해제해, 한 토큰은 한 사용자에게만 매핑된다 (로그아웃한 기기로 알림이 새지 않게 함).\n\n알림 표시 동의는 OS 권한이 게이트하므로 서버는 동의 여부를 저장하지 않는다 — 발송은 모든 기기에 시도하고 OS 가 표시를 막는다.","operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FcmTokenRegisterRequest"}}},"required":true},"responses":{"200":{"description":"등록/갱신 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"등록/갱신 성공":{"value":{"data":null,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (token 또는 deviceId 가 비어 있음 · 길이 초과)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토큰 또는 기기 식별자 누락":{"value":{"data":null,"detail":"FCM 토큰과 기기 식별자는 비어 있을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}},"delete":{"tags":["FCM"],"summary":"FCM 기기 해제","description":"현재 인증 유저의 이 기기 등록을 제거한다(로그아웃). **멱등** — 없는 기기를 지워도 성공이다.\n\n- 인증이 필요하므로 로그아웃 시퀀스에서 `/auth/logout` 보다 **먼저**(토큰이 아직 유효할 때) 호출해야 한다.\n- 로그아웃한 세션·기기로 알림이 새지 않게 하기 위함이다.","operationId":"unregister","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FcmDeviceUnregisterRequest"}}},"required":true},"responses":{"200":{"description":"해제 성공 (data=null)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"해제 성공":{"value":{"data":null,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (deviceId 가 비어 있음 · 길이 초과)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"기기 식별자 누락":{"value":{"data":null,"detail":"기기 식별자는 비어 있을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/dev/{userId}/token":{"post":{"tags":["Dev"],"summary":"기존 user 의 토큰 발급 (임의 user 가장)","description":"이미 존재하는 user (GUEST·MEMBER 모두)의 access·refresh 토큰을 발급한다. 개발·테스트에서 특정 user 시나리오를 재현할 때 사용한다.\n\n- GUEST 토큰으로 호출해야 한다.\n- OAuth 통합 전까지의 임시 endpoint 로, 다른 dev API 들과 함께 운영에서 차단 예정.","operationId":"issueTokenForUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"201":{"description":"토큰 발급 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토큰 발급 성공":{"value":{"data":{"user":{"id":"3b9c1d2e-4f5a-4b6c-8d7e-9f0a1b2c3d4e","nickname":"홍길동","profileImage":"https://piki-assets.s3.ap-northeast-2.amazonaws.com/defaults/user-profile-2.png","identityType":"MEMBER"},"accessToken":"eyJhbGciOiJIUzI1NiJ9.access","refreshToken":"eyJhbGciOiJIUzI1NiJ9.refresh"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"GUEST 권한 없음 (MEMBER 토큰으로 호출 불가 · GUEST 필요)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"GUEST 권한 없음 (MEMBER 토큰으로 호출 불가)":{"value":{"data":null,"detail":"권한 없음 — 접근 불가","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"userId 에 해당하는 user 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"userId 에 해당하는 user 없음":{"value":{"data":null,"detail":"유저를 찾을 수 없습니다. userId=3b9c1d2e-4f5a-4b6c-8d7e-9f0a1b2c3d4e","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"탈퇴(soft delete) 된 user — 토큰 발급 거부","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"탈퇴된 user":{"value":{"data":null,"detail":"탈퇴한 유저입니다. userId=3b9c1d2e-4f5a-4b6c-8d7e-9f0a1b2c3d4e","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/dev/users":{"get":{"tags":["Dev"],"summary":"유저 목록 조회","description":"등록된 모든 유저의 userId 와 nickname 을 반환한다. 개발 편의용.","operationId":"listUsers","parameters":[{"name":"size","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"유저 목록 반환","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유저 목록 (다음 페이지 있음)":{"value":{"data":[{"userId":"8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f","nickname":"뛰어다니는 강아지"},{"userId":"3b9c1d2e-4f5a-4b6c-8d7e-9f0a1b2c3d4e","nickname":"홍길동"}],"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":"1","hasNext":true}}}}}}}}},"post":{"tags":["Dev"],"summary":"개발용 MEMBER 생성","description":"OAuth 없이 MEMBER User 를 생성하고 JWT 토큰 쌍을 발급한다. GUEST 토큰으로 호출해야 한다. OAuth 통합 전까지의 임시 endpoint.","operationId":"createDevUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevUserCreateRequest"}}},"required":true},"responses":{"201":{"description":"MEMBER 생성 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"MEMBER 생성 성공":{"value":{"data":{"user":{"id":"3b9c1d2e-4f5a-4b6c-8d7e-9f0a1b2c3d4e","nickname":"홍길동","profileImage":"https://piki-assets.s3.ap-northeast-2.amazonaws.com/defaults/user-profile-2.png","identityType":"MEMBER"},"accessToken":"eyJhbGciOiJIUzI1NiJ9.access","refreshToken":"eyJhbGciOiJIUzI1NiJ9.refresh"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"닉네임 미입력 또는 형식 오류","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"닉네임 미입력":{"value":{"data":null,"detail":"nickname: 닉네임은 필수입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"GUEST 권한 없음 (MEMBER 토큰으로 호출 불가 · GUEST 필요)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"GUEST 권한 없음 (MEMBER 토큰으로 호출 불가)":{"value":{"data":null,"detail":"권한 없음 — 접근 불가","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"이미 사용 중인 닉네임","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"이미 사용 중인 닉네임":{"value":{"data":null,"detail":"이미 사용 중인 닉네임입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/dev/fcm/push":{"post":{"tags":["Dev"],"summary":"[DEV] FCM 즉시 발송 (토큰 직접 지정)","description":"본문의 FCM 토큰으로 즉시 푸시를 발송한다. FE 가 Xcode 에서 받은 토큰을 Postman 으로 던져 \"우리 서버 → FCM → 내 기기\" 도달을 자가 확인하는 개발 도구다. 등록(#244) 없이 토큰만으로 쏜다.\n\n- 발송은 운영과 동일한 `FirebaseMessageSender` 를 태운다. `@Profile(\"!prod\")` 라 운영에는 라우트가 없다.\n- **인증** — `/api/v1/dev/**` 는 GUEST 권한이 필요하다 (`POST /api/v1/auth/guest` 로 게스트 토큰 발급 후 Bearer).\n\n**응답 해석**\n\n| 필드 | 의미 |\n|---|---|\n| `fcmEnabled=false` | 이 서버에 `FIREBASE_SERVICE_ACCOUNT` 가 없어 발송이 no-op |\n| `staleTokenCount=1` | 그 토큰이 무효/만료 (앱 삭제 등) |","operationId":"push","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DevPushRequest"}}},"required":true},"responses":{"200":{"description":"발송 위임 성공 (fcmEnabled·staleTokenCount 로 실제 발송 여부 확인)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"발송 성공":{"value":{"data":{"fcmEnabled":true,"staleTokenCount":0},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"FCM 미설정 (no-op)":{"value":{"data":{"fcmEnabled":false,"staleTokenCount":0},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (token 비어 있음 · 길이 초과)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토큰 누락":{"value":{"data":null,"detail":"FCM 토큰은 비어 있을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"GUEST 권한 없음 (/api/v1/dev/** 는 GUEST 권한 필요)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"GUEST 권한 없음":{"value":{"data":null,"detail":"권한 없음 — 접근 불가","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/auth/token/refresh":{"post":{"tags":["Auth"],"summary":"토큰 갱신","description":"리프레시 토큰을 검증하고 새 access·refresh 토큰 쌍을 발급(회전)한다. 만료된 access token 클라이언트도 호출할 수 있도록 인증 없이 진입한다.\n\n- **입력** — 리프레시 토큰을 `refresh_token` 쿠키(기본) 또는 요청 body(app) 어느 쪽으로든 받는다.\n- **출력** — 기본은 새 토큰을 `Set-Cookie` 로 회전하고 body 토큰은 `null`, `X-Client-Type: app` 일 때만 body 로 내린다.","operationId":"refresh","parameters":[{"name":"X-Client-Type","in":"header","description":"클라이언트 종류. app 이면 body 로 받는다. 그 외·미설정은 새 토큰을 Set-Cookie 로 회전하고 body 토큰은 null(기본).","schema":{"type":"string","enum":["web","app"]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRefreshRequest"}}}},"responses":{"200":{"description":"토큰 갱신 성공 (기본: Set-Cookie 회전 + body 토큰 null / app: body 토큰)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토큰 갱신 성공 (APP — body 토큰)":{"value":{"data":{"accessToken":"eyJhbGciOiJIUzI1NiJ9.access","refreshToken":"eyJhbGciOiJIUzI1NiJ9.refresh"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"리프레시 토큰 미입력 (쿠키·body 모두 없음) · body 의 refreshToken 이 공백","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"리프레시 토큰 미입력":{"value":{"data":null,"detail":"리프레시 토큰이 필요합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"유효하지 않은 리프레시 토큰 (파싱 불가 · 만료 · Redis 값 불일치 · 탈퇴 유저)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유효하지 않은 토큰":{"value":{"data":null,"detail":"유효하지 않은 토큰입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}},"security":[]}},"/api/v1/auth/logout":{"post":{"tags":["Auth"],"summary":"로그아웃","description":"리프레시 토큰을 삭제해 로그아웃 처리하고, 토큰 쿠키를 만료(`Max-Age=0`)시킨다.\n\n- 쿠키 만료는 클라이언트 종류와 무관하게 **항상** 내려간다 (웹 쿠키가 확실히 삭제되도록).\n- APP 은 쿠키를 쓰지 않아 영향 없다.","operationId":"logout","responses":{"200":{"description":"로그아웃 성공 (토큰 쿠키 만료)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"로그아웃 성공":{"value":{"data":{"loggedOut":true},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/auth/login/{provider}":{"post":{"tags":["Auth"],"summary":"소셜 로그인","description":"`kakao` / `google` 소셜 로그인. 처음 보는 소셜이면 **MEMBER 로 가입**(닉네임 자동 fill, 이후 수정 가능)하고, 기존이면 로그인한다.\n\n**요청 방식 (provider 버전별)**\n\n| 버전 | 보내는 값 |\n|---|---|\n| v1 (웹) | body 에 `code` + `redirectUri` |\n| v2 (SDK) | body 에 `accessToken` |\n\n- **게스트 승격** — 게스트 토큰을 함께 보내면 그 게스트 계정에 소셜을 연결+승격해 위시·토너먼트 데이터를 이어준다.\n- **토큰 전달** — 응답 토큰은 기본 HttpOnly 쿠키로 내려가며, `X-Client-Type: app` 일 때만 body 로 내린다.\n- **CSRF** — v1 웹 흐름은 `GET /auth/{provider}/url` 로 발급받은 `state` 를 body 에 함께 보내면 검증이 활성화된다.","operationId":"login","parameters":[{"name":"provider","in":"path","description":"소셜 제공자","required":true,"schema":{"type":"string","enum":["kakao","google"]},"example":"kakao"},{"name":"X-Client-Type","in":"header","description":"클라이언트 종류. app 이면 body 로 토큰을 받는다. 그 외·미설정은 HttpOnly 쿠키(기본).","schema":{"type":"string","enum":["web","app"]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthLoginRequest"}}},"required":true},"responses":{"200":{"description":"로그인/가입 성공 (기본: Set-Cookie + body 토큰 null / app: body 토큰)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"로그인/가입 성공 (app — body 토큰)":{"value":{"data":{"user":{"id":"8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f","nickname":"뛰어다니는 강아지","profileImage":"https://lh3.googleusercontent.com/profile.jpg","identityType":"MEMBER"},"accessToken":"eyJhbGciOiJIUzI1NiJ9.access","refreshToken":"eyJhbGciOiJIUzI1NiJ9.refresh"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청\n\n- `code`+`redirectUri` 도 `accessToken` 도 없음\n- `accessToken` 과 `code` 를 동시 전달\n- 지원하지 않는 provider\n- provider 인가코드(`code`)가 만료/재사용/무효 — 재로그인으로 새 code 를 받아 재시도","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"요청 본문 검증 실패 (두 흐름 동시 전달 또는 둘 다 누락/공백)":{"value":{"data":null,"detail":"validFlow: code+redirectUri 또는 accessToken 중 한 흐름만 보내야 합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"자격증명 누락 (code+redirectUri·accessToken 모두 공백)":{"value":{"data":null,"detail":"소셜 로그인 요청이 올바르지 않습니다 (code+redirectUri 또는 accessToken 이 필요합니다).","pageResponse":{"nextCursor":null,"hasNext":false}}},"지원하지 않는 provider":{"value":{"data":null,"detail":"지원하지 않는 소셜 로그인 제공자입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"인가코드(code) 만료/재사용/무효":{"value":{"data":null,"detail":"소셜 로그인 인가 정보가 만료되었거나 유효하지 않습니다. 다시 시도해 주세요.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"인증 실패\n\n- `state` 검증 실패 (만료·미발급·이미 소비됨) — `GET /auth/{provider}/url` 로 재발급 필요\n- provider access token 무효/만료 — 재로그인으로 새 토큰을 받아 재시도","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"state 검증 실패 (만료 또는 미발급)":{"value":{"data":null,"detail":"유효하지 않은 state 파라미터입니다. 인가 URL 을 새로 발급받아 다시 시도하세요.","pageResponse":{"nextCursor":null,"hasNext":false}}},"provider access token 무효/만료 (재로그인 필요)":{"value":{"data":null,"detail":"소셜 로그인 토큰이 유효하지 않습니다. 다시 로그인해 주세요.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"502":{"description":"소셜 제공자 호출 경계 실패 (응답 body 의 `category` 로 구분)\n\n- **RETRYABLE** — provider 장애(네트워크·5xx·점검·user_info 조회 실패·미지 에러코드 fallback). 동일 요청으로 재시도 가능.\n- **SERVER_ERROR** — 우리 OAuth 설정·요청 오류(client_id/secret·client_secret JWT·필수 인자 누락 등). 재시도 무의미, 서버 설정 수정 필요.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"소셜 제공자 장애 (RETRYABLE — 재시도 가능)":{"value":{"data":null,"detail":"소셜 로그인 제공자 호출에 실패했습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"우리 OAuth 설정/요청 오류 (SERVER_ERROR — 재시도 무의미)":{"value":{"data":null,"detail":"소셜 로그인 설정 오류가 발생했습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}},"security":[]}},"/api/v1/auth/guest":{"post":{"tags":["Auth"],"summary":"게스트 생성","description":"새 **GUEST** User 를 생성하고 JWT 토큰 쌍(access·refresh)을 발급한다.\n\n토큰 전달 방식은 `X-Client-Type` 헤더로 갈린다.\n\n| X-Client-Type | 토큰 전달 | body 토큰 |\n|---|---|---|\n| 미설정 · `web` | `Set-Cookie` HttpOnly 쿠키 2개 (secure by default) | `null` |\n| `app` | 응답 body (네이티브 secure storage) | 포함 |","operationId":"createGuest","parameters":[{"name":"X-Client-Type","in":"header","description":"클라이언트 종류. app 이면 body 로 토큰을 받는다(네이티브 secure storage). 그 외·미설정은 HttpOnly 쿠키로 받고 body 토큰은 null(기본, secure by default).","schema":{"type":"string","enum":["web","app"]}}],"responses":{"201":{"description":"게스트 생성 성공 (기본: Set-Cookie 2개 + body 토큰 null / app: body 토큰)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"게스트 생성 성공 (APP — body 토큰)":{"value":{"data":{"user":{"id":"8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f","nickname":"뛰어다니는 강아지","profileImage":"https://piki-assets.s3.ap-northeast-2.amazonaws.com/defaults/user-profile-1.png","identityType":"GUEST"},"accessToken":"eyJhbGciOiJIUzI1NiJ9.access","refreshToken":"eyJhbGciOiJIUzI1NiJ9.refresh"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}},"security":[]}},"/api/v1/auth/apple/notifications":{"post":{"tags":["Auth"],"summary":"Apple 서버-서버 알림 수신","description":"**Apple 이 직접 호출하는 서버-서버 엔드포인트다 (클라이언트 앱이 호출하지 않는다).**\n\n사용자가 Apple 쪽에서 \"Apple 로 로그인\" 연결을 끊거나 Apple ID 를 삭제하면, Apple 이 이 경로로 서명된 알림(JWT)을 보낸다. 우리는 서명을 검증한 뒤 계정 상태를 동기화한다. 엔드포인트 URL 은 Apple Developer Console 의 Server-to-Server Notification Endpoint 에 등록해야 동작한다.\n\n요청은 `application/x-www-form-urlencoded` 의 `payload` 필드에 서명된 JWT 로 담겨 온다. 진위는 오직 그 JWT 서명(Apple JWKS)·issuer·aud 로 가린다 (우리 JWT 인증 없음).\n\n**이벤트별 처리**\n\n| 이벤트 | 의미 | 처리 |\n|---|---|---|\n| `account-delete` | Apple ID 자체 삭제 | 회원 탈퇴 (데이터 파기) |\n| `consent-revoked` | 앱-Apple 연결 해제 | 세션 종료(로그아웃). 계정·데이터는 유지, 재로그인 시 복귀 |\n| `email-disabled` / `email-enabled` | Private Relay 전달 on/off | 로그만 (메일 발송 안 함) |\n\n- 대상 유저가 없거나(미가입·이미 탈퇴) 미지원 이벤트면 멱등하게 200 으로 흡수한다 (Apple 은 2xx 가 아니면 재시도하므로).","operationId":"handle","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"payload":{"type":"string","description":"Apple 이 보내는 서명된 알림 JWT (form 필드 payload)"}},"required":["payload"]}}}},"responses":{"200":{"description":"알림 처리 완료 (멱등 — 대상 유저 없음·이미 탈퇴·미지원 이벤트 포함)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"알림 처리 완료 (멱등)":{"value":{"data":null,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"유효하지 않은 Apple 알림 (서명·issuer·aud 검증 실패 또는 payload 형식 오류 = 위조/비정상 호출)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유효하지 않은 Apple 알림":{"value":{"data":null,"detail":"유효하지 않은 Apple 서버 알림입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"502":{"description":"외부 의존성 실패 (서명 검증에 필요한 Apple JWKS 조회 실패 — 재시도 가능)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"Apple 키 조회 실패 (재시도 가능)":{"value":{"data":null,"detail":"Apple 알림 검증 중 외부 키 조회에 실패했습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}},"security":[]}},"/api/v1/wishlists/{wishId}":{"get":{"tags":["Wishlist"],"summary":"위시리스트 조회 (단건)","description":"\n            wishId 로 위시 항목 하나를 조회한다. 응답 모양은 목록 조회 항목과 같은 WishItemResponse(wish + item).\n            본인 위시만 조회 가능하며, item 을 직접 노출하지 않고 위시 소유 단위로 권한을 검증한다.\n            item.status 로 파싱 상태(PENDING/PROCESSING/READY/FAILED)를 구분한다 — 상세 화면 진입 시 단건 폴링에 쓸 수 있다.\n        ","operationId":"getWish","parameters":[{"name":"wishId","in":"path","description":"위시 항목 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1024}],"responses":{"200":{"description":"조회 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"조회 성공":{"value":{"data":{"wish":{"id":1024,"createdAt":"2026-05-21T10:00:00Z"},"item":{"id":512,"status":"READY","name":"에어 조던 1 미드","currentPrice":119000,"currency":"KRW","imageUrl":"https://cdn.example.com/p/512.jpg","sourceUrl":"https://www.example-shop.com/products/12345"}},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (GUEST 권한으로 접근 불가 · MEMBER 필요, 또는 본인 위시가 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"본인 위시 아님":{"value":{"data":null,"detail":"해당 위시 아이템에 접근할 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"존재하지 않는 위시 항목 (삭제된 항목 포함)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"존재하지 않는 위시 항목":{"value":{"data":null,"detail":"존재하지 않는 위시리스트 항목입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}},"delete":{"tags":["Wishlist"],"summary":"위시리스트 삭제 (단건)","description":"\n            위시 항목을 삭제한다(soft delete). 멱등 — 이미 없거나 삭제된 항목이면 아무 일도 하지 않고 성공한다.\n            존재하는 항목은 본인 위시만 삭제 가능하다. 삭제된 항목은 조회 결과에서 제외된다.\n        ","operationId":"deleteWish","parameters":[{"name":"wishId","in":"path","description":"위시 항목 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1024}],"responses":{"200":{"description":"삭제 성공 (없거나 이미 삭제된 항목도 멱등 성공, data 없음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"삭제 성공":{"value":{"data":null,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (GUEST 권한으로 접근 불가 · MEMBER 필요, 또는 본인 위시가 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"본인 위시 아님":{"value":{"data":null,"detail":"해당 위시 아이템에 접근할 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}},"patch":{"tags":["Wishlist"],"summary":"위시 항목 복구 (추출 실패 보정)","description":"\n            추출에 실패(item.status=FAILED)한 위시 항목의 상품 정보를 사용자가 직접 채워 복구한다(multipart/form-data).\n            텍스트(이름·현재가·통화)는 form 필드로, 이미지는 image 파트로 받는다 — 이미지는 URL 이 아니라 파일로만 받아\n            서버가 그대로 S3 에 올려 대표 이미지로 채운다(추출·크롭 없음). 들어온 값만 갱신하고, 보정에 성공하면 READY 로 복구된다.\n            item 데이터는 링크에서 기계 추출한 사실이라, 이미 완성(READY)된 항목은 수정할 수 없고(409 CONFLICT),\n            대기·파싱 중(PENDING·PROCESSING)인 항목은 백그라운드 워커 소관이라 끼어들 수 없다(409 CONFLICT).\n            본인 위시만 보정 가능하며, item 을 직접 노출하지 않고 위시 소유 단위로 권한을 검증한다.\n        ","operationId":"recoverWishItem","parameters":[{"name":"wishId","in":"path","description":"위시 항목 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1024}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/WishlistUpdateRequest"}}}},"responses":{"200":{"description":"추출 실패(FAILED) 항목 보정 성공 — status 가 READY 로 복구됨","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"FAILED 보정 성공 (READY 로 복구)":{"value":{"data":{"wish":{"id":1024,"createdAt":"2026-05-21T10:00:00Z"},"item":{"id":512,"status":"READY","name":"에어 조던 1 미드","currentPrice":119000,"currency":"KRW","imageUrl":"https://cdn.example.com/p/512.jpg","sourceUrl":"https://www.example-shop.com/products/12345"}},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (보정 후에도 상품명 없음 · currentPrice 음수 · name/currency 길이 초과 · 빈 이미지 · 지원하지 않는 이미지 형식(png/jpeg/webp/heic/heif만 허용))","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"가격 음수":{"value":{"data":null,"detail":"currentPrice: 가격은 0 이상이어야 합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"상품명 없이 복구 시도":{"value":{"data":null,"detail":"상품명을 입력해야 합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (GUEST 권한으로 접근 불가 · MEMBER 필요, 또는 본인 위시가 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"본인 위시 아님":{"value":{"data":null,"detail":"해당 위시 아이템에 접근할 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"존재하지 않는 위시 항목 (삭제된 항목 포함)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"존재하지 않는 위시 항목":{"value":{"data":null,"detail":"존재하지 않는 위시리스트 항목입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"수정할 수 없는 상태 (이미 등록 완료(READY) · 아직 대기·처리 중(PENDING·PROCESSING))","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"이미 등록 완료(READY) 항목 — 수정 불가":{"value":{"data":null,"detail":"이미 등록 완료된 상품은 수정할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"아직 대기·처리 중(PENDING·PROCESSING) 항목 — 수정 불가":{"value":{"data":null,"detail":"아직 처리 중인 상품은 수정할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"502":{"description":"이미지 저장소(S3) 업로드 실패 — 재시도 가능","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"이미지 저장 실패":{"value":{"data":null,"detail":"이미지 저장에 실패했습니다. 잠시 후 다시 시도해 주세요.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/users/me":{"get":{"tags":["User"],"summary":"내 정보 조회","description":"현재 로그인된 유저(**GUEST 포함**)의 정보를 조회한다. 소셜 계정 `email` 도 함께 내려준다 (미수집·미동의·backfill 전이면 `null`).","operationId":"getMe","responses":{"200":{"description":"조회 성공 (email 은 미수집·미동의 시 null)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"내 정보 조회 성공 (email 있음)":{"value":{"data":{"id":"8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f","nickname":"뛰어다니는 강아지","profileImage":"https://api.dicebear.com/9.x/bottts/svg?seed=8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f","identityType":"MEMBER","email":"user@gmail.com"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"내 정보 조회 성공 (email 미수집·게스트)":{"value":{"data":{"id":"8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f","nickname":"뛰어다니는 강아지","profileImage":"https://api.dicebear.com/9.x/bottts/svg?seed=8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f","identityType":"GUEST","email":null},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"유저를 찾을 수 없음 (JWT 유효하지만 DB에서 유저가 삭제된 경우)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유저 없음 (JWT 유효하나 DB에 없음)":{"value":{"data":null,"detail":"리소스 없음 — 존재하지 않는 대상","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}},"delete":{"tags":["User"],"summary":"회원 탈퇴","description":"현재 로그인된 **MEMBER** 의 계정을 탈퇴 처리한다. 게스트는 탈퇴 대상이 아니라 403 으로 거부하며, **멱등**이라 재요청해도 200.\n\n**데이터 처리**\n\n- `users` 행은 익명 tombstone 으로 남겨 공유 토너먼트 참조를 보존한다.\n- 소셜 식별자(`user_details`)·기기 토큰(`user_devices`)·위시·알림은 즉시 **하드삭제** (PIPA 지체없이 파기).\n- refresh token 무효화·SSE 연결 종료까지 함께 처리한다.","operationId":"withdraw","responses":{"200":{"description":"탈퇴 성공 (data=null)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"탈퇴 성공":{"value":{"data":null,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"게스트는 탈퇴할 수 없음 (탈퇴는 MEMBER 전용)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"게스트 탈퇴 거부":{"value":{"data":null,"detail":"게스트는 탈퇴할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"유저를 찾을 수 없음 (JWT 유효하지만 DB에서 유저가 삭제된 경우)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유저 없음 (JWT 유효하나 DB에 없음)":{"value":{"data":null,"detail":"리소스 없음 — 존재하지 않는 대상","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}},"patch":{"tags":["User"],"summary":"내 정보 수정","description":"내 정보(`nickname` · 프로필 이미지)를 한 요청(`multipart/form-data`)으로 **부분 수정**한다. 들어온 필드만 갱신하며, 둘 다 보내면 한 트랜잭션에 묶여 함께 반영되고, 아무 필드도 안 보내면 변화 없이 200 으로 통과한다.\n\n**필드별 권한·동작**\n\n| 필드 | 권한 | 동작 |\n|---|---|---|\n| `nickname` | GUEST·MEMBER | 닉네임 변경 |\n| `image` | **MEMBER 전용** | 파일 업로드 → S3 저장 → 그 URL 로 `profileImage` 갱신 |\n\n- GUEST 가 `image` 파트를 담아 호출하면 **403** 으로 거부한다 (닉네임 동반 여부와 무관하게 요청 전체 거부).\n- 이미지 허용 형식: `png` / `jpeg` / `webp` / `heic` / `heif` (그 외는 400). 파일 크기 5MB 이하.","operationId":"updateMe","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UserUpdateRequest"}}}},"responses":{"200":{"description":"수정 성공 (갱신된 nickname · profileImage 포함. 빈 요청이면 기존 값 그대로)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"수정 성공 (닉네임·프로필 이미지)":{"value":{"data":{"id":"8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f","nickname":"새닉네임","profileImage":"https://cdn.example.com/profiles/8f1a3c2b/9d44.jpg","identityType":"MEMBER"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청\n\n- **닉네임** — 공백 · 10자 초과 · '탈퇴' 예약 prefix 로 시작\n- **이미지** — 빈 파일 · 타입 미지정 · 지원하지 않는 형식(`png`/`jpeg`/`webp`/`heic`/`heif` 만 허용) · 선언한 Content-Type 과 실제 파일 내용 불일치(헤더 위조·파일 손상)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"닉네임 길이/공백 검증 실패":{"value":{"data":null,"detail":"nickname: 닉네임은 1자 이상 10자 이하여야 한다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"빈 이미지 파일":{"value":{"data":null,"detail":"빈 이미지 파일은 업로드할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"지원하지 않는 이미지 형식":{"value":{"data":null,"detail":"지원하지 않는 이미지 형식입니다. (지원: image/png, image/jpeg, image/webp, image/heic, image/heif)","pageResponse":{"nextCursor":null,"hasNext":false}}},"형식과 내용 불일치":{"value":{"data":null,"detail":"이미지 파일이 손상되었거나 형식과 내용이 일치하지 않습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (GUEST 가 프로필 이미지 수정을 시도 — 이미지 수정은 MEMBER 전용)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"게스트의 프로필 이미지 수정 거부":{"value":{"data":null,"detail":"프로필 이미지는 회원만 수정할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"유저를 찾을 수 없음 (JWT 유효하지만 DB에서 유저가 삭제된 경우)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유저 없음 (JWT 유효하나 DB에 없음)":{"value":{"data":null,"detail":"리소스 없음 — 존재하지 않는 대상","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (닉네임 중복 · 탈퇴한 유저)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"닉네임 중복":{"value":{"data":null,"detail":"이미 사용 중인 닉네임입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"탈퇴한 유저":{"value":{"data":null,"detail":"탈퇴한 유저입니다. userId=8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"413":{"description":"파일 크기가 허용 한도(multipart max-file-size)를 초과함","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"파일 크기 초과":{"value":{"data":null,"detail":"입력 오류 — 요청을 수정하여 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"502":{"description":"외부 의존성 실패 (이미지 저장소(S3) 업로드 실패 — 재시도 가능)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"이미지 저장소(S3) 업로드 실패":{"value":{"data":null,"detail":"이미지 저장에 실패했습니다. 잠시 후 다시 시도해 주세요.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/tournaments/{tournamentId}/items/{tournamentItemId}":{"get":{"tags":["Tournament Item"],"summary":"토너먼트 아이템 단건 조회","description":"\n            토너먼트 아이템의 상세 정보와 파싱 상태를 조회한다.\n            링크·이미지로 아이템을 추가하면 비동기 파싱이 진행되므로,\n            클라이언트가 status 필드를 폴링해 PENDING → PROCESSING → READY/FAILED 전환을 감지할 수 있다.\n            - PENDING: URL 등록 접수 후 파싱 대기 (디스패처가 집기 전, name·price·imageUrl 은 null). 이미지 등록은 PROCESSING 으로 시작.\n            - PROCESSING: 파싱 진행 중 (name·price·imageUrl 은 null)\n            - READY: 파싱 완료 (모든 필드 채워짐)\n            - FAILED: 파싱 실패 (상품 페이지 아님 또는 추출 불가)\n            sourceUrl: 등록 시 입력한 원본 링크. 이미지로 등록한 경우 null.\n            토너먼트 참여자만 조회할 수 있다.\n        ","operationId":"getTournamentItem","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1},{"name":"tournamentItemId","in":"path","description":"토너먼트 아이템 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":10}],"responses":{"200":{"description":"아이템 조회 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"READY - 파싱 완료 (링크 등록)":{"value":{"data":{"tournamentItemId":10,"itemId":100,"sourceUrl":"https://www.nike.com/kr/t/air-max/example","name":"나이키 에어맥스","imageUrl":"https://cdn.example.com/items/1.jpg","price":129000,"currency":"KRW","status":"READY"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"READY - 파싱 완료 (이미지 등록, sourceUrl=null)":{"value":{"data":{"tournamentItemId":11,"itemId":101,"name":"아디다스 울트라부스트","imageUrl":"https://cdn.example.com/items/2.jpg","price":189000,"currency":"KRW","status":"READY"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"PENDING - 파싱 대기 (URL 등록 직후)":{"value":{"data":{"tournamentItemId":10,"itemId":100,"sourceUrl":"https://www.nike.com/kr/t/air-max/example","status":"PENDING"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"PROCESSING - 파싱 진행 중":{"value":{"data":{"tournamentItemId":10,"itemId":100,"sourceUrl":"https://www.nike.com/kr/t/air-max/example","status":"PROCESSING"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"FAILED - 파싱 실패":{"value":{"data":{"tournamentItemId":10,"itemId":100,"sourceUrl":"https://www.nike.com/kr/t/air-max/example","status":"FAILED"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트 권한 없음":{"value":{"data":null,"detail":"해당 토너먼트에 대한 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음 · 토너먼트 아이템을 찾을 수 없음 · 아이템이 해당 토너먼트에 속하지 않음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"토너먼트 아이템을 찾을 수 없음 · 아이템이 해당 토너먼트에 속하지 않음":{"value":{"data":null,"detail":"토너먼트 아이템을 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}},"delete":{"tags":["Tournament Item"],"summary":"토너먼트 아이템 삭제","description":"PENDING 상태의 토너먼트에서 아이템을 제거한다. 아이템을 추가한 본인 또는 토너먼트 소유자만 삭제할 수 있다.","operationId":"deleteItem","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1},{"name":"tournamentItemId","in":"path","description":"토너먼트 아이템 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":10}],"responses":{"200":{"description":"아이템 삭제 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"아이템 삭제 성공":{"value":{"data":null,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (아이템을 추가한 본인도 아니고 토너먼트 소유자도 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트 권한 없음":{"value":{"data":null,"detail":"해당 토너먼트에 대한 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음 · 토너먼트 아이템을 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"토너먼트 아이템을 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트 아이템을 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (PENDING이 아닌 토너먼트)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"PENDING 상태 아님":{"value":{"data":null,"detail":"PENDING 상태인 토너먼트에만 수행할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}},"patch":{"tags":["Tournament Item"],"summary":"토너먼트 아이템 수정","description":"\n            파싱 실패(FAILED) 상태인 토너먼트 아이템을 유저가 직접 보정한다.\n            수정 성공 시 아이템 상태가 FAILED → READY 로 전환된다.\n            수정 가능 필드: 이름, 가격, 가격 단위, 이미지(multipart/form-data 의 image 파트) — null 이면 기존 값 유지.\n            이미지는 파일로 업로드하며 서버가 S3 에 저장한 URL 로 item.imageUrl 을 갱신한다.\n            READY·PENDING·PROCESSING 아이템은 수정 불가(409). 아이템을 등록한 본인만 수정 가능.\n            이름은 수정 후에도 반드시 존재해야 한다 — 기존 이름이 없고 name 도 미입력이면 400.\n        ","operationId":"updateItem","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1},{"name":"tournamentItemId","in":"path","description":"토너먼트 아이템 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":10}],"requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UpdateTournamentItemRequest"}}}},"responses":{"200":{"description":"수정 성공 (FAILED → READY 전환)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"수정 성공 (FAILED → READY)":{"value":{"data":null,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (이름 없이 수정 시도 · 이름 1자 미만 512자 초과 · 가격 음수 · 가격단위 8자 초과 · 빈 이미지 · 이미지 타입 미지정 · 지원하지 않는 이미지 형식(png/jpeg/webp/heic/heif만 허용))","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"상품명 없이 보정 시도":{"value":{"data":null,"detail":"상품명을 입력해야 합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님 · 아이템을 등록한 본인이 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트 권한 없음":{"value":{"data":null,"detail":"해당 토너먼트에 대한 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음 · 토너먼트 아이템을 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"토너먼트 아이템을 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트 아이템을 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (PENDING이 아닌 토너먼트 · READY·PENDING·PROCESSING 상태 아이템)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"PENDING 상태 아님":{"value":{"data":null,"detail":"PENDING 상태인 토너먼트에만 수행할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"이미 등록 완료(READY) 항목 — 수정 불가":{"value":{"data":null,"detail":"이미 등록 완료된 상품은 수정할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"아직 처리 중(PROCESSING) 항목 — 수정 불가":{"value":{"data":null,"detail":"아직 처리 중인 상품은 수정할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"502":{"description":"외부 의존성 실패 (이미지 저장소(S3) 업로드 실패 — 재시도 가능)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"이미지 저장 실패":{"value":{"data":null,"detail":"이미지 저장에 실패했습니다. 잠시 후 다시 시도해 주세요.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/tournaments/{tournamentId}/invite":{"patch":{"tags":["Tournament"],"summary":"친구 초대 마감 시각 수정","description":"PENDING 상태 토너먼트의 초대 마감 시각을 지금으로부터 N분 후로 재설정한다. 주최자만 가능. 최소 1분, 최대 1440분(24시간).","operationId":"updateInviteExpiry","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInviteDurationRequest"}}},"required":true},"responses":{"200":{"description":"수정 성공 (새 inviteExpiresAt 반환)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"초대 마감 시각 수정 성공":{"value":{"data":"2026-06-07T11:00:00Z","detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (inviteDurationMinutes 1 미만 또는 1440 초과)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유효 시간 범위 초과":{"value":{"data":null,"detail":"inviteDurationMinutes: 초대 유효 시간은 1440분(24시간) 이하이어야 합니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님 · 소유자가 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"권한 없음 (소유자 아님)":{"value":{"data":null,"detail":"해당 토너먼트에 대한 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (PENDING이 아닌 토너먼트)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"PENDING이 아닌 토너먼트":{"value":{"data":null,"detail":"PENDING 상태인 토너먼트에만 수행할 수 있습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/health":{"get":{"tags":["health-controller"],"operationId":"health","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}},"security":[]}},"/api/v1/users/nickname/check":{"get":{"tags":["User"],"summary":"닉네임 중복 체크","description":"닉네임이 이미 다른 유저에게 점유됐는지 확인한다. 회원 전환 / 닉네임 수정 전 사전 확인용.\n\n- 본인의 현재 닉네임은 중복으로 잡지 않는다 — 자기 닉네임 유지 / 자기 닉네임으로 재확인 흐름이 자연스럽게 통과한다.","operationId":"checkNickname","parameters":[{"name":"request","in":"query","required":true,"schema":{"$ref":"#/components/schemas/NicknameCheckRequest"}}],"responses":{"200":{"description":"확인 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"사용 가능":{"value":{"data":{"available":true},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"이미 사용 중":{"value":{"data":{"available":false},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"닉네임 형식 검증 실패","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"닉네임 형식 검증 실패":{"value":{"data":null,"detail":"nickname: nickname 은 10자 이하여야 한다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/tournaments/{tournamentId}":{"get":{"tags":["Tournament"],"summary":"토너먼트 단건 조회","description":"\n            토너먼트 ID로 상태에 따른 상세 정보를 조회한다.\n            isOwner: 요청자가 해당 토너먼트 인스턴스(ROOT 또는 CLONE)의 소유자이면 true.\n            isRoot: 소셜 토너먼트 원본(ROOT)이면 true, 멤버·플레이링크용 복사본(CLONE)이면 false. 솔로 토너먼트는 항상 true.\n            sourceTournamentId: CLONE 토너먼트이면 원본 ROOT의 id, ROOT이면 null. 게스트는 이 id로 GET /tournaments/{sourceTournamentId}/group-result를 호출한다.\n            플레이 링크 공유는 isRoot && isOwner 일 때만 허용된다. isOwner 단독으로 분기하면 CLONE 소유자에게도 공유 버튼이 노출되어 시안 위반이다.\n            응답의 status 필드에 따라 포함되는 데이터가 달라진다.\n            - PENDING: pending 필드 (아이템 목록, 참여자 목록)\n              - 각 아이템에 status 포함 (READY / PENDING / PROCESSING / FAILED). PENDING·PROCESSING 이면 name·price·imageUrl 은 null 이라 응답에서 제외됨\n              - pending.ownerStarted = false\n            - IN_PROGRESS: 요청자 역할에 따라 두 가지 응답이 있다.\n              - 소유자(isOwner=true) 또는 이미 매치를 시작한 멤버: inProgress 필드\n                - currentRound: 다음에 진행할 라운드 번호\n                - lastHistory: 가장 최근에 기록된 매치 결과. 라운드 전환 직후에는 currentRound와 다른 라운드의 매치일 수 있음. 매치 기록이 없으면 null\n                - remainingItems: 현재 라운드에서 아직 대결하지 않은 생존 아이템 목록, 가격 오름차순. 이 순서가 클라이언트의 매치 페어링 순서([0]vs[1], [2]vs[3] …)를 결정함\n              - 아직 매치를 시작하지 않은 멤버(isOwner=false): pending 필드 (ROOT 아이템·참여자 목록)\n                - pending.ownerStarted = true. 클라이언트는 이 플래그로 \"주최자 대기\" vs \"주최자 시작 완료·지금 시작하세요\" UI 를 분기한다\n                - pending.inviteCode, pending.inviteExpiresAt 은 null (초대 기간 종료)\n            - COMPLETED: completed 필드\n              - result: 1위부터 최대 4위까지 순위 아이템 목록\n              - hasGroupResult: 참여자 2명 이상이면 true. 클라이언트는 이 값으로 친구 토너먼트 결과 보기 버튼을 제어한다.\n            나머지 필드는 응답에 포함되지 않는다.\n        ","operationId":"getTournamentById","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"responses":{"200":{"description":"토너먼트 조회 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"PENDING - 아이템 담는 중":{"value":{"data":{"tournamentId":1,"name":"내 토너먼트","status":"PENDING","isOwner":true,"isRoot":true,"pending":{"inviteCode":"ABC123","inviteExpiresAt":"2026-05-30T15:00:00Z","items":[{"tournamentItemId":1,"itemId":10,"name":"나이키 에어맥스","price":129000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/1.jpg","status":"READY"},{"tournamentItemId":2,"itemId":20,"name":"아디다스 울트라부스트","price":189000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/2.jpg","status":"READY"}],"participants":[{"userId":"11111111-2222-3333-4444-555555555555","nickname":"참여자1","profileImage":"https://cdn.example.com/profiles/user1.jpg","isWithdrawn":false}],"ownerStarted":false}},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"IN_PROGRESS - 멤버 대기 중 (주최자 시작 완료, 본인 미시작)":{"value":{"data":{"tournamentId":1,"name":"내 토너먼트","status":"IN_PROGRESS","isOwner":false,"isRoot":true,"pending":{"items":[{"tournamentItemId":1,"itemId":10,"name":"나이키 에어맥스","price":129000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/1.jpg","status":"READY"},{"tournamentItemId":2,"itemId":20,"name":"아디다스 울트라부스트","price":189000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/2.jpg","status":"READY"}],"participants":[{"userId":"11111111-2222-3333-4444-555555555555","nickname":"주최자","profileImage":"https://cdn.example.com/profiles/user1.jpg","isWithdrawn":false},{"userId":"22222222-3333-4444-5555-666666666666","nickname":"참여자","profileImage":"https://cdn.example.com/profiles/user2.jpg","isWithdrawn":false}],"ownerStarted":true}},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"IN_PROGRESS - 진행 중 복원 (CLONE)":{"value":{"data":{"tournamentId":2,"name":"내 토너먼트","status":"IN_PROGRESS","isOwner":false,"isRoot":false,"sourceTournamentId":1,"inProgress":{"currentRound":4,"lastHistory":{"currentRound":4,"firstTournamentItemId":1,"secondTournamentItemId":2,"selectedTournamentItemId":1},"remainingItems":[{"tournamentItemId":3,"itemId":30,"name":"뉴발란스 993","price":259000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/3.jpg","status":"READY"},{"tournamentItemId":4,"itemId":40,"name":"살로몬 XT-6","price":279000,"currency":"KRW","status":"READY"}]}},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"COMPLETED - 최종 결과":{"value":{"data":{"tournamentId":1,"name":"내 토너먼트","status":"COMPLETED","isOwner":true,"isRoot":true,"completed":{"result":[{"rank":1,"tournamentItemId":1,"itemId":10,"name":"나이키 에어맥스","price":129000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/1.jpg"},{"rank":2,"tournamentItemId":2,"itemId":20,"name":"아디다스 울트라부스트","price":189000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/2.jpg"},{"rank":3,"tournamentItemId":3,"itemId":30,"name":"뉴발란스 993","price":259000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/3.jpg"},{"rank":4,"tournamentItemId":4,"itemId":40,"name":"살로몬 XT-6","price":279000,"currency":"KRW"}],"hasGroupResult":true,"playLinkExpiresAt":"2026-06-20T22:00:00Z"}},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트 권한 없음":{"value":{"data":null,"detail":"해당 토너먼트에 대한 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}},"delete":{"tags":["Tournament"],"summary":"토너먼트 삭제","description":"토너먼트를 소유자 목록에서 제거한다. 소유자만 호출 가능하며, IN_PROGRESS 상태에서는 불가. 토너먼트 데이터(아이템·히스토리)는 유지되어 다른 참여자들은 계속 접근할 수 있다.","operationId":"deleteTournament","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"responses":{"200":{"description":"토너먼트 삭제 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트 삭제 성공":{"value":{"data":null,"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님 · 토너먼트 소유자가 아님)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트 권한 없음":{"value":{"data":null,"detail":"해당 토너먼트에 대한 권한이 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"토너먼트를 찾을 수 없음":{"value":{"data":null,"detail":"토너먼트를 찾을 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (IN_PROGRESS 토너먼트는 삭제 불가)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"진행 중 토너먼트 삭제 불가":{"value":{"data":null,"detail":"진행 중인 토너먼트는 삭제할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/tournaments/{tournamentId}/play-link-info":{"get":{"tags":["Tournament"],"summary":"플레이 링크로 참여 화면 진입","description":"플레이 링크가 유효한 토너먼트의 정보(이름, 아이템 수, 만료 시간)를 반환한다. 인증 불필요.","operationId":"getPlayLinkInfo","parameters":[{"name":"tournamentId","in":"path","description":"원본 토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"responses":{"200":{"description":"조회 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"플레이 링크 정보 조회 성공":{"value":{"data":{"sourceTournamentId":1,"tournamentName":"내 토너먼트","itemCount":8,"playLinkExpiresAt":"2026-06-09T22:00:00Z"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음 · 플레이 링크가 생성되지 않은 토너먼트","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}},"409":{"description":"플레이 링크 만료","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}}}}},"/api/v1/tournaments/{tournamentId}/invite-preview":{"get":{"tags":["Tournament"],"summary":"초대 링크로 참여 화면 진입","description":"초대 링크 직접 접근 시 tournamentId 만으로 토너먼트 정보(이름·아이템 수·참여자 수)를 반환한다. 인증 불필요.","operationId":"getInvitePreview","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"responses":{"200":{"description":"조회 성공 (토너먼트 이름·아이템 수·참여자 수 반환)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"링크 접근 미리보기 성공":{"value":{"data":{"tournamentId":1,"tournamentName":"내 토너먼트","itemCount":8,"participantCount":2},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}},"409":{"description":"상태 충돌 (PENDING이 아닌 토너먼트 · 초대 링크 만료)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}}}}},"/api/v1/tournaments/{tournamentId}/group-result":{"get":{"tags":["Tournament"],"summary":"그룹 결과 조회","description":"\n            완료된 토너먼트의 그룹 결과를 조회한다.\n            원본 토너먼트와 플레이 링크로 복제된 모든 토너먼트의 결과를 비교해,\n            각 순위의 아이템마다 동일한 결과를 선택한 참여자 정보를 반환한다.\n        ","operationId":"getGroupResult","parameters":[{"name":"tournamentId","in":"path","description":"토너먼트 ID","required":true,"schema":{"type":"integer","format":"int64"},"example":1}],"responses":{"200":{"description":"그룹 결과 조회 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"그룹 결과 조회 성공":{"value":{"data":{"items":[{"rank":1,"itemId":10,"name":"나이키 에어맥스","price":129000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/1.jpg","chosenBy":[{"userId":"11111111-2222-3333-4444-555555555555","nickname":"참여자A","profileImage":"https://piki-assets.s3.ap-northeast-2.amazonaws.com/defaults/user-profile-3.png","isWithdrawn":false},{"userId":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","nickname":"탈퇴aaaaaaaa","profileImage":"https://api.dicebear.com/9.x/bottts/svg?seed=default","isWithdrawn":true}]},{"rank":2,"itemId":20,"name":"아디다스 울트라부스트","price":189000,"currency":"KRW","imageUrl":"https://cdn.example.com/items/2.jpg","chosenBy":[{"userId":"11111111-2222-3333-4444-555555555555","nickname":"참여자A","profileImage":"https://piki-assets.s3.ap-northeast-2.amazonaws.com/defaults/user-profile-3.png","isWithdrawn":false}]}]},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}},"403":{"description":"권한 없음 (토너먼트 참여자가 아님 · 플레이링크로 참여한 복제 토너먼트는 그룹 결과 조회 불가)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"플레이링크 복제 토너먼트에서 그룹 결과 조회 불가":{"value":{"data":null,"detail":"플레이 링크로 참여한 토너먼트에서는 친구 결과를 조회할 수 없습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"토너먼트를 찾을 수 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}},"409":{"description":"상태 충돌 (COMPLETED가 아닌 토너먼트)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"}}}}}}},"/api/v1/tournaments/by-invite-code":{"get":{"tags":["Tournament"],"summary":"초대 코드로 참여 화면 진입","description":"\n            홈 다이얼로그에서 6자리 코드만 입력하는 경로 전용 — tournamentId 없이 코드만으로 조회한다.\n            코드가 유효하면 tournamentId·이름·아이템 수·참여자 수를 반환한다.\n            이후 /join 또는 /join/guest 호출 시 응답의 tournamentId를 사용하면 된다.\n        ","operationId":"getInvitePreviewByCode","parameters":[{"name":"code","in":"query","description":"초대 코드 (영어 대문자 3자리 + 숫자 3자리)","required":true,"schema":{"type":"string"},"example":"ABC123"}],"responses":{"200":{"description":"조회 성공 (tournamentId·토너먼트 이름·아이템 수·참여자 수 반환)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"코드 조회 성공":{"value":{"data":{"tournamentId":1,"tournamentName":"내 토너먼트","itemCount":8,"participantCount":2},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (코드에 해당하는 토너먼트 없음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"코드에 해당하는 토너먼트 없음":{"value":{"data":null,"detail":"초대 코드가 올바르지 않습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"상태 충돌 (PENDING이 아닌 토너먼트 · 초대 링크 만료)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"초대 링크 만료":{"value":{"data":null,"detail":"초대 링크가 만료되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/notifications":{"get":{"tags":["Notification"],"summary":"알림 히스토리 조회","description":"로그인한 유저 본인의 알림을 **최신순(`id` desc)** 으로 조회한다 (**GUEST·MEMBER 모두**).\n\n**커서 페이지네이션**\n\n- 직전 응답의 `pageResponse.nextCursor` 를 다음 요청 `cursor` 로 그대로 전달한다.\n- 마지막 페이지면 `nextCursor` 는 `null`, `hasNext` 는 `false`.\n- `size` 는 미지정 시 20, 1~50 범위를 벗어나면 양 끝으로 보정된다.\n\n**카테고리 필터**\n\n- `category` 미지정 시 전체. `ACTIVITY`(활동: 토너먼트 소셜 알림) / `SYSTEM`(시스템: 파싱·공지)로 탭 필터.\n- `unreadCount` 는 category 와 무관하게 항상 전체 안읽음 수(앱 badge)다.\n- `unreadCountByCategory` 로 탭별 안읽음 수(탭 badge)를 함께 내려준다 — 모든 카테고리 키 포함, 없으면 0.\n\n**응답 활용**\n\n- 응답 `data` 의 `unreadCount`(앱 badge) · `unreadCountByCategory`(탭 badge)로 안읽음 수를 함께 내려준다 (별도 카운트 API 없음).\n- 각 항목의 `imageUrl` 은 항상 채워진다 — 사람 알림은 발송 시점 프사, 시스템 알림은 피키 로고. 사람/시스템 구분은 `category`. 클라는 `imageUrl` 을 그대로 아바타로 렌더한다.\n- 각 항목 셰입은 SSE `notification` 이벤트 payload 와 동일하다 — `type` 으로 화면을, `refId` 로 딥링크 이동을, 파싱 알림은 `kind` 로 출처(위시/토너먼트)를 분기하고, `id` 로 단건 읽음 처리(`POST /read`)를 한다.\n\n**알림 타입 카탈로그 (전 9종)**\n\n`type` 으로 화면을 분기하고 `refId` 로 이동 대상을 정한다. `body` 는 현재 전 타입 빈 문자열(`\"\"`).\n\n| `type` | 트리거 | 카테고리 | `refId` | `title` 예시 | 아바타(`imageUrl`) |\n|---|---|---|---|---|---|\n| `TOURNAMENT_JOINED` | 토너먼트 참가 | ACTIVITY | tournamentId | {참가자}님이 참가했어요 | 행위자 프사 |\n| `TOURNAMENT_ITEM_ADDED` | 아이템 추가 | ACTIVITY | tournamentId | {참가자}님이 아이템을 추가했어요 | 행위자 프사 |\n| `TOURNAMENT_STARTED` | 토너먼트 시작 | ACTIVITY | tournamentId | {주최자}님이 토너먼트를 시작했어요 | 행위자 프사 |\n| `TOURNAMENT_PLAYED_FROM_LINK` | 플레이링크로 플레이 시작 | ACTIVITY | ROOT 토너먼트 id | {플레이어}님이 회원님 토너먼트를 플레이했어요 | 행위자 프사 |\n| `TOURNAMENT_COMPLETED` | 멤버가 클론 완료 | ACTIVITY | ROOT 토너먼트 id | {멤버}님이 회원님 토너먼트를 완료했어요 | 행위자 프사 |\n| `TOURNAMENT_RESULT_READY` | 주최자가 ROOT 완료 | ACTIVITY | ROOT 토너먼트 id | 참여하신 {주최자}님의 토너먼트 결과가 나왔어요 | 주최자 프사 |\n| `ITEM_PARSING_COMPLETED` | 상품 추출 성공 | SYSTEM | itemId | 상품 정보가 저장됐어요 | 피키 로고 |\n| `ITEM_PARSING_FAILED` | 상품 추출 실패 | SYSTEM | itemId | 상품 정보를 가져오지 못했어요 | 피키 로고 |\n| `ANNOUNCEMENT` | 관리자 공지(후속) | SYSTEM | 공지 id/0 | (관리자 입력) | 피키 로고 |\n\n> 파싱 알림(`ITEM_PARSING_*`)만 출처별 `kind`(WISH/TOURNAMENT)·`tournamentId`·`tournamentItemId` 가 추가로 실린다. 나머지 타입엔 그 키가 없다. `title` 은 발송 시점 렌더 값이라 클라는 문구가 아니라 `type` 으로 분기한다.","operationId":"getHistory","parameters":[{"name":"cursor","in":"query","description":"직전 응답의 nextCursor (없으면 첫 페이지)","required":false,"schema":{"type":"string"},"example":1010},{"name":"size","in":"query","description":"페이지 크기 (기본 20, 최대 50)","required":false,"schema":{"type":"integer","format":"int32"},"example":20},{"name":"category","in":"query","description":"카테고리 필터 (미지정 시 전체). ACTIVITY(활동) / SYSTEM(시스템)","required":false,"schema":{"type":"string","enum":["ACTIVITY","SYSTEM"]},"example":"ACTIVITY"}],"responses":{"200":{"description":"조회 성공 (목록 + unreadCount)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"조회 성공 (안읽음·읽음 혼재, 마지막 페이지)":{"value":{"data":{"items":[{"id":1026,"type":"TOURNAMENT_JOINED","category":"ACTIVITY","title":"토너먼트에 참가했어요","body":"지금 바로 픽을 시작해보세요","imageUrl":"https://images.piki/profiles/abc/3f7c.png","refId":77,"isRead":false,"createdAt":"2026-06-08T10:10:00Z"},{"id":1025,"type":"ITEM_PARSING_COMPLETED","category":"SYSTEM","title":"상품 정보가 준비됐어요","body":"에어 조던 1 미드","imageUrl":"https://images.piki/defaults/push-icon.svg","refId":512,"isRead":true,"createdAt":"2026-06-08T10:05:00Z","kind":"WISH"},{"id":1024,"type":"ITEM_PARSING_COMPLETED","category":"SYSTEM","title":"토너먼트 아이템이 준비됐어요","body":"나이키 덩크 로우","imageUrl":"https://images.piki/defaults/push-icon.svg","refId":513,"isRead":false,"createdAt":"2026-06-08T10:00:00Z","tournamentId":99,"tournamentItemId":555,"kind":"TOURNAMENT"}],"unreadCount":2,"unreadCountByCategory":{"ACTIVITY":1,"SYSTEM":1}},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}},"조회 성공 (다음 페이지 있음)":{"value":{"data":{"items":[{"id":1024,"type":"ITEM_PARSING_COMPLETED","category":"SYSTEM","title":"토너먼트 아이템이 준비됐어요","body":"나이키 덩크 로우","imageUrl":"https://images.piki/defaults/push-icon.svg","refId":513,"isRead":false,"createdAt":"2026-06-08T10:00:00Z","tournamentId":99,"tournamentItemId":555,"kind":"TOURNAMENT"}],"unreadCount":1,"unreadCountByCategory":{"ACTIVITY":0,"SYSTEM":1}},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":"1024","hasNext":true}}},"빈 알림함":{"value":{"data":{"items":[],"unreadCount":0,"unreadCountByCategory":{"ACTIVITY":0,"SYSTEM":0}},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"유효하지 않은 cursor 값 (숫자로 변환 불가) · 유효하지 않은 category 값 (ACTIVITY/SYSTEM 외)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유효하지 않은 cursor":{"value":{"data":null,"detail":"유효하지 않은 cursor 입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"인증 필요":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/notifications/subscribe":{"get":{"tags":["Notification"],"summary":"알림 실시간 구독 (SSE)","description":"인증 유저의 알림을 실시간으로 받는 **SSE(Server-Sent Events)** 스트림을 연다.\n\n응답은 `ApiResponseBody` JSON 래퍼가 아니라 `text/event-stream` 스트림이며, 다음 이벤트가 흘러온다.\n\n| 이벤트 | 시점 | 내용 |\n|---|---|---|\n| `connect` | 구독 직후 1회 | `data=\"connected\"`. 연결 성립 신호 |\n| `notification` | 알림 1건마다 | `type` 으로 화면을, 파싱 알림은 `kind` 로 출처(위시/토너먼트)를 분기. 출처별 payload 셰입과 라우팅 필드(`kind`·`tournamentId`·`tournamentItemId`)는 `notification-sse-spec.md` 참조 |\n| `(주석 ping)` | 약 30초 간격 | 하트비트. 연결 유지용이며 data 이벤트가 아니다 |\n\n- 토너먼트 알림은 해당 토너먼트 참여자에게만 fan-out 되므로, **자기 스트림 1개만 구독**하면 토너먼트·개인 알림이 모두 도착한다.\n- 연결은 **30분 후 타임아웃**되며, 클라이언트는 끊기면 재연결한다.","operationId":"subscribe","responses":{"200":{"description":"SSE 스트림 시작 (`text/event-stream`). `notification` 이벤트 data payload 는 알림 종류별로 셰입이 다르고(파싱 알림은 출처별 `kind`·`tournamentId`·`tournamentItemId`), 스트림·다형 구조라 OpenAPI 로 표현이 어려워 `notification-sse-spec.md` 로 문서화한다.","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/SseEmitter"}}}},"401":{"description":"미인증 (JWT 토큰 없음 또는 유효하지 않음)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"미인증":{"value":{"data":null,"detail":"인증 필요 — 로그인 후 재시도","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/dev/users/{userId}":{"get":{"tags":["Dev"],"summary":"단건 유저 조회","description":"userId 로 유저 정보와 AT·RT 를 발급해 반환한다. 개발 편의용.","operationId":"getUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"유저 정보 및 AT·RT 반환","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"유저 + 토큰 발급 성공":{"value":{"data":{"user":{"id":"3b9c1d2e-4f5a-4b6c-8d7e-9f0a1b2c3d4e","nickname":"홍길동","profileImage":"https://piki-assets.s3.ap-northeast-2.amazonaws.com/defaults/user-profile-2.png","identityType":"MEMBER"},"accessToken":"eyJhbGciOiJIUzI1NiJ9.access","refreshToken":"eyJhbGciOiJIUzI1NiJ9.refresh"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"404":{"description":"userId 에 해당하는 유저 없음","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"userId 에 해당하는 유저 없음":{"value":{"data":null,"detail":"리소스 없음 — 존재하지 않는 대상","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"409":{"description":"탈퇴(soft delete) 된 유저 — 토큰 발급 거부","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"탈퇴된 유저 — 토큰 발급 거부":{"value":{"data":null,"detail":"상태 충돌 — 입력을 바꿔도 해소되지 않음","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}}}},"/api/v1/auth/{provider}/url":{"get":{"tags":["Auth"],"summary":"OAuth 인가 URL 생성","description":"provider 인가 페이지 URL 과 CSRF 방지용 `state` 를 반환한다.\n\n**FE 흐름**\n\n1. 이 URL 로 사용자를 redirect 한다.\n2. provider 콜백에서 받은 `state` 가 응답의 `state` 와 일치하는지 검증한다.\n3. `POST /auth/login/{provider}` 에 `code` · `redirectUri` · `state` 를 전송한다.","operationId":"getAuthUrl","parameters":[{"name":"provider","in":"path","description":"소셜 제공자","required":true,"schema":{"type":"string","enum":["kakao","google","apple"]},"example":"kakao"},{"name":"redirectUri","in":"query","description":"redirect_uri 동적 지정 (생략 시 서버 기본값 사용). 로컬 개발 등 프로덕션과 다른 콜백 URL 이 필요할 때 사용.","required":false,"schema":{"type":"string"},"example":"http://localhost:3000/auth/callback/google"}],"responses":{"200":{"description":"인가 URL + state 생성 성공","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"카카오 인가 URL 생성 성공":{"value":{"data":{"url":"https://kauth.kakao.com/oauth/authorize?client_id=kakao-client-id&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback%2Fkakao&response_type=code&state=550e8400-e29b-41d4-a716-446655440000","state":"550e8400-e29b-41d4-a716-446655440000"},"detail":"정상적으로 처리되었습니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}},"400":{"description":"잘못된 요청 (지원하지 않는 provider)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponseBody"},"examples":{"지원하지 않는 provider":{"value":{"data":null,"detail":"지원하지 않는 소셜 로그인 제공자입니다.","pageResponse":{"nextCursor":null,"hasNext":false}}}}}}}},"security":[]}}},"components":{"schemas":{"WishlistRegisterRequest":{"type":"object","description":"위시리스트 등록 요청","properties":{"url":{"type":"string","description":"등록할 상품 페이지 URL","example":"https://www.example-shop.com/products/12345","maxLength":2048,"minLength":0}},"required":["url"]},"ApiResponseBody":{"type":"object","properties":{"data":{"type":"null"},"detail":{"type":"string"},"pageResponse":{"$ref":"#/components/schemas/PageResponse"}}},"PageResponse":{"type":"object","properties":{"nextCursor":{"type":["string","null"],"description":"다음 페이지 조회용 커서. 다음 페이지가 없으면 null","example":"1024"},"hasNext":{"type":"boolean","description":"다음 페이지 존재 여부","example":false}}},"CreateTournamentRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1},"inviteDurationMinutes":{"type":["integer","null"],"format":"int64","maximum":1440,"minimum":1}},"required":["name"]},"RecordMatchRequest":{"type":"object","properties":{"currentRound":{"type":"integer","format":"int32"},"firstTournamentItemId":{"type":"integer","format":"int64"},"secondTournamentItemId":{"type":"integer","format":"int64"},"selectedTournamentItemId":{"type":"integer","format":"int64"}}},"JoinTournamentRequest":{"type":"object","properties":{"inviteCode":{"type":["string","null"],"pattern":"[A-Z]{3}\\d{3}"}}},"JoinTournamentAsGuestRequest":{"type":"object","properties":{"inviteCode":{"type":["string","null"],"pattern":"[A-Z]{3}\\d{3}"},"nickname":{"type":"string","maxLength":10,"minLength":0}},"required":["nickname"]},"AddTournamentItemsRequest":{"type":"object","properties":{"itemIds":{"type":"array","items":{"type":"integer","format":"int64"},"maxItems":32,"minItems":1}}},"AddTournamentItemFromLinkRequest":{"type":"object","properties":{"url":{"type":"string","description":"등록할 상품 페이지 URL","example":"https://www.example-shop.com/products/12345","maxLength":2048,"minLength":0}},"required":["url"]},"NotificationReadRequest":{"type":"object","description":"알림 읽음 처리 요청 — all=true(전체) 또는 ids(지정) 중 정확히 하나","properties":{"all":{"type":["boolean","null"],"description":"true 면 본인 안읽음 알림 전부 읽음 처리 (전체 읽음 버튼). ids 와 동시 사용 불가","example":true},"ids":{"type":["array","null"],"description":"읽음 처리할 알림 id 목록 (단건 클릭은 [id] 1개). all 과 동시 사용 불가","example":[1024],"items":{"type":"integer","format":"int64"}}}},"FcmTokenRegisterRequest":{"type":"object","description":"FCM 토큰 등록 요청 (로그인·알림설정 ON·토큰 갱신 시 호출)","properties":{"token":{"type":"string","description":"FCM 등록 토큰 (Firebase iOS SDK 가 발급)","example":"fGcServerTokenSample:APA91bF...","maxLength":512,"minLength":0},"deviceId":{"type":"string","description":"기기 식별자 (iOS IDFV — UIDevice.current.identifierForVendor)","example":"1B2A3C4D-5E6F-7A8B-9C0D-1E2F3A4B5C6D","maxLength":255,"minLength":0}},"required":["deviceId","token"]},"DevUserCreateRequest":{"type":"object","description":"개발용 MEMBER 생성 요청","properties":{"nickname":{"type":"string","description":"닉네임","example":"홍길동","minLength":1}},"required":["nickname"]},"DevPushRequest":{"type":"object","description":"[DEV] FCM 즉시 발송 요청 — 본문의 토큰으로 바로 푸시(등록 불필요). FE 가 Xcode 에서 받은 토큰을 붙여 발송 경로를 확인한다.","properties":{"token":{"type":"string","description":"발송할 FCM 토큰 (Xcode 등에서 실시간으로 받은 값)","example":"fGcServerTokenSample:APA91bF...","maxLength":512,"minLength":0},"title":{"type":"string","default":"PIKI 테스트 알림","description":"푸시 제목","example":"PIKI 테스트 알림","maxLength":255,"minLength":0},"body":{"type":"string","default":"FCM 발송이 정상 동작하는지 확인하는 테스트 메시지입니다.","description":"푸시 본문","example":"FCM 발송이 정상 동작하는지 확인하는 테스트 메시지입니다.","maxLength":255,"minLength":0}},"required":["token"]},"TokenRefreshRequest":{"type":"object","description":"토큰 갱신 요청","properties":{"refreshToken":{"type":"string","description":"리프레시 토큰","minLength":1}},"required":["refreshToken"]},"OAuthLoginRequest":{"type":"object","description":"소셜 로그인 요청 — v1(웹): code+redirectUri / v2(SDK): accessToken","properties":{"code":{"type":["string","null"],"description":"v1 웹 흐름 — provider 가 redirect 로 준 인가 코드"},"redirectUri":{"type":["string","null"],"description":"v1 웹 흐름 — 코드 발급에 사용한 redirect_uri (provider 에 등록된 값과 일치)"},"accessToken":{"type":["string","null"],"description":"v2 SDK 흐름 — 모바일 SDK 가 받은 access_token"},"state":{"type":["string","null"],"description":"CSRF 방지용 state. GET /auth/{provider}/url 로 발급받은 값을 전송 (v1 웹 흐름 권장·v2 선택). 미전송 시 state 검증을 생략한다."}}},"WishlistUpdateRequest":{"type":"object","description":"위시 항목 수정 요청 — 들어온 필드만 갱신한다","properties":{"name":{"type":["string","null"],"description":"수정할 상품명","example":"에어 조던 1 미드","maxLength":512,"minLength":0},"currentPrice":{"type":["integer","null"],"format":"int32","description":"수정할 현재 판매가","example":119000,"minimum":0},"currency":{"type":["string","null"],"description":"수정할 통화 코드 (ISO 4217)","example":"KRW","maxLength":8,"minLength":0}}},"UserUpdateRequest":{"type":"object","description":"유저 정보 수정 요청 — 들어온 필드만 갱신한다 (multipart/form-data)","properties":{"nickname":{"type":["string","null"],"description":"변경할 닉네임 (선택, 최대 10자)","example":"새닉네임","maxLength":10,"minLength":1},"image":{"type":["string","null"],"format":"binary","description":"변경할 프로필 이미지 (선택 · png/jpeg/webp/heic/heif · 5MB 이하)"}}},"UpdateTournamentItemRequest":{"type":"object","description":"토너먼트 아이템 수정 요청 — 들어온 필드만 갱신한다","properties":{"name":{"type":["string","null"],"description":"수정할 상품명","example":"나이키 에어맥스","maxLength":512,"minLength":1},"price":{"type":["integer","null"],"format":"int32","description":"수정할 현재 판매가","example":129000,"minimum":0},"currency":{"type":["string","null"],"description":"수정할 통화 코드 (ISO 4217)","example":"KRW","maxLength":8,"minLength":0},"image":{"type":["string","null"],"format":"binary","description":"수정할 상품 이미지"}}},"UpdateInviteDurationRequest":{"type":"object","properties":{"inviteDurationMinutes":{"type":"integer","format":"int64","description":"새 초대 마감까지 남은 시간 (분 단위, 1-1440)","example":60,"maximum":1440,"minimum":1}}},"NicknameCheckRequest":{"type":"object","description":"닉네임 중복 체크 요청 (query parameter)","properties":{"nickname":{"type":"string","description":"확인할 닉네임 (최대 10자)","example":"새닉네임","maxLength":10,"minLength":0}},"required":["nickname"]},"SseEmitter":{"type":"object","properties":{"timeout":{"type":"integer","format":"int64"}}},"FcmDeviceUnregisterRequest":{"type":"object","description":"FCM 기기 해제 요청 (로그아웃 시 호출 — /auth/logout 보다 먼저)","properties":{"deviceId":{"type":"string","description":"해제할 기기 식별자 (iOS IDFV)","example":"1B2A3C4D-5E6F-7A8B-9C0D-1E2F3A4B5C6D","maxLength":255,"minLength":0}},"required":["deviceId"]}},"securitySchemes":{"bearerAuth":{"type":"http","description":"게스트 생성·소셜 로그인으로 발급받은 액세스 토큰. `Bearer ` 접두어는 문서 UI 가 자동으로 붙인다.","scheme":"bearer","bearerFormat":"JWT"}}}}