feat: 소셜 토너먼트 전체 (4개 PR 통합 — #142/#156/#159/#163)#167
Conversation
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: 토너먼트 친구 초대 바텀시트 다이얼로그 구현
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (35)
Walkthrough초대 코드 입력부터 게스트 참여, 생성 화면 제어(카운트다운/참여자/시작 확인), 결과 공유(플레이 링크), 플레이 링크 진입, 그룹 결과 조회까지 소셜 토너먼트의 전체 엔드투엔드 플로우를 구현합니다. API 엔드포인트, 응답 타입, 라우트, 클라이언트 로직, UI 컴포넌트, 유틸을 일관되게 추가·변경합니다. Changes소셜 토너먼트 엔드투엔드 플로우
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
|
No description provided. |
There was a problem hiding this comment.
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담기 마감 시 빈 슬롯의 시각적 피드백이 누락되었습니다.
isDepositClosed가true일 때 완전히 빈<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
⛔ Files ignored due to path filters (4)
apps/web/src/app/home/_assets/sad-face.svgis excluded by!**/*.svgapps/web/src/app/tournament/[id]/create/_assets/confirm-exit-basket.svgis excluded by!**/*.svgapps/web/src/app/tournament/[id]/create/_assets/confirm-start-face.svgis excluded by!**/*.svgapps/web/src/assets/icons/fill/timer.svgis excluded by!**/*.svg
📒 Files selected for processing (76)
apps/web/src/apis/client.tsapps/web/src/apis/getNicknameCheck.tsapps/web/src/app/archive/_components/wish-grid/index.tsxapps/web/src/app/home/_components/InviteTournamentButton.tsxapps/web/src/app/home/_components/TournamentList.tsxapps/web/src/app/home/_components/invite-code-dialog/InvalidCodeDialog.tsxapps/web/src/app/home/_components/invite-code-dialog/InviteCodeDialog.tsxapps/web/src/app/home/page.tsxapps/web/src/app/invite/[id]/_components/InviteClient.tsxapps/web/src/app/invite/[id]/page.tsxapps/web/src/app/layout.tsxapps/web/src/app/login/_apis/postGuestLogin.tsapps/web/src/app/login/_components/LoginButtons.tsxapps/web/src/app/login/_components/SocialLoginButton.tsxapps/web/src/app/login/page.tsxapps/web/src/app/play/[id]/_components/PlayClient.tsxapps/web/src/app/play/[id]/page.tsxapps/web/src/app/play/_apis/postFromPlayLink.tsapps/web/src/app/play/_types/play.tsapps/web/src/app/tournament/[id]/_common/_types/tournamentResponse.tsapps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsxapps/web/src/app/tournament/[id]/create/_components/deposit-countdown/DepositCountdown.tsxapps/web/src/app/tournament/[id]/create/_components/invite-friends/InviteFriends.tsxapps/web/src/app/tournament/[id]/create/_components/invite-friends/InviteFriendsDialog.tsxapps/web/src/app/tournament/[id]/create/_components/member-join-confirm-dialog/MemberJoinConfirmDialog.tsxapps/web/src/app/tournament/[id]/create/_components/participant-panel/ParticipantChip.tsxapps/web/src/app/tournament/[id]/create/_components/participant-panel/ParticipantPanel.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-header/ConfirmExitDialog.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-header/TournamentHeader.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-item-basket-status/TournamentItemBasketStatus.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/EmptyBasketSlot.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasket.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-start-button/ConfirmStartDialog.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-start-button/TournamentStartButton.tsxapps/web/src/app/tournament/[id]/create/_components/welcome-join-dialog/WelcomeJoinDialog.tsxapps/web/src/app/tournament/[id]/create/_hooks/useCountdown.tsapps/web/src/app/tournament/[id]/create/_hooks/useGetTournamentItem.tsapps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentItemLink.tsapps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentStart.tsapps/web/src/app/tournament/[id]/create/_types/tournament.tsapps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectHeader.tsxapps/web/src/app/tournament/[id]/create/page.tsxapps/web/src/app/tournament/[id]/match/_hooks/useTournament.tsapps/web/src/app/tournament/[id]/match/page.tsxapps/web/src/app/tournament/[id]/result/_apis/getGroupResult.tsapps/web/src/app/tournament/[id]/result/_apis/postPlayLink.tsapps/web/src/app/tournament/[id]/result/_components/ReceiptDrawMachine.tsxapps/web/src/app/tournament/[id]/result/_components/ReceiptPaper.tsxapps/web/src/app/tournament/[id]/result/_components/ResultClient.tsxapps/web/src/app/tournament/[id]/result/_components/group-result-entry-card/GroupResultEntryCard.tsxapps/web/src/app/tournament/[id]/result/_components/plate-share-dialog/PlateShareDialog.tsxapps/web/src/app/tournament/[id]/result/_hooks/useGetGroupResult.tsapps/web/src/app/tournament/[id]/result/_hooks/usePostPlayLink.tsapps/web/src/app/tournament/[id]/result/_types/groupResult.tsapps/web/src/app/tournament/[id]/result/group/_components/GroupResultClient.tsxapps/web/src/app/tournament/[id]/result/group/page.tsxapps/web/src/app/tournament/[id]/result/page.tsxapps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsxapps/web/src/app/tournament/join/[id]/page.tsxapps/web/src/app/tournament/join/_apis/getInvitePreview.tsapps/web/src/app/tournament/join/_apis/getInvitePreviewByCode.tsapps/web/src/app/tournament/join/_apis/postJoinGuest.tsapps/web/src/app/tournament/join/_consts/randomNickname.tsapps/web/src/app/tournament/join/_hooks/useGetInvitePreview.tsapps/web/src/app/tournament/join/_hooks/usePostJoinGuest.tsapps/web/src/app/tournament/join/_types/join.tsapps/web/src/app/tournament/join/_utils/joinSession.tsapps/web/src/app/tournament/join/_utils/verifyInviteCode.tsapps/web/src/components/user-profile-group/UserProfile.tsxapps/web/src/consts/api.tsapps/web/src/consts/route.tsapps/web/src/hooks/useGetNicknameCheck.tsapps/web/src/types/user.tsapps/web/src/utils/share.tsdocs/social-tournament-api-integration.md
💤 Files with no reviewable changes (1)
- apps/web/src/app/tournament/[id]/create/_components/invite-friends/InviteFriends.tsx
| 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('공유에 실패했어요. 다시 시도해주세요.'); | ||
| }; |
There was a problem hiding this comment.
만료된 초대 링크 공유 가능 여부를 검증하세요.
현재 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.
| const inviteUrl = inviteCode ? buildInviteUrl(tournamentId, inviteCode) : ''; | ||
|
|
||
| const handleToggleExpand = () => setIsExpanded(prev => !prev); | ||
| const handleOpenInvite = () => setIsInviteDialogOpen(true); |
There was a problem hiding this comment.
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'; | |||
There was a problem hiding this comment.
상대 경로 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.
| 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
| const { nicknameCheckData, isNicknameCheckFetching } = useGetNicknameCheck(nickname); | ||
| const { postJoinGuestMutation, isPostJoinGuestPending } = usePostJoinGuest(); |
There was a problem hiding this comment.
닉네임 중복 검사와 제출 값의 정규화 기준을 일치시켜주세요.
현재 중복 검사는 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.
| await queryClient.prefetchQuery({ | ||
| queryKey: ['invitePreview', tournamentId], | ||
| queryFn: () => getInvitePreview(tournamentId), |
There was a problem hiding this comment.
초대 코드 기반 프리패치 검증이 누락되었습니다.
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.
| ## 0. 공통 응답 래퍼 | ||
|
|
||
| 모든 응답은 `ApiResponseBody` 로 래핑됨. | ||
|
|
||
| ```json | ||
| { | ||
| "data": { | ||
| /* 실제 응답 */ | ||
| }, | ||
| "detail": "정상적으로 처리되었습니다.", | ||
| "pageResponse": { "nextCursor": null, "hasNext": false } | ||
| } | ||
| ``` | ||
|
|
||
| - 클라이언트는 axios 인터셉터에서 `.data.data` 를 풀어 사용 | ||
| - 실패 시 `data: null`, `detail`/`code` 에 사유 | ||
|
|
There was a problem hiding this comment.
공통 응답 래퍼 정의가 팀 API 계약과 충돌합니다.
여기서는 ApiResponseBody를 data/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
| ### 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) |
There was a problem hiding this comment.
상세 섹션 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
left a comment
There was a problem hiding this comment.
진짜 고생 많았다~~ 근데 지금은 모달에서 초대 코드를 입력하도록 되어 있는데 원래는 초대 버튼을 클릭하면 초대 코드 입력 페이지로 이동하는 방식 아니었어? 바뀐건강?
| } catch (error) { | ||
| // 400 (코드 불일치) / 409 (만료) — 잘못된 링크로 통합 | ||
| if (isAxiosError(error)) { | ||
| setState('invalid'); | ||
| return; | ||
| } | ||
| setState('invalid'); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Axios 에러든 아니든 setState('invalid')로 동일하게 처리하는데 분기로 처리한 이유가 있어?
There was a problem hiding this comment.
[반영] 음 에러처리 다르게 하고 싶어서 하긴 했는데 별 의미가 없긴 하네
분기 자체가 불필요한거 같으니 정리할게
There was a problem hiding this comment.
api랑 type 폴더를 play/[id]/_apis/postFromPlayLink.ts, play/[id]/_types/play.ts로 내려야 할 것 같아~
| @@ -29,7 +43,13 @@ export type PostTournamentItemLinkResponseT = { | |||
| tournamentItemId: number; | |||
| }; | |||
There was a problem hiding this comment.
p2) _common/_types/tournamentResponse.ts에 이미 존재하는 것 같은데 정리가 필요해보여! (이 타입 말고도 중복되어 있는 게 좀 있는 것 같은데 수정할때 같이 부탁행) create/ 전용 타입만 여기에 두고 중복 타입은_common/으로 옮기는 거 어떨까?
There was a problem hiding this comment.
+create/_hooks/useGetTournament 이 훅도 삭제해줘!(refetchInterval 사용안함) common에 중복으로 들어가있다
There was a problem hiding this comment.
p2) useGetTournament queryKey는 ['tournament', number]인데 여기서는 string으로 넘기고 있네? 내가 저번에 빠트린 것 같아서 Number(tournamentId)로 수정 같이 해줘!
| isParticipant = false, | ||
| }: TournamentStartButtonProps) { | ||
| const [isConfirmOpen, setIsConfirmOpen] = useState(false); | ||
| const { tournamentData } = useGetTournament(Number(tournamentId)); |
There was a problem hiding this comment.
p3) 여기도 TournamentHeader랑 똑같이 useGetTournament 다시 호출되고 있다!
| import { useGetTournament } from '../../../_common/_hooks/useGetTournament'; | ||
| import { useGetGroupResult } from '../../_hooks/useGetGroupResult'; |
There was a problem hiding this comment.
p4) 전체적으로 alias 적용해줘~ 나중에 파일 이동하거나 할때 오류도 안나고 좋더라구
There was a problem hiding this comment.
오키~! 이거는 담주에 한꺼번에 정리할게!
| 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}`; | ||
| }; |
There was a problem hiding this comment.
p3) ReceiptPaper.tsx에도 동일 함수가 있네 result/_utils로 옮기는 건 어때?
| const { id: _tournamentId } = useParams<{ id: string }>(); | ||
| const tournamentId = parseIdParam(_tournamentId); |
There was a problem hiding this comment.
p3) 사용하는 변수니까 _ 삭제해줘! (내가 수정하려고 했다가 까먹어서..
| const router = useRouter(); | ||
| const [nickname, setNickname] = useState(DEFAULT_RANDOM_NICKNAME.nickname); | ||
|
|
||
| if (tournamentId === null) notFound(); |
There was a problem hiding this comment.
p2) page.tsx에서 parseIdParam으로 null 체크 후 notFound()를 호출하고 있는데 여기서도 이중으로 검사하는 이유가 있어? 타입도 null 제거하고 number로 좁히면 내부 null 체크도 제거 할 수 있을듯!
| 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]!; |
There was a problem hiding this comment.
랜덤 닉네임 api는 없는건가?? 그럼 항상 지친 루피가 기본값인데 이미 이 닉네임을 써버리면 다음 사람은 항상 닉네임을 변경해야 하니까 약간 귀찮을수도..
There was a problem hiding this comment.
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
📒 Files selected for processing (17)
apps/web/src/app/play/[id]/_apis/postFromPlayLink.tsapps/web/src/app/play/[id]/_components/PlayClient.tsxapps/web/src/app/play/[id]/_types/play.tsapps/web/src/app/tournament/[id]/_common/_hooks/useGetTournament.tsapps/web/src/app/tournament/[id]/_common/_types/tournamentResponse.tsapps/web/src/app/tournament/[id]/create/_apis/getTournament.tsapps/web/src/app/tournament/[id]/create/_apis/postTournamentStart.tsapps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-header/TournamentHeader.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentBasketItem.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasket.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-item-basket/TournamentItemBasketCarousel.tsxapps/web/src/app/tournament/[id]/create/_components/tournament-start-button/TournamentStartButton.tsxapps/web/src/app/tournament/[id]/create/_hooks/useGetTournament.tsapps/web/src/app/tournament/[id]/create/_types/tournament.tsapps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsxapps/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
작업 요약
/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. 친구 초대 / 참여 흐름
GET /tournaments/by-invite-code호출 → 성공 시/tournament/join/{id}?code=XXX라우팅, 400/409 시InvalidCodeDialog안내/invite/[id]?code=XXX— 카톡 등으로 받은 링크 클릭 시 코드 자동 검증 →/tournament/join/{id}?code=XXX라우팅 (코드 query 유지)POST /tournaments/{id}/join/guestPOST /tournaments/{id}/join+ 환영 바텀시트[A-Z]{3}\d{3}강화, 자동 대문자 변환2. 토너먼트 진행 — ROOT/CLONE 구조 대응 (BE PR #456, #467)
POST /tournaments/{id}/start응답에tournamentId추가ownerStarted)ownerStarted=true→ 본인 시작 버튼 활성화 → 클릭 시 본인 CLONE 생성 후 매치 진입GetTournamentMemberWaitingResponseT신규 — IN_PROGRESS 상태에서도 참여자가 ROOT 의 pending 정보 (items / participants) 받을 수 있음3. 친구 토너먼트 결과보기 (group-result)
/tournament/[id]/result/group라우트 — 영수증 형태 (Kode Mono) + 1st Place 강조 카드 + Others 축약 라인 + chosenBy 표시isRoot일 때만 노출 (주최자 + 친구 초대 멤버)GET /tournaments/{id}/group-result호출useQuery+retry: false로 에러 분기4. 플레이 링크 (
/play/[id])POST /tournaments/{sourceId}/from-play-link호출/create(바구니 미리보기 + 시작 버튼)/match/result(재진입 시 본인 결과)5. 영수증 / 플레이 링크 공유
PlateShareDialog— 시안 일치 (스톱워치 / 체크리스트 제거, 단일 안내 텍스트)canSharePlayLink분기 —isRoot && isOwner일 때만 "플레이 링크 공유" 액션 노출initialPlayLinkExpiresAt있으면 mutation 생략하고 바로 share (만료 갱신 방지)6. API 연동
GET /tournaments/by-invite-codeGET /tournaments/{id}/invite-previewPOST /tournaments/{id}/joinPOST /tournaments/{id}/join/guestPOST /tournaments/{id}/startPOST /tournaments/{id}/play-linkPOST /tournaments/{sourceId}/from-play-linkGET /tournaments/{id}/group-resultGET /users/nickname/check7. 응답 타입 확장
isRoot: boolean(ROOT/CLONE 구분)pending:ownerStarted: booleaninviteCode,inviteExpiresAtnullable (ownerStarted=true시 null)completed:hasGroupResult: booleanplayLinkExpiresAt?: stringGetTournamentMemberWaitingResponseT신규8. mock / dead code 제거
mocks/deposit.ts,mocks/tournamentPreview.ts,mocks/participants.ts삭제MOCK_DEPOSIT_DURATION_MS,MOCK_PARTICIPANTS,withMockParticipants제거/tournament/join+InviteClient.tsx삭제verifyInviteCodemock 정답 코드 제거 → 형식 검증 함수로 축소9. 그 외 정리
홈으로 가기)body에suppressHydrationWarning(Grammarly 등 확장 대응)null→{}통일 (415 방지)WishGridkey prop 누락 fix후속 작업 (별도 이슈)
연관 이슈
closes #139
closes #140
closes #157
closes #158
Summary by CodeRabbit
릴리즈 노트
새로운 기능
문서