Skip to content

feat: 소셜 토너먼트 전체 (4개 PR 통합 — #142/#156/#159/#163)#167

Merged
ychany merged 15 commits into
devfrom
feat/social-tournament-full
Jun 13, 2026
Merged

feat: 소셜 토너먼트 전체 (4개 PR 통합 — #142/#156/#159/#163)#167
ychany merged 15 commits into
devfrom
feat/social-tournament-full

Conversation

@ychany

@ychany ychany commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

작업 요약

4개 PR (#142, #156, #159, #163) 을 squash merge 로 단일 PR 로 통합. 모든 작업이 누적 의존성을 가지고 있어 분리 리뷰 어렵다고 판단.

  • 친구 초대 (코드/링크) 흐름 + 비회원 게스트 참여
  • 토너먼트 진행 (ROOT/CLONE 구조) — 주최자/멤버/게스트 역할 분기
  • 친구 토너먼트 결과보기 (group-result) 화면
  • 플레이 링크 진입 라우트 (/play/[id]) + 친구 초대 진입 라우트 (/invite/[id])
  • 영수증 + 플레이 링크 공유 다이얼로그

PR 통합 안내

기존 작업 브랜치 흐름:
dev
├── feat/139 (#142 — 친구 초대 / 공유 플로우 UI)
│ └── feat/140 (#156 — 초대 코드 입력 / 참여자 입장 UI)
│ └── feat/157 (#159 — 소셜 토너먼트 API 연동)
│ └── feat/158 (#163 — 그룹 결과 + 플레이 링크 진입)

브랜치를 순차적으로 따와 작업해서 각 PR 이 누적 의존성을 가짐. 기존 4개 PR 을 모두 close 하고 본 PR 하나로 통합. (기존 PR 의 commit message 는 본 PR 의 squash commit 본문에 모두 보존됨.)

작업 세부 내용

1. 친구 초대 / 참여 흐름

  • 홈 초대 코드 다이얼로그 — 6자리 코드 입력 → GET /tournaments/by-invite-code 호출 → 성공 시 /tournament/join/{id}?code=XXX 라우팅, 400/409 시 InvalidCodeDialog 안내
  • 친구 초대 링크 진입 라우트 /invite/[id]?code=XXX — 카톡 등으로 받은 링크 클릭 시 코드 자동 검증 → /tournament/join/{id}?code=XXX 라우팅 (코드 query 유지)
  • 비회원 게스트 참여 — 닉네임 입력 + 중복 체크 debounce 300ms → POST /tournaments/{id}/join/guest
  • 회원 참여POST /tournaments/{id}/join + 환영 바텀시트
  • 초대 코드 형식 정규식 [A-Z]{3}\d{3} 강화, 자동 대문자 변환

2. 토너먼트 진행 — ROOT/CLONE 구조 대응 (BE PR #456, #467)

  • POST /tournaments/{id}/start 응답에 tournamentId 추가
    • 주최자(ROOT): 본인 id 그대로
    • 참여자(CLONE): 새로 생성된 CLONE id → 본인 인스턴스의 매치 화면으로 redirect
  • 참여자 화면 분기 (ownerStarted)
    • PENDING → "주최자가 시작해야 플레이 할 수 있어요" 안내
    • IN_PROGRESS + ownerStarted=true → 본인 시작 버튼 활성화 → 클릭 시 본인 CLONE 생성 후 매치 진입
  • 응답 타입 GetTournamentMemberWaitingResponseT 신규 — IN_PROGRESS 상태에서도 참여자가 ROOT 의 pending 정보 (items / participants) 받을 수 있음
  • match/page.tsx SSR 가드 — COMPLETED → result, IN_PROGRESS + pending → create 로 자동 리다이렉트

3. 친구 토너먼트 결과보기 (group-result)

  • /tournament/[id]/result/group 라우트 — 영수증 형태 (Kode Mono) + 1st Place 강조 카드 + Others 축약 라인 + chosenBy 표시
  • 결과 화면 진입 카드isRoot 일 때만 노출 (주최자 + 친구 초대 멤버)
    • 클릭 시 GET /tournaments/{id}/group-result 호출
    • 친구 0명 / 권한 없음 / 에러 시 "아직 친구 결과가 없어요" 안내 화면
    • useQuery + retry: false 로 에러 분기

4. 플레이 링크 (/play/[id])

  • 진입 흐름
    • POST /tournaments/{sourceId}/from-play-link 호출
    • 401 (또는 refresh 실패의 400) → 자동 게스트 발급 → 재시도
    • 응답 받은 tournamentId 의 status 별 분기 라우팅:
      • PENDING → /create (바구니 미리보기 + 시작 버튼)
      • IN_PROGRESS + 매치 진행 중 → /match
      • COMPLETED → /result (재진입 시 본인 결과)
  • 백엔드 idempotent 정책 지원 — 같은 사용자 재진입 시 본인의 기존 CLONE id 반환

5. 영수증 / 플레이 링크 공유

  • PlateShareDialog — 시안 일치 (스톱워치 / 체크리스트 제거, 단일 안내 텍스트)
  • canSharePlayLink 분기isRoot && isOwner 일 때만 "플레이 링크 공유" 액션 노출
    • CLONE owner (플레이 링크 게스트) 는 이미지 공유만
  • 기존 링크 재사용initialPlayLinkExpiresAt 있으면 mutation 생략하고 바로 share (만료 갱신 방지)
  • 공유 성공 시 자동 닫기

6. API 연동

Endpoint 용도
GET /tournaments/by-invite-code 6자리 코드만으로 토너먼트 조회
GET /tournaments/{id}/invite-preview RSC prefetch + HydrationBoundary
POST /tournaments/{id}/join 회원 참여
POST /tournaments/{id}/join/guest 비회원 참여
POST /tournaments/{id}/start 시작 (응답 tournamentId 활용)
POST /tournaments/{id}/play-link 플레이 링크 생성
POST /tournaments/{sourceId}/from-play-link 플레이 링크로 CLONE 생성
GET /tournaments/{id}/group-result 그룹 결과 조회
GET /users/nickname/check 닉네임 중복 체크 (debounce + staleTime)

7. 응답 타입 확장

  • 최상단: isRoot: boolean (ROOT/CLONE 구분)
  • pending:
    • ownerStarted: boolean
    • inviteCode, inviteExpiresAt nullable (ownerStarted=true 시 null)
  • completed:
    • hasGroupResult: boolean
    • playLinkExpiresAt?: string
  • GetTournamentMemberWaitingResponseT 신규

8. mock / dead code 제거

  • mocks/deposit.ts, mocks/tournamentPreview.ts, mocks/participants.ts 삭제
  • MOCK_DEPOSIT_DURATION_MS, MOCK_PARTICIPANTS, withMockParticipants 제거
  • 풀스크린 dead route /tournament/join + InviteClient.tsx 삭제
  • verifyInviteCode mock 정답 코드 제거 → 형식 검증 함수로 축소

9. 그 외 정리

  • 회원 프로필 이미지 정사각형 컨테이너 (비율 일그러짐 보정)
  • 비회원 참가자 dicebear URL 무시 → 기본 SVG fallback
  • 결과 화면 하단 단일 CTA (홈으로 가기)
  • 친구 없을 때 카운트다운 / 시작 확인 다이얼로그 안 보이도록 분기
  • bodysuppressHydrationWarning (Grammarly 등 확장 대응)
  • POST body null{} 통일 (415 방지)
  • WishGrid key prop 누락 fix

후속 작업 (별도 이슈)

  • 그룹 결과 진입 카드의 실제 참여자 아바타 (백엔드 응답에 participants 필드 추가 시)
  • SSE 도입 검토 (참여자 자동 진입 polling 30초 → 즉시)
  • 플레이 링크 게스트의 group-result 조회 권한 (현재는 ROOT 만)

연관 이슈

closes #139
closes #140
closes #157
closes #158

Summary by CodeRabbit

릴리즈 노트

  • 새로운 기능

    • 초대 코드를 통한 토너먼트 참여 기능 추가
    • 토너먼트 공유 링크 생성 및 관리 기능
    • 플레이 링크 기반 토너먼트 복제 기능
    • 그룹 결과 조회 및 표시 기능
    • 닉네임 중복 체크 기능
    • 게스트 참여 흐름 개선
  • 문서

    • 소셜 토너먼트 API 통합 가이드 추가

commit 52c225d
Author: joyeongchan <tigerbone@naver.com>
Date:   Tue Jun 9 21:52:08 2026 +0900

    chore: format

commit 4f45210
Author: joyeongchan <tigerbone@naver.com>
Date:   Tue Jun 9 21:31:28 2026 +0900

    refactor: 외부 라우트 import 를 절대경로로 변경

commit de4f8b4
Author: joyeongchan <tigerbone@naver.com>
Date:   Tue Jun 9 19:00:18 2026 +0900

    fix: 친구 토너먼트 결과보기 카드 노출 조건 단순화 + 에러 안내

commit 143ca1e
Author: joyeongchan <tigerbone@naver.com>
Date:   Tue Jun 9 18:42:01 2026 +0900

    fix: 플레이 링크 진입 후 CLONE 상태에 따라 라우팅 분기

commit e8cfd72
Author: joyeongchan <tigerbone@naver.com>
Date:   Tue Jun 9 18:39:49 2026 +0900

    fix: 비로그인 진입 시 PlayClient 게스트 발급 회복 (401/400 둘 다 처리)

commit 3bcfaa3
Author: joyeongchan <tigerbone@naver.com>
Date:   Tue Jun 9 18:31:34 2026 +0900

    fix: 플레이 링크 진입 시 토너먼트 상태에 따라 적절 화면 라우팅

commit 31cf810
Author: joyeongchan <tigerbone@naver.com>
Date:   Tue Jun 9 18:28:26 2026 +0900

    fix: 결승 직후 토너먼트 캐시 시드 제거 (그룹 결과 카드 즉시 노출)

commit 8f6cc22
Author: joyeongchan <tigerbone@naver.com>
Date:   Tue Jun 9 16:27:51 2026 +0900

    chore: from-play-link idempotent 동작 주석 갱신

commit 534dbed
Author: joyeongchan <tigerbone@naver.com>
Date:   Tue Jun 9 14:07:22 2026 +0900

    fix: 결과 페이지에서 토너먼트 캐시를 권위 응답으로 강제 덮어쓰기

commit eec26db
Author: joyeongchan <tigerbone@naver.com>
Date:   Tue Jun 9 04:26:29 2026 +0900

    chore : format

commit 7c0bb2b
Author: joyeongchan <tigerbone@naver.com>
Date:   Tue Jun 9 04:22:59 2026 +0900

    fix: 그룹 결과 카드를 ROOT 토너먼트에서만 노출

commit e813384
Author: joyeongchan <tigerbone@naver.com>
Date:   Mon Jun 8 20:56:22 2026 +0900

    feat: ownerStarted/isRoot 기반 참여자 대기·플레이 링크 공유 분기

commit 6d7805a
Author: joyeongchan <tigerbone@naver.com>
Date:   Mon Jun 8 20:43:08 2026 +0900

    fix: 친구 토너먼트 결과보기 카드에 아바타 그룹 추가 (시안 일치)

commit 02ae3d4
Author: joyeongchan <tigerbone@naver.com>
Date:   Mon Jun 8 20:36:39 2026 +0900

    refactor: PlateShareDialog 시안 일치 (스톱워치/체크리스트 제거)

commit 9810007
Author: joyeongchan <tigerbone@naver.com>
Date:   Mon Jun 8 19:24:41 2026 +0900

    feat: 참여자 토너먼트 진행 (CLONE 구조 대응)

commit 0990df0
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 05:18:02 2026 +0900

    fix: refresh/guest/play-link POST body를 빈 객체로 통일

commit c5e70d6
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 05:12:06 2026 +0900

    fix: 비회원 게스트 참여 시 inviteCode 전달

commit 9d55b3a
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 04:31:11 2026 +0900

    feat: 참여자 화면이 status 변화 감지해 자동 라우팅

commit bca0b40
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 04:08:54 2026 +0900

    fix: 결과 화면 하단 단일 CTA로 정리

commit f654e73
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 04:05:54 2026 +0900

    fix: body에 suppressHydrationWarning 추가 (확장 프로그램 대응)

commit bb1bc17
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 04:00:31 2026 +0900

    feat: 그룹 결과 화면 1st 강조 + Others 축약 형태로 재구성

commit 57d088b
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 03:55:22 2026 +0900

    fix: 회원 프로필 이미지 정사각형 컨테이너로 렌더링

commit 1189b79
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 03:53:27 2026 +0900

    fix: 비회원 참가자 프로필을 기본 SVG로 표시 (홈/토너먼트 일관 적용)

commit a73264d
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 03:51:38 2026 +0900

    fix: 비회원 참가자 프로필을 기본 SVG로 표시

commit a3b7ce1
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 03:43:26 2026 +0900

    feat: 친구 초대 링크 진입 라우트 추가

commit 5c13804
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 03:22:42 2026 +0900

    fix: 플레이 링크 공유 주최자 분기 + 기존 링크 재사용

commit 3726ce2
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 03:15:03 2026 +0900

    fix: WishGrid 리스트 항목에 누락된 key prop 추가

commit fd45030
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 03:12:06 2026 +0900

    fix: 참여자 mock 제거 및 친구 없을 때 카운트다운/시작 확인 분기

commit 4488d2c
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 03:03:53 2026 +0900

    fix: 주최자는 초대 만료와 무관하게 담기 가능하도록 수정

commit 7307ee7
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 02:35:08 2026 +0900

    feat: 그룹 결과 보기 화면 추가 및 결과 진입 카드 연동

commit ea2ddad
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 02:19:01 2026 +0900

    feat: 플레이 링크 진입 시 토너먼트 자동 복제 및 라우팅

commit d970c4a
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 01:43:30 2026 +0900

    chore: prettier

commit 8460097
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 01:27:39 2026 +0900

    feat: 플레이 링크 만료 시각 서버 응답 기반으로 표시

commit 9f032ee
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 01:21:19 2026 +0900

    feat: 초대 코드 검증 API 연동 및 미리보기 흐름 실 데이터화

commit 939b758
Author: joyeongchan <tigerbone@naver.com>
Date:   Sun Jun 7 00:03:57 2026 +0900

    refactor: 담기 마감을 초대 코드 만료 시간으로 통일

commit 03515cb
Author: joyeongchan <tigerbone@naver.com>
Date:   Sat Jun 6 03:33:11 2026 +0900

    feat: 친구 초대 다이얼로그 동적 데이터 연동 (만료시간/링크)

commit 77a1806
Author: joyeongchan <tigerbone@naver.com>
Date:   Sat Jun 6 03:25:24 2026 +0900

    feat: 비회원 게스트 토너먼트 참여 API 연동

commit 76a9a4e
Author: joyeongchan <tigerbone@naver.com>
Date:   Sat Jun 6 03:16:41 2026 +0900

    feat: 플레이 링크 생성 API 연동 및 공유 URL 동적 구성

commit 3622c24
Author: joyeongchan <tigerbone@naver.com>
Date:   Sat Jun 6 03:04:28 2026 +0900

    refactor: 주최자 분기를 tournamentData.isOwner로 통일

commit 9678e2a
Author: joyeongchan <tigerbone@naver.com>
Date:   Sat Jun 6 02:56:28 2026 +0900

    feat: 닉네임 중복 체크 API 연동

commit 118eec3
Author: joyeongchan <tigerbone@naver.com>
Date:   Sat Jun 6 02:36:24 2026 +0900

    docs: 소셜 토너먼트 API 연동 계획 문서 추가

commit 8b7d832
Author: joyeongchan <tigerbone@naver.com>
Date:   Sat Jun 6 02:20:07 2026 +0900

    chore: 슬랙봇 임시완화 복구

commit c38bc5f
Author: joyeongchan <tigerbone@naver.com>
Date:   Sat Jun 6 02:19:24 2026 +0900

    chore: 슬랙봇 누락

commit 0a4624d
Author: joyeongchan <tigerbone@naver.com>
Date:   Sat Jun 6 02:14:13 2026 +0900

    chore: prettier format

commit 496717d
Merge: b2cb0f7 c858551
Author: joyeongchan <tigerbone@naver.com>
Date:   Sat Jun 6 02:11:22 2026 +0900

    merge: dev 머지 + components/common 경로 리네임 반영

commit b2cb0f7
Author: joyeongchan <tigerbone@naver.com>
Date:   Sat Jun 6 01:55:52 2026 +0900

    chore: format

commit ae1c2b8
Author: joyeongchan <tigerbone@naver.com>
Date:   Thu Jun 4 04:12:40 2026 +0900

    feat: 홈 초대 코드 다이얼로그 + 형식/불일치 에러 분기 구현

commit 2a112f2
Author: joyeongchan <tigerbone@naver.com>
Date:   Thu Jun 4 02:54:50 2026 +0900

    feat: 참여자 패널 친구 유무에 따른 UI 분기

commit a2a860b
Author: joyeongchan <tigerbone@naver.com>
Date:   Thu Jun 4 02:29:23 2026 +0900

    chore: mock 담기 종료 시간 1분 조정

commit 0590e74
Author: joyeongchan <tigerbone@naver.com>
Date:   Thu Jun 4 02:22:10 2026 +0900

    feat: 초대 참여자에게 시작 버튼 비활성 + 안내 툴팁 표시

commit 43a8960
Author: joyeongchan <tigerbone@naver.com>
Date:   Thu Jun 4 02:05:46 2026 +0900

    fix: 담기 종료 후 슬롯 UI 정리 + 캐러셀 memo 처리

commit e3a7d8b
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 22:28:59 2026 +0900

    feat: 담기 종료 이후 상태별 UI 분기 처리

commit efb5d97
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 20:09:47 2026 +0900

    feat: 회원/비회원 초대 참여 분기 + 회원용 확인 다이얼로그 추가

commit 3839f69
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 20:01:12 2026 +0900

    fix: 초대 환영 바텀시트 대상 토너먼트에서만 노출

commit a5575e2
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 19:56:43 2026 +0900

    feat: 초대 참여 후 환영 바텀시트 자동 노출

commit 06088a1
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 19:45:29 2026 +0900

    fix: 초대 참여하기 화면 풀스크린 페이지로 변경

commit 1b11188
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 19:42:40 2026 +0900

    chore: 임시 mock 초대 코드 111111로 변경

commit 2ca2217
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 19:39:20 2026 +0900

    feat: 초대 참여 닉네임 확인 화면 UI 구현

commit 6eb6e7f
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 18:43:26 2026 +0900

    style: 초대 코드 화면 타이포·간격 시안 매칭

commit c012c39
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 18:41:25 2026 +0900

    fix: 홈 초대 토너먼트 입장 버튼 라우팅 연결

commit 46e1b95
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 18:37:11 2026 +0900

    fix: 초대 코드 에러 헬퍼 텍스트 디자인에 맞게 수정

commit 46f7399
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 17:51:21 2026 +0900

    refactor: 초대 코드 페이지 경로 invite → join 변경

commit 3b4f985
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 17:48:29 2026 +0900

    feat: 초대 코드 입력 화면 UI 구현

commit 3273be5
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 16:31:06 2026 +0900

    refactor: 익명 핸들러 분리 + 사용 안 하는 InviteFriends 제거

commit 8cf4580
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 04:26:02 2026 +0900

    chore: format

commit 229ab7a
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 04:25:00 2026 +0900

    feat: 결과 화면 플레이트 공유 다이얼로그 구현

commit 4841629
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 04:06:52 2026 +0900

    refactor: 친구 초대/참여자 패널 아이콘 색상 토큰 정리

commit c3ceccd
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 04:03:44 2026 +0900

    fix: 토너먼트 시작/뒤로가기 모달 아이콘 디자인 교체

commit 84c3dd4
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 03:59:28 2026 +0900

    fix: 친구 mock 머지 임계값을 2명 이상으로 조정

commit 3fd0f3c
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 03:44:21 2026 +0900

    refactor: 친구 포함 여부 판단을 tournamentData participants로 변경

commit c60ac1d
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 03:42:12 2026 +0900

    feat: 친구 포함 토너먼트 시작 확인 모달 추가

commit cf30ac6
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 03:34:03 2026 +0900

    feat: 후보 담기 마감 카운트다운 UI 구현

commit a71c8c3
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 03:21:44 2026 +0900

    feat: 토너먼트 참여자 패널 collapsed/expanded UI 구현

commit ecdfe2a
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 03:09:26 2026 +0900

    fix: 초대 링크 공유 토스트 메시지 통일

commit 11ecd7b
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 03:06:15 2026 +0900

    feat: 친구 초대 링크 공유 시트 연동 / 임시 더미 url

commit 6274dd7
Author: joyeongchan <tigerbone@naver.com>
Date:   Wed Jun 3 02:59:52 2026 +0900

    feat: 토너먼트 친구 초대 바텀시트 다이얼로그 구현
@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
depromeet Ready Ready Preview, Comment Jun 13, 2026 6:40pm

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ychany, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 53 minutes and 37 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8914b8e5-10e6-4d37-9a82-f7981256136d

📥 Commits

Reviewing files that changed from the base of the PR and between 60c7a78 and 7c61c41.

📒 Files selected for processing (35)
  • apps/app/apis/postSocialLogin.ts
  • apps/app/hooks/useWebviewCookieSync.ts
  • apps/app/utils/handleImage.ts
  • apps/web/src/apis/client.ts
  • apps/web/src/app/archive/_components/WishlistContent.tsx
  • apps/web/src/app/archive/_components/wish-grid/index.tsx
  • apps/web/src/app/archive/wish/[id]/_components/ItemEditForm.tsx
  • apps/web/src/app/auth/callback/[provider]/_apis/postSocialLogin.ts
  • apps/web/src/app/auth/callback/apple/route.ts
  • apps/web/src/app/home/page.tsx
  • apps/web/src/app/layout.tsx
  • apps/web/src/app/login/_apis/getAuthUrl.ts
  • apps/web/src/app/login/_apis/postGuestLogin.ts
  • apps/web/src/app/mypage/_components/LogoutMenuItem.tsx
  • apps/web/src/app/mypage/edit/_components/EditForm.tsx
  • apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasket.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-start-button/TournamentStartButton.tsx
  • apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx
  • apps/web/src/app/tournament/[id]/item/[itemId]/_components/ItemEditForm.tsx
  • apps/web/src/app/tournament/[id]/match/page.tsx
  • apps/web/src/app/tournament/[id]/result/_components/ReceiptPaper.tsx
  • apps/web/src/app/tournament/[id]/result/_utils/formatReceipt.ts
  • apps/web/src/app/tournament/[id]/result/group/_components/GroupResultClient.tsx
  • apps/web/src/app/tournament/[id]/result/page.tsx
  • apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx
  • apps/web/src/components/get-item-dialog/ByImageDialog.tsx
  • apps/web/src/components/get-item-dialog/ByLinkDialog.tsx
  • apps/web/src/consts/api.ts
  • apps/web/src/consts/route.ts
  • apps/web/src/hooks/usePostWishLink.ts
  • apps/web/src/types/user.ts
  • apps/web/src/utils/pushNotificationRoute.ts
  • packages/core/src/index.ts

Walkthrough

초대 코드 입력부터 게스트 참여, 생성 화면 제어(카운트다운/참여자/시작 확인), 결과 공유(플레이 링크), 플레이 링크 진입, 그룹 결과 조회까지 소셜 토너먼트의 전체 엔드투엔드 플로우를 구현합니다. API 엔드포인트, 응답 타입, 라우트, 클라이언트 로직, UI 컴포넌트, 유틸을 일관되게 추가·변경합니다.

Changes

소셜 토너먼트 엔드투엔드 플로우

Layer / File(s) Summary
API 엔드포인트·응답 타입 기반 정의
apps/web/src/consts/api.ts, apps/web/src/consts/route.ts, apps/web/src/app/tournament/[id]/_common/_types/tournamentResponse.ts, apps/web/src/app/tournament/join/_types/join.ts, apps/web/src/types/user.ts, apps/web/src/app/play/[id]/_types/play.ts, apps/web/src/app/tournament/[id]/result/_types/groupResult.ts
토너먼트 상태별 응답 타입(PENDING/MEMBER_WAITING/IN_PROGRESS/COMPLETED)에 isOwner, isRoot, sourceTournamentId, playLinkExpiresAt, hasGroupResult를 추가하고, 초대/참여/공유/플레이 링크 엔드포인트와 라우트 상수를 정의합니다.
공통 전송 형식·공유·검증 유틸
apps/web/src/apis/client.ts, apps/web/src/app/login/_apis/postGuestLogin.ts, apps/web/src/utils/share.ts
401 리프레시 요청 바디를 null에서 {}로 조정하고, Web Share API 폴백(클립보드 복사)을 구현하며, 초대 코드 형식 검증 유틸을 추가합니다.
홈 초대 코드 입력 진입 UI
apps/web/src/app/home/_components/InviteTournamentButton.tsx, apps/web/src/app/home/_components/invite-code-dialog/InviteCodeDialog.tsx, apps/web/src/app/home/_components/invite-code-dialog/InvalidCodeDialog.tsx, apps/web/src/app/home/page.tsx
홈 페이지에 초대 입장 버튼을 추가하고 코드 입력 다이얼로그, 오류 다이얼로그를 구현해 코드 검증과 라우팅을 트리거합니다.
초대 링크 검증·리다이렉트
apps/web/src/app/invite/[id]/page.tsx, apps/web/src/app/invite/[id]/_components/InviteClient.tsx
/invite/[id] 라우트에서 코드 기반 프리뷰로 tournamentId 일치를 검증한 후 조인 경로로 리다이렉트하거나 무효 안내를 표시합니다.
조인 프리뷰·닉네임 중복 체크·게스트 참여
apps/web/src/app/tournament/join/[id]/page.tsx, apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx, apps/web/src/app/tournament/join/_apis/getInvitePreview.ts, apps/web/src/app/tournament/join/_apis/getInvitePreviewByCode.ts, apps/web/src/app/tournament/join/_apis/postJoinGuest.ts, apps/web/src/apis/getNicknameCheck.ts, apps/web/src/hooks/useGetNicknameCheck.ts, apps/web/src/app/tournament/join/_utils/joinSession.ts, apps/web/src/app/tournament/join/_consts/randomNickname.ts
조인 페이지의 SSR 프리페치/하이드레이션, 닉네임 debounce 기반 중복 조회, 게스트 참여 뮤테이션, 환영 세션 저장을 구현합니다.
생성 화면 참여자 패널·초대·조인 다이얼로그
apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx, apps/web/src/app/tournament/[id]/create/_components/participant-panel/ParticipantPanel.tsx, apps/web/src/app/tournament/[id]/create/_components/participant-panel/ParticipantChip.tsx, apps/web/src/app/tournament/[id]/create/_components/invite-friends/InviteFriendsDialog.tsx, apps/web/src/app/tournament/[id]/create/_components/welcome-join-dialog/WelcomeJoinDialog.tsx, apps/web/src/app/tournament/[id]/create/_components/member-join-confirm-dialog/MemberJoinConfirmDialog.tsx
생성 화면에 참여자 패널, 초대 다이얼로그, 환영/참여 확인 다이얼로그를 추가하고 조인 세션 페이로드에 따라 조건부 렌더링합니다.
담기 마감 카운트다운·바스켓 게이팅
apps/web/src/app/tournament/[id]/create/_hooks/useCountdown.ts, apps/web/src/app/tournament/[id]/create/_components/deposit-countdown/DepositCountdown.tsx, apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasket.tsx, apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx, apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/EmptyBasketSlot.tsx, apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket-status/TournamentItemBasketStatus.tsx
useCountdown 훅과 DepositCountdown UI를 추가하고, isDepositClosed 기준으로 슬롯·배지·추가 버튼·캐러셀의 렌더링과 상호작용을 분기합니다.
시작·이탈 확인 다이얼로그·시작 로직
apps/web/src/app/tournament/[id]/create/_components/tournament-header/TournamentHeader.tsx, apps/web/src/app/tournament/[id]/create/_components/tournament-header/ConfirmExitDialog.tsx, apps/web/src/app/tournament/[id]/create/_components/tournament-start-button/TournamentStartButton.tsx, apps/web/src/app/tournament/[id]/create/_components/tournament-start-button/ConfirmStartDialog.tsx, apps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentStart.ts
헤더에 이탈 확인 다이얼로그, 시작 버튼에 확인 다이얼로그를 추가하고, 대기/참여/친구 여부에 따라 동작을 분기하며, 성공 시 응답의 nextTournamentId로 라우팅합니다.
매치 페이지 클론 ID 반영
apps/web/src/app/tournament/[id]/match/page.tsx, apps/web/src/app/tournament/[id]/match/_hooks/useTournament.ts
매치 상태 조회 및 결과 저장에서 원래 tournamentId 대신 복제된 playTournamentId를 사용하고, IN_PROGRESS+pending 상태 시 생성 화면으로 리다이렉트합니다.
결과 화면 플레이 링크 공유
apps/web/src/app/tournament/[id]/result/_apis/postPlayLink.ts, apps/web/src/app/tournament/[id]/result/_hooks/usePostPlayLink.ts, apps/web/src/app/tournament/[id]/result/_components/ResultClient.tsx, apps/web/src/app/tournament/[id]/result/_components/ReceiptDrawMachine.tsx, apps/web/src/app/tournament/[id]/result/_components/ReceiptPaper.tsx, apps/web/src/app/tournament/[id]/result/_components/plate-share-dialog/PlateShareDialog.tsx
결과 화면에서 소유자 권한(isOwner && isRoot)에 따라 플레이 링크 공유를 제어하고 플레이트 공유 다이얼로그로 연결합니다.
플레이 링크 진입·복제 생성
apps/web/src/app/play/[id]/page.tsx, apps/web/src/app/play/[id]/_components/PlayClient.tsx, apps/web/src/app/play/[id]/_apis/postFromPlayLink.ts
/play/[id] 라우트에서 플레이 링크 복제 생성과 인증 실패 시 게스트 로그인 재시도, 상태별 목적지 라우팅을 구현합니다.
그룹 결과 조회·페이지
apps/web/src/app/tournament/[id]/result/_apis/getGroupResult.ts, apps/web/src/app/tournament/[id]/result/_hooks/useGetGroupResult.ts, apps/web/src/app/tournament/[id]/result/group/page.tsx, apps/web/src/app/tournament/[id]/result/group/_components/GroupResultClient.tsx, apps/web/src/app/tournament/[id]/result/_types/groupResult.ts, apps/web/src/app/tournament/[id]/result/_components/group-result-entry-card/GroupResultEntryCard.tsx
그룹 결과 API, 훅, SSR 페이지를 추가하고 순위별 카드 UI로 아이템과 선택자 정보를 렌더링합니다.
보조 UI·import·문서 정리
apps/web/src/app/archive/_components/wish-grid/index.tsx, apps/web/src/app/home/_components/TournamentList.tsx, apps/web/src/app/layout.tsx, apps/web/src/app/login/_components/LoginButtons.tsx, apps/web/src/app/login/_components/SocialLoginButton.tsx, apps/web/src/app/login/page.tsx, apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectHeader.tsx, apps/web/src/app/tournament/[id]/create/page.tsx, apps/web/src/components/user-profile-group/UserProfile.tsx, docs/social-tournament-api-integration.md
기존 화면의 JSX/스타일 재구성, import 순서 정리, dicebear 아바타 필터링, Image 렌더링 최적화, 소셜 토너먼트 API 통합 문서를 추가합니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • depromeet/PIKI-Client#141: 바스켓 컴포넌트(TournamentItemBasket.tsx, TournamentItemBasketCarousel.tsx)의 maxHeight, 슬롯 다이얼로그 조건, 캐러셀 내부 상태 관리를 동일 코드 영역에서 수정하므로 직접 연관됩니다.
  • depromeet/PIKI-Client#124: 401 토큰 리프레시 흐름(apps/web/src/apis/client.ts)을 메인 PR과 동일하게 수정하므로 코드 레벨 충돌 가능성이 있습니다.
  • depromeet/PIKI-Client#104: 참가자 프로필 이미지 처리(TournamentList.tsximageUrl 조건부 매핑)를 동일 코드 영역에서 수정하므로 직접 연관됩니다.

Suggested labels

WEB, refactor

Suggested reviewers

  • kanghaeun
  • iOdiO89

Poem

🐰 초대 코드가 울려 퍼지고
닉네임 중복 체크를 통과할 땐
카운트다운 타이머가 콩콩콩 뛴다
플레이 링크를 공유해 친구들과
그룹 결과로 승부를 갈라본다! 🏆

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/social-tournament-full

@github-actions
github-actions Bot requested review from iOdiO89 and soyeong0115 June 9, 2026 16:44
@github-actions github-actions Bot added the feature New feature or request label Jun 9, 2026
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

No description provided.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🧹 Nitpick comments (10)
apps/web/src/app/home/_components/InviteTournamentButton.tsx (1)

7-7: ⚡ Quick win

상대 경로 import를 @/ 절대 경로로 통일해주세요.

apps/web/src 하위에서 새로 추가되는 import는 절대 경로 alias로 맞추는 게 규칙 일관성에 맞습니다.

♻️ 제안 수정
-import InviteCodeDialog from './invite-code-dialog/InviteCodeDialog';
+import InviteCodeDialog from '`@/app/home/_components/invite-code-dialog/InviteCodeDialog`';

As per coding guidelines apps/web/src/**/*.{ts,tsx}: Use absolute path alias @/* for imports instead of relative paths.

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

In `@apps/web/src/app/home/_components/InviteTournamentButton.tsx` at line 7,
Replace the relative import of InviteCodeDialog with the project alias path:
update the import statement in InviteTournamentButton.tsx to use the
'`@/app/home/_components/invite-code-dialog/InviteCodeDialog`' absolute alias
(i.e., swap the current "./invite-code-dialog/InviteCodeDialog" for the
equivalent "`@/`..." path) so it follows the apps/web/src alias convention and
keeps imports consistent.

Source: Coding guidelines

apps/web/src/app/tournament/join/_apis/getInvitePreview.ts (1)

8-8: ⚡ Quick win

타입 import도 절대 경로 alias로 맞춰 주세요.

Line 8의 상대 경로는 현재 import 규칙과 불일치합니다.

권장 수정안
-import type { GetInvitePreviewResponseT } from '../_types/join';
+import type { GetInvitePreviewResponseT } from '`@/app/tournament/join/_types/join`';

As per coding guidelines, apps/web/src/**/*.{ts,tsx}: Use absolute path alias @/* for imports instead of relative paths.

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

In `@apps/web/src/app/tournament/join/_apis/getInvitePreview.ts` at line 8, The
import in getInvitePreview.ts uses a relative path for the type
GetInvitePreviewResponseT; update the import to use the project absolute alias
(e.g. import type { GetInvitePreviewResponseT } from
'`@/app/tournament/join/_types/join`') so it matches the
apps/web/src/**/*.{ts,tsx} rule; locate the import statement for
GetInvitePreviewResponseT and replace the relative path with the corresponding
'`@/`...' alias.

Source: Coding guidelines

apps/web/src/app/invite/[id]/page.tsx (1)

5-5: ⚡ Quick win

상대 경로 import 대신 절대 경로 alias를 사용해 주세요.

Line 5의 ./_components/InviteClient는 현재 apps/web/src/**/*.{ts,tsx} import 규칙과 불일치합니다.

권장 수정안
-import InviteClient from './_components/InviteClient';
+import InviteClient from '`@/app/invite/`[id]/_components/InviteClient';

As per coding guidelines, apps/web/src/**/*.{ts,tsx}: Use absolute path alias @/* for imports instead of relative paths.

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

In `@apps/web/src/app/invite/`[id]/page.tsx at line 5, Replace the relative import
of the InviteClient component with the project absolute alias: change the import
that currently references './_components/InviteClient' to use the '`@/`...' alias
path that points to the same InviteClient module (i.e., import InviteClient from
'`@/`…/InviteClient' using the app's `@/` root alias) so the file conforms to the
apps/web/src/**/*.{ts,tsx} rule; keep the imported symbol name InviteClient
unchanged.

Source: Coding guidelines

apps/web/src/apis/getNicknameCheck.ts (1)

7-8: ⚡ Quick win

apis 내부 import도 절대 경로 alias를 사용해 주세요.

Line 7-8은 apps/web/src 공통 import 규칙과 불일치합니다.

권장 수정안
-import { clientApi } from './client';
-import { serverApi } from './server';
+import { clientApi } from '`@/apis/client`';
+import { serverApi } from '`@/apis/server`';

As per coding guidelines, apps/web/src/**/*.{ts,tsx}: Use absolute path alias @/* for imports instead of relative paths.

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

In `@apps/web/src/apis/getNicknameCheck.ts` around lines 7 - 8, Replace the
relative imports for clientApi and serverApi in getNicknameCheck.ts with the
project absolute alias import form (use the `@/` alias) so they follow the
apps/web/src import convention; update the import statements that bring in
clientApi and serverApi to use the alias path that resolves to the same modules
and run a quick typecheck to ensure imports still resolve.

Source: Coding guidelines

apps/web/src/app/tournament/join/[id]/page.tsx (1)

7-8: ⚡ Quick win

상대 경로 import를 절대 경로 alias로 통일해 주세요.

Line 7-8은 apps/web/src 하위 import 규칙과 불일치합니다.

권장 수정안
-import { getInvitePreview } from '../_apis/getInvitePreview';
-import JoinPreviewClient from './_components/JoinPreviewClient';
+import { getInvitePreview } from '`@/app/tournament/join/_apis/getInvitePreview`';
+import JoinPreviewClient from '`@/app/tournament/join/`[id]/_components/JoinPreviewClient';

As per coding guidelines, apps/web/src/**/*.{ts,tsx}: Use absolute path alias @/* for imports instead of relative paths.

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

In `@apps/web/src/app/tournament/join/`[id]/page.tsx around lines 7 - 8, Change
the two relative imports in page.tsx to use the project's absolute alias (`@/`...)
to match apps/web/src import rules: replace the import of getInvitePreview and
JoinPreviewClient with their corresponding absolute paths (using the `@/` prefix)
so getInvitePreview and JoinPreviewClient are imported from the alias-based
locations instead of '../_apis/getInvitePreview' and
'./_components/JoinPreviewClient'.

Source: Coding guidelines

apps/web/src/app/tournament/[id]/create/_components/invite-friends/InviteFriendsDialog.tsx (1)

64-66: ⚡ Quick win

inviteUrl 유효성을 검증하세요.

share() 호출 전에 inviteUrl이 빈 문자열이 아닌지만 확인하고 있습니다. 잘못된 URL 형식이 전달될 경우 공유가 실패하거나 예상치 못한 동작이 발생할 수 있습니다. URL 형식 검증을 추가하는 것을 권장합니다.

♻️ URL 형식 검증 추가
  const handleSendInviteLink = async () => {
-   if (!inviteUrl) return;
+   if (!inviteUrl || !/^https?:\/\/.+/.test(inviteUrl)) {
+     toast.warning('유효하지 않은 초대 링크입니다.');
+     return;
+   }

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

In
`@apps/web/src/app/tournament/`[id]/create/_components/invite-friends/InviteFriendsDialog.tsx
around lines 64 - 66, handleSendInviteLink에서 inviteUrl이 빈 값인지뿐 아니라 올바른 URL 형식인지
검증하도록 수정하세요: inviteUrl 문자열을 공유하기 전에 URL 생성자(new URL(inviteUrl)) 또는 적절한 정규식으로 유효성
검사하고 protocol이 http 또는 https인지 확인한 뒤 유효하지 않으면 사용자에게 에러 토스트/메시지를 표시하고 share() 호출을
막으세요; URL 변환이나 검증 중 예외가 날 수 있으니 handleSendInviteLink 내부에서 try/catch로 잡아 에러를
로깅하거나 사용자에게 알리는 처리를 추가하세요.
apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx (2)

40-54: ⚡ Quick win

onScrolled 콜백 의존성이 불필요한 재실행을 유발할 수 있습니다.

Effect가 onScrolled에 의존하고 있어, 부모 컴포넌트에서 이 콜백을 메모이제이션하지 않으면 매 렌더링마다 Effect가 재실행됩니다. 부모에서 useCallback으로 래핑하거나, Effect 내부에서 최신 참조를 사용하도록 패턴을 변경하는 것을 권장합니다.

♻️ useRef로 최신 콜백 참조 유지
+ const onScrolledRef = useRef(onScrolled);
+ useLayoutEffect(() => {
+   onScrolledRef.current = onScrolled;
+ });
+
  useEffect(() => {
    if (!isCarouselEnabled) {
      prevItemCountRef.current = items.length;
      return;
    }

    if (!carouselApi) return;

    if (items.length > prevItemCountRef.current) {
      carouselApi.scrollTo(getBasketIndexForLastItem(items.length));
-     onScrolled?.();
+     onScrolledRef.current?.();
    }

    prevItemCountRef.current = items.length;
- }, [carouselApi, isCarouselEnabled, items.length, onScrolled]);
+ }, [carouselApi, isCarouselEnabled, items.length]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx
around lines 40 - 54, The effect currently lists onScrolled in its dependency
array causing unnecessary re-runs when the parent doesn't memoize the callback;
change to store the latest onScrolled in a ref (e.g., onScrolledRef.current)
outside the main useEffect and update that ref whenever onScrolled changes, then
remove onScrolled from the effect dependencies and call
onScrolledRef.current?.() inside the effect after scroll; keep other symbols
intact (useEffect, prevItemCountRef, carouselApi, isCarouselEnabled,
items.length, getBasketIndexForLastItem) so the effect only depends on stable
values and uses the ref for the latest callback.

40-54: 바구니 가득 참 알림 제거로 인한 사용자 피드백 감소를 검토하세요.

이전 구현에서는 바구니가 가득 찼을 때 toast로 "카트가 꽉 찼어요" 알림을 표시했으나, 현재 버전에서는 해당 로직이 제거되었습니다. 사용자가 더 이상 아이템을 추가할 수 없는 시점을 명확히 인지하지 못할 수 있습니다. 대체 피드백(UI 상태 표시, 버튼 비활성화 등)이 다른 곳에 구현되었는지 확인하세요.

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

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx
around lines 40 - 54, The carousel component removed the user-facing "basket
full" feedback; restore a clear notification and disable add actions when
capacity is reached by updating TournamentItemBasketCarousel
(useEffect/prevItemCountRef) to check against the basket capacity (e.g., a
maxItems prop or existing capacity constant) and trigger the appropriate UI
feedback (show toast "카트가 꽉 찼어요" or set a disabled state for add buttons) when
items.length >= capacity; ensure the new logic lives near the existing useEffect
that tracks prevItemCountRef and does not interfere with carouselApi/onScrolled
behavior, and if capacity is controlled by a parent component, propagate a
callback or prop (e.g., onBasketFull or maxItems) so the parent can disable add
controls and/or render the toast.
apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/EmptyBasketSlot.tsx (1)

11-14: ⚡ Quick win

담기 마감 시 빈 슬롯의 시각적 피드백이 누락되었습니다.

isDepositClosedtrue일 때 완전히 빈 <div>만 렌더링되어 사용자에게 아무런 시각적 단서를 제공하지 않습니다. TODO 주석에 언급된 디자인 SVG가 적용될 때까지 임시 스타일(예: 회색 배경, 아이콘, 텍스트 등)을 추가하는 것을 권장합니다.

♻️ 임시 시각적 피드백 추가
  if (isDepositClosed) {
-   // TODO: 디자이너가 전달한 빈 슬롯 SVG로 교체
-   return <div className="relative aspect-square w-full" />;
+   return (
+     <div className="relative aspect-square w-full rounded-2xl border-[3px] border-white bg-gray-100 shadow-[0_0_8px_rgba(0,0,0,0.16)]" />
+   );
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournament-item-basket/EmptyBasketSlot.tsx
around lines 11 - 14, When isDepositClosed is true in the EmptyBasketSlot
component, replace the empty <div> return with a visually informative
placeholder: render a square container with a muted gray background, subtle
border or rounded corners, centered icon (e.g., a lock or X) and a short label
like "담기 마감" to indicate deposits are closed; keep the existing aspect-square
w-full wrapper but add utility classes (background-gray-100/200, border, flex,
items-center, justify-center, gap, text-xs/typo classes) and an accessible
aria-label so screen-readers know the slot is closed. Locate EmptyBasketSlot and
update the block guarded by isDepositClosed to render this temporary styled
placeholder until the designer SVG is provided.
apps/web/src/app/tournament/[id]/create/_components/tournament-header/TournamentHeader.tsx (1)

38-50: 💤 Low value

헤더 높이 고정이 접근성 및 반응형 레이아웃에 영향을 줄 수 있습니다.

h-7.5(30px) 고정 높이는 긴 토너먼트 이름이나 폰트 크기 확대 시 텍스트가 잘릴 수 있습니다. 헤더 높이를 콘텐츠에 맞춰 자동 조정(min-h-7.5 + py-*)하거나, 타이틀에 truncate/line-clamp를 적용하는 것을 고려하세요.

♻️ 유연한 헤더 높이
-     <header className="relative flex h-7.5 w-full shrink-0 items-center">
+     <header className="relative flex min-h-7.5 w-full shrink-0 items-center py-1">
        <button
          type="button"
          aria-label="뒤로가기"
          onClick={handleBackClick}
          className="cursor-pointer p-0.75"
        >
          <ChevronBackwardIconFill className="size-6 text-icon-neutral-secondary" />
        </button>
-       <h1 className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 title-1">
+       <h1 className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 truncate title-1">
          {name}
        </h1>
      </header>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournament-header/TournamentHeader.tsx
around lines 38 - 50, The fixed header height (class h-7.5) can cause truncation
when names are long or font sizes are increased; update the header container
(the <header> element rendered by TournamentHeader) to use a flexible height
approach (replace h-7.5 with min-h-7.5 and add vertical padding classes like
py-1 or py-2) so it grows with content, and add truncation or clamping to the
title element (the h1 with class title-1) by adding appropriate utility classes
such as truncate or line-clamp to prevent overflow while preserving
accessibility and responsive layout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/app/home/_components/invite-code-dialog/InviteCodeDialog.tsx`:
- Around line 31-38: The success handlers are using the live `code` state which
can change while the mutation is pending; capture the submitted code into a
local const immediately before calling the mutation and then use that captured
value in the onSuccess handlers (e.g., for the preview mutation created via
useMutation with mutationFn getInvitePreviewByCode and any join mutation such as
joinMutation/joinTournamentWithCode) so routing uses the exact code that was
validated rather than the current input state; update the call sites to store
const submittedCode = code and reference submittedCode inside the corresponding
onSuccess (router.push) and reset logic.

In `@apps/web/src/app/play/`[id]/_components/PlayClient.tsx:
- Around line 60-70: The run function is not awaiting the async navigation
helper goToTournament, so navigation errors (from getTournament) escape the
try/catch and leave the UI stuck; update run to await goToTournament(...)
wherever it is called (both after postFromPlayLink and after the postGuestLogin
+ postFromPlayLink retry) so any navigation failure is caught by run's catch and
loading/error state can be handled; ensure the retry branch still returns only
after awaiting the second goToTournament call.

In
`@apps/web/src/app/tournament/`[id]/create/_components/invite-friends/InviteFriendsDialog.tsx:
- Around line 64-77: The handleSendInviteLink should prevent sharing when the
invite is expired: inside handleSendInviteLink (and/or the share button's
onClick) check inviteExpiresAt or expiresInfo.remainingLabel and if
expiresInfo.remainingLabel === '마감' (or inviteExpiresAt is in the past) then do
not call share; instead call toast.warning to inform the user the link is
expired and suggest regenerating the invite (or open the regen flow), otherwise
proceed with the current share(...) logic using inviteUrl and keep the existing
success/failure toasts.

In
`@apps/web/src/app/tournament/`[id]/create/_components/participant-panel/ParticipantPanel.tsx:
- Around line 50-53: The invite flow currently opens even when inviteCode is
missing; update the UI and handlers in ParticipantPanel to guard on
inviteCode/inviteUrl: change handleOpenInvite (and any click handlers or CTA
render logic that currently calls setIsInviteDialogOpen) to first check
inviteCode (or inviteUrl truthiness) and only call setIsInviteDialogOpen(true)
when present, and also disable or render the invite button as inactive when
inviteCode is falsy so users cannot trigger an empty invite dialog; apply the
same guard to the other invite-related handlers/renders referenced in this
component (the other invite CTA areas around the second and third invite
usages).

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournament-header/TournamentHeader.tsx:
- Around line 16-29: The back-button logic doesn't handle loading from
useGetTournament, so hasFriends computed from tournamentData.pending can be
incorrect; update TournamentHeader to read the loading flag from
useGetTournament (e.g., isLoading or status) and change handleBackClick to
early-return or disable navigation while loading, or compute hasFriends only
when not loading (e.g., when tournamentData and tournamentData.pending are
defined); ensure you still open the exit confirm via setIsExitConfirmOpen(true)
when participants indicate friends and only call router.back() when not loading
and no friends are present.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournament-start-button/TournamentStartButton.tsx:
- Around line 39-48: The tooltip visibility state (isTooltipVisible) is only
initialized from isWaitingForOwnerStart and never set to true when
isWaitingForOwnerStart later becomes true; update the useEffect that currently
references isWaitingForOwnerStart to explicitly call setIsTooltipVisible(true)
when isWaitingForOwnerStart becomes true, then start the existing timeout
(window.setTimeout with PARTICIPANT_TOOLTIP_DURATION_MS) to hide it and clear
the timeout on cleanup; modify the effect that touches isTooltipVisible /
setIsTooltipVisible (in TournamentStartButton.tsx) so it both sets visibility to
true on change to true and still clears visibility after the duration.

In
`@apps/web/src/app/tournament/`[id]/create/_components/TournamentCreateClient.tsx:
- Around line 45-50: The code currently sets depositDeadline to '' which makes
useCountdown treat a missing inviteExpiresAt as not-expired (fail-open); change
the logic so a missing tournamentData.pending?.inviteExpiresAt is treated as
expired (fail-closed) — either pass undefined/null to useCountdown and make
useCountdown return isExpired=true for empty/undefined inputs, or explicitly
compute isDepositClosed by checking for a missing inviteExpiresAt (e.g., const
hasInvite = Boolean(tournamentData.pending?.inviteExpiresAt); const
isDepositClosed = !tournamentData.isOwner && !ownerStarted && (!hasInvite ||
isExpired)). Update references to depositDeadline, useCountdown and
isDepositClosed accordingly (also apply same fix at the other occurrence around
line 107).

In
`@apps/web/src/app/tournament/`[id]/result/_components/plate-share-dialog/PlateShareDialog.tsx:
- Around line 34-45: The code treats any present initialPlayLinkExpiresAt as
valid; change the logic in handleSendPlayLink to consider expiration: compute
whether initialPlayLinkExpiresAt is in the future (e.g., new
Date(initialPlayLinkExpiresAt) > new Date()) instead of
Boolean(initialPlayLinkExpiresAt), and only skip calling postPlayLinkMutation
when the timestamp exists and is not expired; reference and update
hasExistingPlayLink (or inline the check) and ensure postPlayLinkMutation is
invoked when the stored expiry is past so an up-to-date link is generated.

In
`@apps/web/src/app/tournament/`[id]/result/group/_components/GroupResultClient.tsx:
- Around line 60-65: The component reads tournamentData.name into tournamentName
before guarding for loading/error, causing a crash when useGetTournament is
still loading; move the tournamentName extraction (tournamentData.name) to after
the early-return guard that checks isGroupResultPending, isGroupResultError, and
!groupResultData (and add an explicit guard for !tournamentData), or access the
name conditionally (e.g., tournamentData?.name) so the initial render cannot
throw; update references to tournamentName in GroupResultClient to use the
relocated or guarded value.

In `@apps/web/src/app/tournament/join/_apis/postJoinGuest.ts`:
- Line 5: The import in postJoinGuest.ts uses a relative path for the types
(PostJoinGuestRequestT, PostJoinGuestResponseT from '../_types/join'); update
that import to use the project alias form (@"/*" style) so it follows the
apps/web src import rule — replace the '../_types/join' specifier with the
corresponding aliased path (importing the same PostJoinGuestRequestT and
PostJoinGuestResponseT) in the postJoinGuest.ts file.

In `@apps/web/src/app/tournament/join/_hooks/usePostJoinGuest.ts`:
- Line 3: Update the import in usePostJoinGuest.ts to use the project alias
instead of a relative path: replace the relative import of postJoinGuest (import
{ postJoinGuest } from '../_apis/postJoinGuest') with the equivalent aliased
path using the `@/` prefix (keep the same named import postJoinGuest and ensure
the path points to the same _apis/postJoinGuest module via the alias).

In `@apps/web/src/app/tournament/join/`[id]/_components/JoinPreviewClient.tsx:
- Around line 35-36: The nickname duplication check and the submit use different
normalization (useGetNicknameCheck is called with raw nickname while
postJoinGuestMutation uses trimmedNickname), causing a race where a nickname
with surrounding spaces can pass the check but fail on submit; fix by
normalizing consistently before both operations—compute trimmedNickname (e.g.,
const trimmedNickname = nickname.trim()) and call
useGetNicknameCheck(trimmedNickname) and use postJoinGuestMutation with
trimmedNickname (or ensure both use the same normalized value), updating
references nicknameCheckData/isNicknameCheckFetching and
postJoinGuestMutation/isPostJoinGuestPending usage accordingly so both
validation and submission use the identical normalized nickname.

In `@apps/web/src/app/tournament/join/`[id]/page.tsx:
- Around line 23-25: The prefetch currently uses queryKey ['invitePreview',
tournamentId] and calls getInvitePreview(tournamentId) but ignores the URL
invite code variable (code); update the prefetch so the cache key includes the
invite code (e.g., ['invitePreview', tournamentId, code]) and call
getInvitePreview with the code (e.g., getInvitePreview(tournamentId, code)),
also handle the case where code is undefined by early-returning or using a no-op
prefetch to avoid storing unverified hydration data; relevant symbols:
queryClient.prefetchQuery, queryKey ['invitePreview', tournamentId], and
getInvitePreview.

In `@docs/social-tournament-api-integration.md`:
- Around line 11-27: Update the ApiResponseBody definition to match the team API
contract: replace the current shape that uses data/detail/pageResponse with the
approved structure including status, data, detail, and code, and rename
pageResponse to pageInfo (ensuring pageInfo contains nextCursor and hasNext
semantics). Locate the ApiResponseBody declaration and any examples or
references in this doc (search for "ApiResponseBody", "pageResponse", and the
example JSON) and change the example JSON and explanatory bullets to show {
status, data, detail, code } and pageInfo, and update notes about the axios
interceptor to reference .data.data consistent with the new wrapper.
- Around line 62-313: The endpoint paths in the 2.x detailed spec (e.g., "GET
`/tournaments/{id}/invite-preview`", "POST `/tournaments/{id}/join`", "POST
`/tournaments/{id}/join/guest`", etc.) are missing the `/api/v1` prefix and must
be made consistent with the top-level list; update every endpoint string in this
document to use the `/api/v1` prefix (e.g.,
`/api/v1/tournaments/{id}/invite-preview`) including examples and failure codes,
and also ensure any referenced client/new API helper names and file mentions
from the diff (e.g., getInvitePreview, postJoin, postJoinGuest,
getNicknameCheck, postPlayLink, getPlayLinkInfo, postFromPlayLink,
getGroupResult and their corresponding use* hooks and new API file notes)
reflect the canonical `/api/v1` paths so later constantization/routing uses a
single format.

---

Nitpick comments:
In `@apps/web/src/apis/getNicknameCheck.ts`:
- Around line 7-8: Replace the relative imports for clientApi and serverApi in
getNicknameCheck.ts with the project absolute alias import form (use the `@/`
alias) so they follow the apps/web/src import convention; update the import
statements that bring in clientApi and serverApi to use the alias path that
resolves to the same modules and run a quick typecheck to ensure imports still
resolve.

In `@apps/web/src/app/home/_components/InviteTournamentButton.tsx`:
- Line 7: Replace the relative import of InviteCodeDialog with the project alias
path: update the import statement in InviteTournamentButton.tsx to use the
'`@/app/home/_components/invite-code-dialog/InviteCodeDialog`' absolute alias
(i.e., swap the current "./invite-code-dialog/InviteCodeDialog" for the
equivalent "`@/`..." path) so it follows the apps/web/src alias convention and
keeps imports consistent.

In `@apps/web/src/app/invite/`[id]/page.tsx:
- Line 5: Replace the relative import of the InviteClient component with the
project absolute alias: change the import that currently references
'./_components/InviteClient' to use the '`@/`...' alias path that points to the
same InviteClient module (i.e., import InviteClient from '`@/`…/InviteClient'
using the app's `@/` root alias) so the file conforms to the
apps/web/src/**/*.{ts,tsx} rule; keep the imported symbol name InviteClient
unchanged.

In
`@apps/web/src/app/tournament/`[id]/create/_components/invite-friends/InviteFriendsDialog.tsx:
- Around line 64-66: handleSendInviteLink에서 inviteUrl이 빈 값인지뿐 아니라 올바른 URL 형식인지
검증하도록 수정하세요: inviteUrl 문자열을 공유하기 전에 URL 생성자(new URL(inviteUrl)) 또는 적절한 정규식으로 유효성
검사하고 protocol이 http 또는 https인지 확인한 뒤 유효하지 않으면 사용자에게 에러 토스트/메시지를 표시하고 share() 호출을
막으세요; URL 변환이나 검증 중 예외가 날 수 있으니 handleSendInviteLink 내부에서 try/catch로 잡아 에러를
로깅하거나 사용자에게 알리는 처리를 추가하세요.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournament-header/TournamentHeader.tsx:
- Around line 38-50: The fixed header height (class h-7.5) can cause truncation
when names are long or font sizes are increased; update the header container
(the <header> element rendered by TournamentHeader) to use a flexible height
approach (replace h-7.5 with min-h-7.5 and add vertical padding classes like
py-1 or py-2) so it grows with content, and add truncation or clamping to the
title element (the h1 with class title-1) by adding appropriate utility classes
such as truncate or line-clamp to prevent overflow while preserving
accessibility and responsive layout.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournament-item-basket/EmptyBasketSlot.tsx:
- Around line 11-14: When isDepositClosed is true in the EmptyBasketSlot
component, replace the empty <div> return with a visually informative
placeholder: render a square container with a muted gray background, subtle
border or rounded corners, centered icon (e.g., a lock or X) and a short label
like "담기 마감" to indicate deposits are closed; keep the existing aspect-square
w-full wrapper but add utility classes (background-gray-100/200, border, flex,
items-center, justify-center, gap, text-xs/typo classes) and an accessible
aria-label so screen-readers know the slot is closed. Locate EmptyBasketSlot and
update the block guarded by isDepositClosed to render this temporary styled
placeholder until the designer SVG is provided.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx:
- Around line 40-54: The effect currently lists onScrolled in its dependency
array causing unnecessary re-runs when the parent doesn't memoize the callback;
change to store the latest onScrolled in a ref (e.g., onScrolledRef.current)
outside the main useEffect and update that ref whenever onScrolled changes, then
remove onScrolled from the effect dependencies and call
onScrolledRef.current?.() inside the effect after scroll; keep other symbols
intact (useEffect, prevItemCountRef, carouselApi, isCarouselEnabled,
items.length, getBasketIndexForLastItem) so the effect only depends on stable
values and uses the ref for the latest callback.
- Around line 40-54: The carousel component removed the user-facing "basket
full" feedback; restore a clear notification and disable add actions when
capacity is reached by updating TournamentItemBasketCarousel
(useEffect/prevItemCountRef) to check against the basket capacity (e.g., a
maxItems prop or existing capacity constant) and trigger the appropriate UI
feedback (show toast "카트가 꽉 찼어요" or set a disabled state for add buttons) when
items.length >= capacity; ensure the new logic lives near the existing useEffect
that tracks prevItemCountRef and does not interfere with carouselApi/onScrolled
behavior, and if capacity is controlled by a parent component, propagate a
callback or prop (e.g., onBasketFull or maxItems) so the parent can disable add
controls and/or render the toast.

In `@apps/web/src/app/tournament/join/_apis/getInvitePreview.ts`:
- Line 8: The import in getInvitePreview.ts uses a relative path for the type
GetInvitePreviewResponseT; update the import to use the project absolute alias
(e.g. import type { GetInvitePreviewResponseT } from
'`@/app/tournament/join/_types/join`') so it matches the
apps/web/src/**/*.{ts,tsx} rule; locate the import statement for
GetInvitePreviewResponseT and replace the relative path with the corresponding
'`@/`...' alias.

In `@apps/web/src/app/tournament/join/`[id]/page.tsx:
- Around line 7-8: Change the two relative imports in page.tsx to use the
project's absolute alias (`@/`...) to match apps/web/src import rules: replace the
import of getInvitePreview and JoinPreviewClient with their corresponding
absolute paths (using the `@/` prefix) so getInvitePreview and JoinPreviewClient
are imported from the alias-based locations instead of
'../_apis/getInvitePreview' and './_components/JoinPreviewClient'.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c6d4164f-19a8-4b0a-b6e1-aea38d776ed9

📥 Commits

Reviewing files that changed from the base of the PR and between 1e148eb and a52d326.

⛔ Files ignored due to path filters (4)
  • apps/web/src/app/home/_assets/sad-face.svg is excluded by !**/*.svg
  • apps/web/src/app/tournament/[id]/create/_assets/confirm-exit-basket.svg is excluded by !**/*.svg
  • apps/web/src/app/tournament/[id]/create/_assets/confirm-start-face.svg is excluded by !**/*.svg
  • apps/web/src/assets/icons/fill/timer.svg is excluded by !**/*.svg
📒 Files selected for processing (76)
  • apps/web/src/apis/client.ts
  • apps/web/src/apis/getNicknameCheck.ts
  • apps/web/src/app/archive/_components/wish-grid/index.tsx
  • apps/web/src/app/home/_components/InviteTournamentButton.tsx
  • apps/web/src/app/home/_components/TournamentList.tsx
  • apps/web/src/app/home/_components/invite-code-dialog/InvalidCodeDialog.tsx
  • apps/web/src/app/home/_components/invite-code-dialog/InviteCodeDialog.tsx
  • apps/web/src/app/home/page.tsx
  • apps/web/src/app/invite/[id]/_components/InviteClient.tsx
  • apps/web/src/app/invite/[id]/page.tsx
  • apps/web/src/app/layout.tsx
  • apps/web/src/app/login/_apis/postGuestLogin.ts
  • apps/web/src/app/login/_components/LoginButtons.tsx
  • apps/web/src/app/login/_components/SocialLoginButton.tsx
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/play/[id]/_components/PlayClient.tsx
  • apps/web/src/app/play/[id]/page.tsx
  • apps/web/src/app/play/_apis/postFromPlayLink.ts
  • apps/web/src/app/play/_types/play.ts
  • apps/web/src/app/tournament/[id]/_common/_types/tournamentResponse.ts
  • apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx
  • apps/web/src/app/tournament/[id]/create/_components/deposit-countdown/DepositCountdown.tsx
  • apps/web/src/app/tournament/[id]/create/_components/invite-friends/InviteFriends.tsx
  • apps/web/src/app/tournament/[id]/create/_components/invite-friends/InviteFriendsDialog.tsx
  • apps/web/src/app/tournament/[id]/create/_components/member-join-confirm-dialog/MemberJoinConfirmDialog.tsx
  • apps/web/src/app/tournament/[id]/create/_components/participant-panel/ParticipantChip.tsx
  • apps/web/src/app/tournament/[id]/create/_components/participant-panel/ParticipantPanel.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-header/ConfirmExitDialog.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-header/TournamentHeader.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket-status/TournamentItemBasketStatus.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/EmptyBasketSlot.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasket.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-start-button/ConfirmStartDialog.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-start-button/TournamentStartButton.tsx
  • apps/web/src/app/tournament/[id]/create/_components/welcome-join-dialog/WelcomeJoinDialog.tsx
  • apps/web/src/app/tournament/[id]/create/_hooks/useCountdown.ts
  • apps/web/src/app/tournament/[id]/create/_hooks/useGetTournamentItem.ts
  • apps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentItemLink.ts
  • apps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentStart.ts
  • apps/web/src/app/tournament/[id]/create/_types/tournament.ts
  • apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectHeader.tsx
  • apps/web/src/app/tournament/[id]/create/page.tsx
  • apps/web/src/app/tournament/[id]/match/_hooks/useTournament.ts
  • apps/web/src/app/tournament/[id]/match/page.tsx
  • apps/web/src/app/tournament/[id]/result/_apis/getGroupResult.ts
  • apps/web/src/app/tournament/[id]/result/_apis/postPlayLink.ts
  • apps/web/src/app/tournament/[id]/result/_components/ReceiptDrawMachine.tsx
  • apps/web/src/app/tournament/[id]/result/_components/ReceiptPaper.tsx
  • apps/web/src/app/tournament/[id]/result/_components/ResultClient.tsx
  • apps/web/src/app/tournament/[id]/result/_components/group-result-entry-card/GroupResultEntryCard.tsx
  • apps/web/src/app/tournament/[id]/result/_components/plate-share-dialog/PlateShareDialog.tsx
  • apps/web/src/app/tournament/[id]/result/_hooks/useGetGroupResult.ts
  • apps/web/src/app/tournament/[id]/result/_hooks/usePostPlayLink.ts
  • apps/web/src/app/tournament/[id]/result/_types/groupResult.ts
  • apps/web/src/app/tournament/[id]/result/group/_components/GroupResultClient.tsx
  • apps/web/src/app/tournament/[id]/result/group/page.tsx
  • apps/web/src/app/tournament/[id]/result/page.tsx
  • apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx
  • apps/web/src/app/tournament/join/[id]/page.tsx
  • apps/web/src/app/tournament/join/_apis/getInvitePreview.ts
  • apps/web/src/app/tournament/join/_apis/getInvitePreviewByCode.ts
  • apps/web/src/app/tournament/join/_apis/postJoinGuest.ts
  • apps/web/src/app/tournament/join/_consts/randomNickname.ts
  • apps/web/src/app/tournament/join/_hooks/useGetInvitePreview.ts
  • apps/web/src/app/tournament/join/_hooks/usePostJoinGuest.ts
  • apps/web/src/app/tournament/join/_types/join.ts
  • apps/web/src/app/tournament/join/_utils/joinSession.ts
  • apps/web/src/app/tournament/join/_utils/verifyInviteCode.ts
  • apps/web/src/components/user-profile-group/UserProfile.tsx
  • apps/web/src/consts/api.ts
  • apps/web/src/consts/route.ts
  • apps/web/src/hooks/useGetNicknameCheck.ts
  • apps/web/src/types/user.ts
  • apps/web/src/utils/share.ts
  • docs/social-tournament-api-integration.md
💤 Files with no reviewable changes (1)
  • apps/web/src/app/tournament/[id]/create/_components/invite-friends/InviteFriends.tsx

Comment thread apps/web/src/app/play/[id]/_components/PlayClient.tsx
Comment on lines +64 to +77
const handleSendInviteLink = async () => {
if (!inviteUrl) return;

const result = await share({
title: 'piki 토너먼트 초대',
text: '친구와 함께 piki 토너먼트에 담아봐요!',
url: inviteUrl,
});

if (result === 'shared' || result === 'copied') {
toast.success('링크를 성공적으로 공유했어요.');
}
if (result === 'failed') toast.warning('공유에 실패했어요. 다시 시도해주세요.');
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

만료된 초대 링크 공유 가능 여부를 검증하세요.

현재 inviteExpiresAt이 과거 시각이어도 공유 버튼이 활성화되어 있습니다. expiresInfo.remainingLabel'마감'일 때는 공유를 차단하거나, 사용자에게 재생성을 안내하는 것이 더 나은 UX입니다.

🛡️ 만료 시 공유 차단 로직 제안
  const handleSendInviteLink = async () => {
    if (!inviteUrl) return;
+   if (expiresInfo?.remainingLabel === '마감') {
+     toast.warning('초대 링크가 만료되었어요. 새로 생성해주세요.');
+     return;
+   }

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

In
`@apps/web/src/app/tournament/`[id]/create/_components/invite-friends/InviteFriendsDialog.tsx
around lines 64 - 77, The handleSendInviteLink should prevent sharing when the
invite is expired: inside handleSendInviteLink (and/or the share button's
onClick) check inviteExpiresAt or expiresInfo.remainingLabel and if
expiresInfo.remainingLabel === '마감' (or inviteExpiresAt is in the past) then do
not call share; instead call toast.warning to inform the user the link is
expired and suggest regenerating the invite (or open the regen flow), otherwise
proceed with the current share(...) logic using inviteUrl and keep the existing
success/failure toasts.

Comment on lines +50 to +53
const inviteUrl = inviteCode ? buildInviteUrl(tournamentId, inviteCode) : '';

const handleToggleExpand = () => setIsExpanded(prev => !prev);
const handleOpenInvite = () => setIsInviteDialogOpen(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

inviteCode가 없는데도 초대 동작이 활성화됩니다

현재 inviteUrl이 빈 문자열이어도 초대 버튼/다이얼로그가 열릴 수 있습니다. inviteCode 존재 여부로 CTA를 가드해서 빈 링크 공유 플로우를 막아주세요.

수정 예시
-  const inviteUrl = inviteCode ? buildInviteUrl(tournamentId, inviteCode) : '';
+  const canInvite = Boolean(inviteCode);
+  const inviteUrl = canInvite ? buildInviteUrl(tournamentId, inviteCode) : '';

-  const handleOpenInvite = () => setIsInviteDialogOpen(true);
+  const handleOpenInvite = () => {
+    if (!canInvite) return;
+    setIsInviteDialogOpen(true);
+  };
...
-              <button
-                type="button"
-                onClick={handleOpenInvite}
+              {canInvite && (
+              <button
+                type="button"
+                onClick={handleOpenInvite}
                 className="inline-flex size-9 cursor-pointer items-center justify-center rounded-full border border-border-neutral-muted bg-bg-layer-default"
                 aria-label="친구 초대하기"
               >
                 <AddIconOutline className="size-4 text-icon-neutral-primary" />
               </button>
+              )}
...
       <InviteFriendsDialog
-        open={isInviteDialogOpen}
+        open={canInvite && isInviteDialogOpen}
         onOpenChange={setIsInviteDialogOpen}
         inviteUrl={inviteUrl}
         inviteExpiresAt={inviteExpiresAt}
       />

Also applies to: 80-96, 118-123

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

In
`@apps/web/src/app/tournament/`[id]/create/_components/participant-panel/ParticipantPanel.tsx
around lines 50 - 53, The invite flow currently opens even when inviteCode is
missing; update the UI and handlers in ParticipantPanel to guard on
inviteCode/inviteUrl: change handleOpenInvite (and any click handlers or CTA
render logic that currently calls setIsInviteDialogOpen) to first check
inviteCode (or inviteUrl truthiness) and only call setIsInviteDialogOpen(true)
when present, and also disable or render the invite button as inactive when
inviteCode is falsy so users cannot trigger an empty invite dialog; apply the
same guard to the other invite-related handlers/renders referenced in this
component (the other invite CTA areas around the second and third invite
usages).

@@ -0,0 +1,11 @@
import { useMutation } from '@tanstack/react-query';

import { postJoinGuest } from '../_apis/postJoinGuest';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

상대 경로 import를 @/* 절대 경로로 통일해주세요.

현재 ../_apis/postJoinGuest는 프로젝트 import 규칙과 불일치합니다.

변경 제안
-import { postJoinGuest } from '../_apis/postJoinGuest';
+import { postJoinGuest } from '`@/app/tournament/join/_apis/postJoinGuest`';

As per coding guidelines, apps/web/src/**/*.{ts,tsx}에서는 상대 경로 대신 @/* alias를 사용해야 합니다.

📝 Committable suggestion

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

Suggested change
import { postJoinGuest } from '../_apis/postJoinGuest';
import { postJoinGuest } from '`@/app/tournament/join/_apis/postJoinGuest`';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/tournament/join/_hooks/usePostJoinGuest.ts` at line 3,
Update the import in usePostJoinGuest.ts to use the project alias instead of a
relative path: replace the relative import of postJoinGuest (import {
postJoinGuest } from '../_apis/postJoinGuest') with the equivalent aliased path
using the `@/` prefix (keep the same named import postJoinGuest and ensure the
path points to the same _apis/postJoinGuest module via the alias).

Source: Coding guidelines

Comment on lines +35 to +36
const { nicknameCheckData, isNicknameCheckFetching } = useGetNicknameCheck(nickname);
const { postJoinGuestMutation, isPostJoinGuestPending } = usePostJoinGuest();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

닉네임 중복 검사와 제출 값의 정규화 기준을 일치시켜주세요.

현재 중복 검사는 nickname(원본)으로 하고, 제출은 trimmedNickname으로 해서 공백 포함 입력 시
검사 통과 후 제출 실패가 발생할 수 있습니다.

수정 예시
-  const { nicknameCheckData, isNicknameCheckFetching } = useGetNicknameCheck(nickname);
+  const { nicknameCheckData, isNicknameCheckFetching } = useGetNicknameCheck(trimmedNickname);

Also applies to: 52-53

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

In `@apps/web/src/app/tournament/join/`[id]/_components/JoinPreviewClient.tsx
around lines 35 - 36, The nickname duplication check and the submit use
different normalization (useGetNicknameCheck is called with raw nickname while
postJoinGuestMutation uses trimmedNickname), causing a race where a nickname
with surrounding spaces can pass the check but fail on submit; fix by
normalizing consistently before both operations—compute trimmedNickname (e.g.,
const trimmedNickname = nickname.trim()) and call
useGetNicknameCheck(trimmedNickname) and use postJoinGuestMutation with
trimmedNickname (or ensure both use the same normalized value), updating
references nicknameCheckData/isNicknameCheckFetching and
postJoinGuestMutation/isPostJoinGuestPending usage accordingly so both
validation and submission use the identical normalized nickname.

Comment on lines +23 to +25
await queryClient.prefetchQuery({
queryKey: ['invitePreview', tournamentId],
queryFn: () => getInvitePreview(tournamentId),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

초대 코드 기반 프리패치 검증이 누락되었습니다.

Line 23-25에서 code를 읽어놓고도 getInvitePreview(tournamentId)로만 조회해, URL의 초대 코드가 서버 프리패치와 캐시 키에 반영되지 않습니다. 코드 불일치 케이스에서 초기 하이드레이션 데이터가 미검증 상태가 될 수 있습니다.

권장 수정안
 async function TournamentJoinPage({ params, searchParams }: TournamentJoinPageProps) {
   const { id } = await params;
   const { code } = await searchParams;
+  const inviteCode = code?.trim() ?? '';
   const tournamentId = parseIdParam(id);

   if (tournamentId === null) notFound();

   const queryClient = getQueryClient();
   await queryClient.prefetchQuery({
-    queryKey: ['invitePreview', tournamentId],
-    queryFn: () => getInvitePreview(tournamentId),
+    queryKey: ['invitePreview', tournamentId, inviteCode],
+    queryFn: () => getInvitePreview(tournamentId, inviteCode || undefined),
   });

   return (
     <HydrationBoundary state={dehydrate(queryClient)}>
-      <JoinPreviewClient tournamentId={tournamentId} inviteCode={code ?? ''} />
+      <JoinPreviewClient tournamentId={tournamentId} inviteCode={inviteCode} />
     </HydrationBoundary>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/tournament/join/`[id]/page.tsx around lines 23 - 25, The
prefetch currently uses queryKey ['invitePreview', tournamentId] and calls
getInvitePreview(tournamentId) but ignores the URL invite code variable (code);
update the prefetch so the cache key includes the invite code (e.g.,
['invitePreview', tournamentId, code]) and call getInvitePreview with the code
(e.g., getInvitePreview(tournamentId, code)), also handle the case where code is
undefined by early-returning or using a no-op prefetch to avoid storing
unverified hydration data; relevant symbols: queryClient.prefetchQuery, queryKey
['invitePreview', tournamentId], and getInvitePreview.

Comment on lines +11 to +27
## 0. 공통 응답 래퍼

모든 응답은 `ApiResponseBody` 로 래핑됨.

```json
{
"data": {
/* 실제 응답 */
},
"detail": "정상적으로 처리되었습니다.",
"pageResponse": { "nextCursor": null, "hasNext": false }
}
```

- 클라이언트는 axios 인터셉터에서 `.data.data` 를 풀어 사용
- 실패 시 `data: null`, `detail`/`code` 에 사유

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

공통 응답 래퍼 정의가 팀 API 계약과 충돌합니다.

여기서는 ApiResponseBodydata/detail/pageResponse로 정의했지만, 가이드 계약은
{ status, data, detail, code }이며 페이징 키도 pageInfo입니다. 이 문서를 기준으로 구현하면
타입/파서/에러처리 방향이 어긋날 수 있어요. 문서 계약을 가이드 기준으로 정정해 주세요.
As per coding guidelines "API responses must use structure: { status, data, detail, code } ... Use pagination ... pageInfo object".

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

In `@docs/social-tournament-api-integration.md` around lines 11 - 27, Update the
ApiResponseBody definition to match the team API contract: replace the current
shape that uses data/detail/pageResponse with the approved structure including
status, data, detail, and code, and rename pageResponse to pageInfo (ensuring
pageInfo contains nextCursor and hasNext semantics). Locate the ApiResponseBody
declaration and any examples or references in this doc (search for
"ApiResponseBody", "pageResponse", and the example JSON) and change the example
JSON and explanatory bullets to show { status, data, detail, code } and
pageInfo, and update notes about the axios interceptor to reference .data.data
consistent with the new wrapper.

Source: Coding guidelines

Comment on lines +62 to +313
### 2.1 GET `/tournaments/{id}/invite-preview`

#### 명세

- query: `inviteCode?: string` — 코드 입력 경로에서만 전달, 직접 링크 진입은 생략
- 응답
```json
{
"tournamentId": 1,
"tournamentName": "내 토너먼트",
"itemCount": 8,
"participantCount": 2
}
```
- 실패: 만료된 초대 → 400, 코드 불일치 → 400

#### 적용 위치

- `apps/web/src/app/home/_components/invite-code-dialog/InviteCodeDialog.tsx`
- 현재: `verifyInviteCode(code)` 로컬 mock
- 변경: 코드만으로는 토너먼트를 못 찾으므로 **초대 링크에 tournamentId 가 함께 들어와야 함** (예: `piki.today/invite/{tournamentId}?code=ABC123`)
- 디자인상 코드 6자만 입력하는 UX 라 백엔드/디자인과 한 번 합의 필요
- 임시 대안: 본인 토너먼트 리스트에서 `inviteCode` 매칭으로 우회

#### 신규 작업

- `apps/web/src/app/tournament/join/_apis/getInvitePreview.ts`
- `useGetInvitePreview` 훅
- 응답 타입 `GetInvitePreviewResponseT` → `src/types/tournament.ts` 또는 `_types/tournament.ts`
- 현재 `mocks/tournamentPreview.ts` 삭제

---

### 2.2 POST `/tournaments/{id}/join`

#### 명세

- body
```ts
type JoinTournamentRequest = {
inviteCode?: string | null; // pattern: [A-Z]{3}\d{3}
};
```
- 응답 데이터: `null`
- JWT 필요 (GUEST/MEMBER 모두 허용)
- 실패: 만료/이미 참여 → 409, 코드 불일치 → 400

#### 적용 위치

- `apps/web/src/app/tournament/join/_components/InviteClient.tsx`
- 현재: `getMe` → identityType 분기 후 `getTournamentList` 첫 ID 로 라우팅 (mock)
- 변경: identityType 분기 후 `join` 호출 → 응답은 `null` 이라 후속 라우팅은 `/tournament/{tournamentId}/create` 로 (path 의 tournamentId 그대로 사용)

#### 신규 작업

- `apps/web/src/app/tournament/join/_apis/postJoin.ts`
- `usePostJoin` (mutation)

---

### 2.3 POST `/tournaments/{id}/join/guest`

#### 명세

- body
```ts
type JoinTournamentAsGuestRequest = {
inviteCode?: string | null;
nickname: string; // maxLength: 10, required
};
```
- 응답
```json
{
"accessToken": "eyJ...",
"refreshToken": "eyJ...",
"userId": "uuid",
"nickname": "멋진친구",
"profileImage": "https://...",
"tournamentId": 1
}
```
- 인증 불필요, 응답으로 토큰 쌍 발급

#### 적용 위치

- `apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx`
- 현재: `getTournamentList` 첫 ID 로 라우팅 (mock) + `setJoinWelcome` sessionStorage
- 변경: `/join/guest` 호출 → 토큰 저장(쿠키 기반이면 자동, 아니면 axios client 갱신) → `/tournament/{tournamentId}/create`

#### 신규 작업

- `apps/web/src/app/tournament/join/_apis/postJoinGuest.ts`
- `usePostJoinGuest` (mutation)
- 토큰 저장 처리 — 현재 `auth/guest` 응답이 HttpOnly 쿠키로 내려옴. `/join/guest` 도 동일하게 쿠키 기반이면 별도 클라 작업 없음. 응답 body 의 `accessToken` 사용이 fallback 인지 확인 필요.

---

### 2.4 GET `/users/nickname/check`

#### 명세

- query: `nickname: string` (required, maxLength 10)
- 응답: `{ available: boolean }`
- 본인의 현재 닉네임은 자동 통과 (서버에서 처리)

#### 적용 위치

- `apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx`
- 닉네임 input `onBlur` 또는 디바운스 후 호출 → 결과에 따라 헬퍼/CTA 비활성

#### 신규 작업

- `apps/web/src/apis/getNicknameCheck.ts`
- `useGetNicknameCheck(debouncedNickname)` 훅 (옵션: `enabled` 로 debounce 결과 활용)

---

### 2.5 PATCH `/users/me`

#### 명세

- body: `{ nickname: string }` (현재는 nickname 만)
- 응답: 수정된 `UserT`
- GUEST 도 호출 가능

#### 적용 위치

- **분담 정리 필요**
- `join/guest` 가 닉네임을 받음 → 게스트 계정 신규 생성 시점에 닉네임 확정
- `PATCH /users/me` → 이미 게스트/회원 인 사용자의 닉네임 변경 (마이페이지 등)
- 현재 PR 범위에서는 `JoinPreviewClient` 의 닉네임 수정은 `join/guest` body 로 전달하면 됨
- `PATCH /users/me` 는 별도 마이페이지 PR 로 분리해도 OK

#### 신규 작업 (별도 PR 가능)

- `apps/web/src/apis/patchMe.ts`
- `usePatchMe` (mutation)
- 성공 시 `['me']` 쿼리 invalidate

---

### 2.6 POST `/tournaments/{id}/play-link`

#### 명세

- body 없음
- 응답: `string` (ISO datetime, `playLinkExpiresAt`)
- 소유자(`isOwner`) 만 호출, 만료 14일 고정, 이미 있으면 갱신
- 실패: COMPLETED 아님 → 409

#### 적용 위치

- `apps/web/src/app/tournament/[id]/result/_components/plate-share-dialog/PlateShareDialog.tsx`
- 현재: `plateUrl` 더미 (`https://piki.today/tournament/{id}/plate`)
- 변경: 다이얼로그 오픈 시 또는 `플레이 링크 보내기` 클릭 시 호출 → URL 생성 → share API 전달
- URL 형식: 디자인/백엔드 합의 필요 (예: `piki.today/play/{tournamentId}` — sourceTournamentId 기반)

#### 신규 작업

- `apps/web/src/app/tournament/[id]/result/_apis/postPlayLink.ts`
- `usePostPlayLink(tournamentId)` (mutation)

---

### 2.7 GET `/tournaments/{id}/play-link-info`

#### 명세

- 인증 불필요
- 응답
```json
{
"sourceTournamentId": 1,
"tournamentName": "내 토너먼트",
"itemCount": 8,
"playLinkExpiresAt": "2026-06-09T22:00:00"
}
```
- 실패: 만료/없음 → 404

#### 적용 위치

- 친구가 플레이 링크로 진입한 화면에서 사용
- 현재 PR 범위 외 (별도 진입점 화면 필요)

#### 신규 작업 (후속)

- 라우트 추가: `apps/web/src/app/play/[id]/page.tsx` 또는 `/tournament/play/[id]` (백엔드 URL 컨벤션과 합의)
- `getPlayLinkInfo.ts` + `useGetPlayLinkInfo` 훅
- 진입 화면: 토너먼트 정보 표시 + `토너먼트 시작하기` → `/tournaments/{sourceId}/from-play-link` 호출

---

### 2.8 POST `/tournaments/{sourceTournamentId}/from-play-link`

#### 명세

- body 없음
- 응답: `number` (새로 생성된 tournamentId)
- 응답 토너먼트는 PENDING 상태로 아이템 복사 완료, 바로 시작 가능

#### 적용 위치

- 2.7 의 후속 — 친구가 `토너먼트 시작하기` 클릭 시
- 응답 tournamentId 로 `/tournament/{newId}/create` 라우팅 (아이템 이미 차있으므로 곧바로 진행 가능)

#### 신규 작업 (후속)

- `apps/web/src/app/play/[id]/_apis/postFromPlayLink.ts`
- `usePostFromPlayLink(sourceTournamentId)` (mutation)

---

### 2.9 GET `/tournaments/{id}/group-result`

#### 명세

- JWT 필요
- 응답
```json
{
"items": [
{
"rank": 1,
"itemId": 10,
"name": "...",
"price": 129000,
"currency": "KRW",
"imageUrl": "https://...",
"chosenBy": [{ "userId": "...", "nickname": "...", "profileImage": "..." }]
}
]
}
```
- 원본 + 플레이 링크 복제본 모두 합쳐서 비교

#### 적용 위치

- `apps/web/src/app/tournament/[id]/result/_components/ResultClient.tsx`
- 조건: `tournamentData.completed.hasGroupResult === true` (참여자 2명 이상)
- 진입점: 결과 화면 상단/하단에 `친구 토너먼트 결과 보기` 버튼 노출 (디자인 합의 필요)

#### 신규 작업

- `apps/web/src/app/tournament/[id]/result/_apis/getGroupResult.ts`
- `useGetGroupResult(tournamentId)` 훅 (조건부 enabled)
- 그룹 결과 화면 컴포넌트 — 디자인 시안 필요

---

### 2.10 GET `/notifications/subscribe` (SSE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

상세 섹션 endpoint 표기에서 /api/v1 접두사 일관성이 깨져 있습니다.

1번 전체 리스트(30-57)는 /api/v1/...인데, 2.x 상세 명세(예: /tournaments/{id}/invite-preview)는
접두사가 빠져 있습니다. 상수화/라우팅 구현 시 잘못된 URL을 만들 위험이 있어 문서 경로를 한 포맷으로
통일하는 게 좋습니다.

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

In `@docs/social-tournament-api-integration.md` around lines 62 - 313, The
endpoint paths in the 2.x detailed spec (e.g., "GET
`/tournaments/{id}/invite-preview`", "POST `/tournaments/{id}/join`", "POST
`/tournaments/{id}/join/guest`", etc.) are missing the `/api/v1` prefix and must
be made consistent with the top-level list; update every endpoint string in this
document to use the `/api/v1` prefix (e.g.,
`/api/v1/tournaments/{id}/invite-preview`) including examples and failure codes,
and also ensure any referenced client/new API helper names and file mentions
from the diff (e.g., getInvitePreview, postJoin, postJoinGuest,
getNicknameCheck, postPlayLink, getPlayLinkInfo, postFromPlayLink,
getGroupResult and their corresponding use* hooks and new API file notes)
reflect the canonical `/api/v1` paths so later constantization/routing uses a
single format.

@kanghaeun kanghaeun left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

진짜 고생 많았다~~ 근데 지금은 모달에서 초대 코드를 입력하도록 되어 있는데 원래는 초대 버튼을 클릭하면 초대 코드 입력 페이지로 이동하는 방식 아니었어? 바뀐건강?

Comment on lines +44 to +52
} catch (error) {
// 400 (코드 불일치) / 409 (만료) — 잘못된 링크로 통합
if (isAxiosError(error)) {
setState('invalid');
return;
}
setState('invalid');
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Axios 에러든 아니든 setState('invalid')로 동일하게 처리하는데 분기로 처리한 이유가 있어?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[반영] 음 에러처리 다르게 하고 싶어서 하긴 했는데 별 의미가 없긴 하네
분기 자체가 불필요한거 같으니 정리할게

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

api랑 type 폴더를 play/[id]/_apis/postFromPlayLink.ts, play/[id]/_types/play.ts로 내려야 할 것 같아~

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

확인!

Comment on lines 18 to 44
@@ -29,7 +43,13 @@ export type PostTournamentItemLinkResponseT = {
tournamentItemId: number;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p2) _common/_types/tournamentResponse.ts에 이미 존재하는 것 같은데 정리가 필요해보여! (이 타입 말고도 중복되어 있는 게 좀 있는 것 같은데 수정할때 같이 부탁행) create/ 전용 타입만 여기에 두고 중복 타입은_common/으로 옮기는 거 어떨까?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+create/_hooks/useGetTournament 이 훅도 삭제해줘!(refetchInterval 사용안함) common에 중복으로 들어가있다

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p2) useGetTournament queryKey는 ['tournament', number]인데 여기서는 string으로 넘기고 있네? 내가 저번에 빠트린 것 같아서 Number(tournamentId)로 수정 같이 해줘!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

확인

isParticipant = false,
}: TournamentStartButtonProps) {
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const { tournamentData } = useGetTournament(Number(tournamentId));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p3) 여기도 TournamentHeader랑 똑같이 useGetTournament 다시 호출되고 있다!

Comment on lines +12 to +13
import { useGetTournament } from '../../../_common/_hooks/useGetTournament';
import { useGetGroupResult } from '../../_hooks/useGetGroupResult';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p4) 전체적으로 alias 적용해줘~ 나중에 파일 이동하거나 할때 오류도 안나고 좋더라구

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

오키~! 이거는 담주에 한꺼번에 정리할게!

Comment on lines +22 to +33
const formatDate = (date: Date) => {
const dd = String(date.getDate()).padStart(2, '0');
const mm = String(date.getMonth() + 1).padStart(2, '0');
const yyyy = date.getFullYear();
return `${dd}/${mm}/${yyyy}`;
};

const formatTime = (date: Date) => {
const hh = String(date.getHours()).padStart(2, '0');
const mm = String(date.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p3) ReceiptPaper.tsx에도 동일 함수가 있네 result/_utils로 옮기는 건 어때?

Comment on lines 31 to 32
const { id: _tournamentId } = useParams<{ id: string }>();
const tournamentId = parseIdParam(_tournamentId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p3) 사용하는 변수니까 _ 삭제해줘! (내가 수정하려고 했다가 까먹어서..

const router = useRouter();
const [nickname, setNickname] = useState(DEFAULT_RANDOM_NICKNAME.nickname);

if (tournamentId === null) notFound();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p2) page.tsx에서 parseIdParam으로 null 체크 후 notFound()를 호출하고 있는데 여기서도 이중으로 검사하는 이유가 있어? 타입도 null 제거하고 number로 좁히면 내부 null 체크도 제거 할 수 있을듯!

Comment on lines +12 to +20
export const RANDOM_NICKNAMES: RandomNicknameT[] = [
{ nickname: '지친 루피', profileType: 'yellow' },
{ nickname: '게으른 사자', profileType: 'blue' },
{ nickname: '행복한 토끼', profileType: 'yellow' },
{ nickname: '활기찬 강쥐', profileType: 'blue' },
{ nickname: '느긋한 키티', profileType: 'yellow' },
];

export const DEFAULT_RANDOM_NICKNAME = RANDOM_NICKNAMES[0]!;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

랜덤 닉네임 api는 없는건가?? 그럼 항상 지친 루피가 기본값인데 이미 이 닉네임을 써버리면 다음 사람은 항상 닉네임을 변경해야 하니까 약간 귀찮을수도..

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/app/tournament/[id]/create/_components/tournament-start-button/TournamentStartButton.tsx (1)

37-46: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

툴팁 가시성 상태가 isWaitingForOwnerStart 전환 시 동기화되지 않습니다.

현재 로직은 초기 마운트 시점의 isWaitingForOwnerStart 값으로만 isTooltipVisible을 설정하고, 이후 해당 prop이 false에서 true로 변경될 때 setIsTooltipVisible(true)를 호출하지 않습니다. effect는 타이머만 설정하므로 툴팁이 표시되지 않습니다.

비동기 데이터 로딩으로 인해 isWaitingForOwnerStart가 나중에 true가 되는 경우가 일반적이므로, 참여자 대기 안내 UX가 작동하지 않습니다.

🐛 툴팁 동기화 수정안
- const [isTooltipVisible, setIsTooltipVisible] = useState(isWaitingForOwnerStart);
+ const [isTooltipVisible, setIsTooltipVisible] = useState(false);

  useEffect(() => {
-   if (!isWaitingForOwnerStart) return;
+   if (!isWaitingForOwnerStart) {
+     setIsTooltipVisible(false);
+     return;
+   }
+   setIsTooltipVisible(true);
    const timeoutId = window.setTimeout(
      () => setIsTooltipVisible(false),
      PARTICIPANT_TOOLTIP_DURATION_MS
    );
    return () => window.clearTimeout(timeoutId);
  }, [isWaitingForOwnerStart]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournament-start-button/TournamentStartButton.tsx
around lines 37 - 46, The tooltip visibility state (isTooltipVisible) isn't
synchronized when isWaitingForOwnerStart flips from false to true; inside the
TournamentStartButton component update the existing useEffect to respond to
changes in isWaitingForOwnerStart by setting setIsTooltipVisible(true) when
isWaitingForOwnerStart becomes true, clear any existing timeout, then start the
timeout that sets setIsTooltipVisible(false) after
PARTICIPANT_TOOLTIP_DURATION_MS; keep the initial
useState(isWaitingForOwnerStart) as-is and ensure the effect returns a cleanup
that clears the timeout.
🧹 Nitpick comments (1)
apps/web/src/app/tournament/[id]/_common/_hooks/useGetTournament.ts (1)

9-11: ⚡ Quick win

폴링 조건을 상태 기반으로 제한하는 것을 권장합니다.

현재 refetchInterval이 모든 화면(생성/매치/결과)에서 무조건 30초마다 실행됩니다. 완료된 토너먼트나 이미 시작된 매치 화면에서는 폴링이 불필요하므로, enabled 옵션을 추가해 status === 'PENDING' && !isOwner 조건일 때만 폴링하도록 최적화하는 것을 고려해보세요.

♻️ 조건부 폴링 예시
 export const useGetTournament = (tournamentId: number) => {
   const { data: tournamentData } = useSuspenseQuery({
     queryKey: ['tournament', tournamentId],
     queryFn: () => getTournament(tournamentId),
-    // 참여자(isOwner=false)는 주최자가 ROOT 를 시작(`ownerStarted=true`) 했는지 감지하려고 주기적으로 polling.
-    // 주최자/완료 화면에선 사실상 무해 (focus refetch 도 함께 동작).
-    refetchInterval: 30_000,
+    refetchInterval: query => {
+      const data = query.state.data;
+      // PENDING 상태이고 참여자일 때만 폴링
+      const shouldPoll = data && 'pending' in data && !data.isOwner;
+      return shouldPoll ? 30_000 : false;
+    },
   });
 
   return { tournamentData };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/tournament/`[id]/_common/_hooks/useGetTournament.ts around
lines 9 - 11, Refactor the polling in the useGetTournament hook by guarding
refetchInterval with React Query's enabled option: keep refetchInterval: 30_000
but add enabled: status === 'PENDING' && !isOwner (or compute a boolean like
shouldPoll = status === 'PENDING' && !isOwner and set enabled: shouldPoll) so
polling only runs for non-owners while the tournament is pending; update any
local variables used to derive status/isOwner if needed so enabled has correct
values at hook initialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@apps/web/src/app/tournament/`[id]/create/_components/tournament-start-button/TournamentStartButton.tsx:
- Around line 37-46: The tooltip visibility state (isTooltipVisible) isn't
synchronized when isWaitingForOwnerStart flips from false to true; inside the
TournamentStartButton component update the existing useEffect to respond to
changes in isWaitingForOwnerStart by setting setIsTooltipVisible(true) when
isWaitingForOwnerStart becomes true, clear any existing timeout, then start the
timeout that sets setIsTooltipVisible(false) after
PARTICIPANT_TOOLTIP_DURATION_MS; keep the initial
useState(isWaitingForOwnerStart) as-is and ensure the effect returns a cleanup
that clears the timeout.

---

Nitpick comments:
In `@apps/web/src/app/tournament/`[id]/_common/_hooks/useGetTournament.ts:
- Around line 9-11: Refactor the polling in the useGetTournament hook by
guarding refetchInterval with React Query's enabled option: keep
refetchInterval: 30_000 but add enabled: status === 'PENDING' && !isOwner (or
compute a boolean like shouldPoll = status === 'PENDING' && !isOwner and set
enabled: shouldPoll) so polling only runs for non-owners while the tournament is
pending; update any local variables used to derive status/isOwner if needed so
enabled has correct values at hook initialization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5dcc0b0e-7da7-4d12-b03e-f2d2a01b99ba

📥 Commits

Reviewing files that changed from the base of the PR and between 2f1a307 and 60c7a78.

📒 Files selected for processing (17)
  • apps/web/src/app/play/[id]/_apis/postFromPlayLink.ts
  • apps/web/src/app/play/[id]/_components/PlayClient.tsx
  • apps/web/src/app/play/[id]/_types/play.ts
  • apps/web/src/app/tournament/[id]/_common/_hooks/useGetTournament.ts
  • apps/web/src/app/tournament/[id]/_common/_types/tournamentResponse.ts
  • apps/web/src/app/tournament/[id]/create/_apis/getTournament.ts
  • apps/web/src/app/tournament/[id]/create/_apis/postTournamentStart.ts
  • apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-header/TournamentHeader.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentBasketItem.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasket.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-start-button/TournamentStartButton.tsx
  • apps/web/src/app/tournament/[id]/create/_hooks/useGetTournament.ts
  • apps/web/src/app/tournament/[id]/create/_types/tournament.ts
  • apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx
  • apps/web/src/app/tournament/[id]/create/page.tsx
💤 Files with no reviewable changes (5)
  • apps/web/src/app/play/[id]/_types/play.ts
  • apps/web/src/app/play/[id]/_apis/postFromPlayLink.ts
  • apps/web/src/app/tournament/[id]/create/_hooks/useGetTournament.ts
  • apps/web/src/app/tournament/[id]/create/_apis/getTournament.ts
  • apps/web/src/app/tournament/[id]/create/_types/tournament.ts
✅ Files skipped from review due to trivial changes (2)
  • apps/web/src/app/tournament/[id]/create/_apis/postTournamentStart.ts
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentBasketItem.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/web/src/app/play/[id]/_components/PlayClient.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasket.tsx
  • apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx
  • apps/web/src/app/tournament/[id]/_common/_types/tournamentResponse.ts
  • apps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

2 participants