Skip to content

refactor: invite 진입 분기 로직을 클라이언트에서 RSC로 이관 - #412

Merged
ychany merged 7 commits into
devfrom
refactor/390-invite-rsc
Aug 3, 2026
Merged

refactor: invite 진입 분기 로직을 클라이언트에서 RSC로 이관#412
ychany merged 7 commits into
devfrom
refactor/390-invite-rsc

Conversation

@ychany

@ychany ychany commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

작업 요약

  • /invite 진입 분기 로직을 클라이언트에서 RSC redirect 로 이관합니다

작업 세부 내용

/invite/[id] 의 초대 코드 검증 → 참여 여부 판별 → 목적지 결정을 InviteClient 의 useEffect 에서 RSC(page.tsx)로 올렸습니다. (#390 작업 상세 4개 항목 전부 반영)

invite/[id]/page.tsx — 판단은 서버가

  • preview 조회 → 코드 불일치/실패 시 InviteInvalid 렌더, joined 면 토너먼트로, 미참여면 join 페이지로 redirect()
  • redirect() 는 NEXT_REDIRECT throw 방식이라 try 밖에서 호출 (catch 가 삼키지 않도록 preview 조회만 감쌈)
  • 409(만료)는 showExpiredDialog prop 으로 기존 만료 다이얼로그 UX 유지

InviteClient → InviteInvalid

  • "초대 링크가 유효하지 않아요" UI + 에러 다이얼로그만 남기고 useEffect·hasRunRef·getMe·postJoin 제거 (-157줄)

getInvitePreviewByCode

  • environmentManager.isServer() 분기 추가 — RSC 에서 serverApi(쿠키 포함)로 호출 (getInvitePreview.ts 패턴)

회원 자동 join → join 페이지로 이관

  • JoinPreviewClient 가 참여 방식을 소유: 회원 = 자동 join(스피너) / 게스트 = 닉네임 입력
  • 이미 참여한 회원의 URL 직접 진입은 postJoin 없이 바로 이동 (joined 가드로 409 방지)
  • effect 트리거 mutation 은 ref 가드 + useCallback 으로 재호출 방어

효과

  • 스피너 화면 + 브라우저↔API 왕복 2~3회 → 서버 내부 왕복 후 리다이렉트 한 번
  • getMe 중복 호출 제거 (join/create 페이지 prefetch 재사용)

테스트

  • 전체 e2e 12/12 통과 (기존 스펙 회귀 없음)
  • 수동 검증 완료
    • 게스트 초대 링크 진입 → 스피너 없이 join 페이지 직행 → 닉네임 입력 후 참여
    • 참여 후 동일 링크 재진입 → join 거치지 않고 토너먼트 바로 진입

연관 이슈

closes #390

Summary by CodeRabbit

  • 새 기능

    • 초대 링크의 유효성·만료 상태를 확인해 상황에 맞는 화면을 제공합니다.
    • 유효하지 않거나 만료된 링크에 안내 메시지와 홈 이동 기능을 표시합니다.
    • 이미 참여한 사용자는 해당 토너먼트로 바로 이동합니다.
    • 로그인한 사용자는 닉네임 입력 없이 자동으로 참여할 수 있습니다.
    • 비로그인 사용자의 닉네임 입력 및 참여 흐름을 유지합니다.
  • 버그 수정

    • 초대 코드 오류와 참여 요청 실패 시 명확한 안내를 제공합니다.
    • 자동 참여 중복 요청을 방지하고, 실패 시 재시도할 수 있습니다.
    • 초대 정보 확인 결과에 따라 잘못된 토너먼트로의 접근을 차단합니다.

@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
piki Ready Ready Preview Aug 3, 2026 8:21am

@github-actions github-actions Bot added refactor Extra attention is needed WEB labels Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

@github-actions
github-actions Bot requested a review from iOdiO89 August 1, 2026 10:12
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 668f9675-e81b-481c-8781-3d882c35e5d4

📥 Commits

Reviewing files that changed from the base of the PR and between 2f429fd and 2a0181e.

📒 Files selected for processing (7)
  • apps/web/src/apis/getInvitePreviewByCode.ts
  • apps/web/src/app/home/_components/InviteTournamentDialog.tsx
  • apps/web/src/app/invite/[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/_types/join.ts
  • apps/web/src/types/tournament.ts
📝 Walkthrough

Walkthrough

초대 페이지가 클라이언트 검증에서 서버 미리보기와 리디렉션 방식으로 변경되었습니다. 무효·만료 링크 UI가 분리되었습니다. 회원 사용자는 참여 페이지에서 자동 참여하며, 기존 참여자는 토너먼트 화면으로 이동합니다.

Changes

초대 진입 및 참여 흐름

Layer / File(s) Summary
서버 초대 검증 및 라우팅
apps/web/src/app/invite/[id]/page.tsx, apps/web/src/app/tournament/join/_apis/getInvitePreviewByCode.ts, apps/web/src/app/invite/[id]/_components/InviteInvalid.tsx
서버 환경에서는 serverApi로 초대 미리보기를 조회합니다. page.tsx는 오류, 토너먼트 불일치, 참여 여부에 따라 화면을 렌더링하거나 리디렉션합니다. InviteInvalid는 무효 및 만료 링크 UI를 제공합니다. 기존 InviteClient는 제거되었습니다.
참여 페이지의 회원 자동 참여
apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx
회원 사용자는 닉네임 입력 없이 자동 참여를 실행합니다. 이미 참여한 사용자는 토너먼트 생성 화면으로 이동합니다. 중복 실행을 방지하고, 실패 시 재시도 UI를 표시합니다.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant InvitePage as 초대 페이지
  participant PreviewApi as getInvitePreviewByCode
  participant ServerApi as serverApi
  participant JoinPreview as JoinPreviewClient
  participant TournamentApi as 참여 mutation

  InvitePage->>PreviewApi: 초대 코드 미리보기 요청
  PreviewApi->>ServerApi: 서버 API 호출
  ServerApi-->>PreviewApi: 초대 미리보기 반환
  PreviewApi-->>InvitePage: 토너먼트 및 참여 상태 전달
  InvitePage->>JoinPreview: 미참여 사용자를 참여 페이지로 리디렉션
  JoinPreview->>TournamentApi: 회원 자동 참여 요청
  TournamentApi-->>JoinPreview: 참여 성공 또는 오류 반환
Loading

Possibly related PRs

Suggested reviewers: iodio89

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 초대 진입 분기 로직을 클라이언트에서 RSC로 이전한 핵심 변경을 정확하고 간결하게 설명합니다.
Linked Issues check ✅ Passed [#390]의 서버 검증·리다이렉트·자동 참여 이전 요구 사항을 변경 사항이 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 초대 분기 로직 이전과 참여 흐름 조정이라는 [#390] 범위에 포함됩니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/390-invite-rsc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/web/src/app/invite/[id]/_components/InviteInvalid.tsx (1)

38-43: 📐 Maintainability & Code Quality | 🔵 Trivial

TODO: 단일 오류 타입 처리.

주석은 409가 만료·이미 참여·이미 시작 등 여러 경우를 포함하지만 서버가 세부 오류 코드를 내려주지 않아 LINK_EXPIRED 하나로 처리한다고 명시합니다. 백엔드에서 세부 오류 코드를 제공하도록 후속 작업을 진행할 계획이 있다면 알려주세요. 필요하면 타입별 분기 처리 초안 작성을 도와드리겠습니다.

🤖 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]/_components/InviteInvalid.tsx around lines 38 -
43, 현재 서버가 세부 오류 코드를 제공하지 않으므로 InviteInvalid의 TournamentErrorDialog는
LINK_EXPIRED 단일 타입 처리를 유지하세요. 백엔드에서 만료·참여 중·시작됨을 구분하는 오류 코드를 제공할 계획이 있다면 해당 후속
작업을 확인한 뒤 타입별 분기를 추가하세요.
🤖 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/invite/`[id]/page.tsx:
- Line 46: Update the redirect URL construction to encode the code query value
before interpolation, using encodeURIComponent(code) in the redirect call while
preserving the existing tournament join route and parameter structure.
- Around line 30-46: Update the invite flow around getInvitePreviewByCode and
the redirect to ROUTES.TOURNAMENT_JOIN_BY_LINK so the fetched preview is stored
through the shared query cache, such as queryClient.setQueryData(), under the
same query key and response shape consumed by JoinPreviewClient. Preserve the
existing invalid-invite, tournament mismatch, and joined-user behavior while
allowing the join page to reuse tournamentName, itemCount, participantCount, and
joined without requesting /tournaments/${id}/invite-preview again.

In `@apps/web/src/app/tournament/join/`[id]/_components/JoinPreviewClient.tsx:
- Around line 97-136: Track non-409 failures from joinTournament in the member
auto-join flow using component state, and render a retry or exit guidance view
instead of the perpetual spinner when that state is set. Update the
joinTournament error handling and the isMember rendering branch around
hasAutoJoinRunRef so failed attempts expose an actionable recovery path while
preserving the existing success and already-joined navigation behavior.

---

Nitpick comments:
In `@apps/web/src/app/invite/`[id]/_components/InviteInvalid.tsx:
- Around line 38-43: 현재 서버가 세부 오류 코드를 제공하지 않으므로 InviteInvalid의
TournamentErrorDialog는 LINK_EXPIRED 단일 타입 처리를 유지하세요. 백엔드에서 만료·참여 중·시작됨을 구분하는 오류
코드를 제공할 계획이 있다면 해당 후속 작업을 확인한 뒤 타입별 분기를 추가하세요.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 345fe46d-f594-489b-bcd9-ec0236cb60df

📥 Commits

Reviewing files that changed from the base of the PR and between 7570cc8 and 0885d19.

📒 Files selected for processing (5)
  • apps/web/src/app/invite/[id]/_components/InviteClient.tsx
  • apps/web/src/app/invite/[id]/_components/InviteInvalid.tsx
  • apps/web/src/app/invite/[id]/page.tsx
  • apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx
  • apps/web/src/app/tournament/join/_apis/getInvitePreviewByCode.ts
💤 Files with no reviewable changes (1)
  • apps/web/src/app/invite/[id]/_components/InviteClient.tsx

Comment thread apps/web/src/app/invite/[id]/page.tsx Outdated
Comment on lines +30 to +46
let preview;
try {
preview = await getInvitePreviewByCode(code);
} catch (error) {
/** 409(만료·비활성 초대)는 만료 다이얼로그 노출, 그 외(400 코드 불일치 등)는 안내 화면만 */
const isExpired = isAxiosError<ApiErrorResponseT>(error) && error.response?.status === 409;
return <InviteInvalid showExpiredDialog={isExpired} />;
}

/** 코드의 토너먼트가 URL path 와 다르면 잘못된 링크 */
if (preview.tournamentId !== tournamentId) return <InviteInvalid />;

/** 이미 참여한 유저(회원·게스트 공통) → join 건너뛰고 토너먼트로 바로 진입 */
if (preview.joined) redirect(ROUTES.TOURNAMENT_CREATE(tournamentId));

/** 미참여 → 참여 방식(회원 자동 / 게스트 닉네임 입력)은 join 페이지가 소유 */
redirect(`${ROUTES.TOURNAMENT_JOIN_BY_LINK(tournamentId)}?code=${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.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm whether the invite preview data fetched by code and the join-page preview fetched by tournamentId hit the same backend resource.
set -euo pipefail

fd -e ts -e tsx useGetInvitePreview apps/web/src | xargs -I{} sh -c 'echo "--- {} ---"; cat -n {}'
rg -n 'TOURNAMENT_INVITE_PREVIEW' apps/web/src -C2

Repository: TeamPiKi/client

Length of output: 3341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- locate relevant files ---"
fd -e ts -e tsx 'page\.tsx|getInvitePreview(ByCode)?|JoinPreviewClient\.tsx|useGetInvitePreview|api\.ts' apps/web/src | sort | sed -n '1,120p'

echo "--- invite page ---"
cat -n apps/web/src/app/invite/[id]/page.tsx

echo "--- join client preview usage ---"
cat -n apps/web/src/app/tournament/join/_components/JoinPreviewClient.tsx
cat -n apps/web/src/app/tournament/join/_apis/getInvitePreview.ts
cat -n apps/web/src/app/tournament/join/_apis/getInvitePreviewByCode.ts

echo "--- route references ---"
rg -n 'InviteInvalid|showExpiredDialog|TOURNAMENT_JOIN_BY_LINK|TournamentJoin|JoinPreviewClient|useGetInvite|getInvitePreview' apps/web/src/app -C 3

echo "--- constants/types mentions ---"
grep -Rn 'GetInvitePreviewResponseT|invite-preview|invite-preview-by-code|TOURNAMENT_INVITE_PREVIEW' apps/web/src -C 2

Repository: TeamPiKi/client

Length of output: 3657


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- join related files ---"
for f in \
  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/_hooks/useGetInvitePreview.ts \
  apps/web/src/consts/api.ts \
  apps/web/src/types/api.ts; do
  if [ -f "$f" ]; then
    echo "### $f"
    cat -n "$f"
  fi
done

echo "--- preview type and endpoint references ---"
rg -n 'GetInvitePreview|toInvitePreview|TOURNAMENT_INVITE_PREVIEW|INVITE_PREVIEW|preview' apps/web/src -C 3

echo "--- API endpoint definitions/usages outside web src if tracked in repo ---"
git ls-files | rg '\.(ts|tsx)$' | xargs rg -n 'TOURNAMENT_INVITE_PREVIEW|by-invite-code|invite-preview|invitePreview' -C 2 2>/dev/null || true

Repository: TeamPiKi/client

Length of output: 30509


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- invite client component ---"
cat -n apps/web/src/app/invite/[id]/_components/InviteInvalid.tsx

echo "--- join client component relevant sections ---"
cat -n apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx | sed -n '1,180p'

echo "--- join types ---"
cat -n apps/web/src/app/tournament/join/_types/join.ts

Repository: TeamPiKi/client

Length of output: 9725


초대 미리보기 조회 결과를 참여 페이지까지 전달하세요.

apps/web/src/app/invite/[id]/page.tsx에서 getInvitePreviewByCode(code)tournamentName, itemCount, participantCount, joined를 취득한 뒤 리다이렉트되면 이 값은 버려집니다. 이후 tournament/join/[id]_components/JoinPreviewClient.tsx가 같은 응답 구조의 미리보기를 /tournaments/${id}/invite-preview로 다시 요청합니다. queryClient.setQueryData() 같은 공유 방식에 미리보기 값을 녹여 중복 서버 요청과 브라우저 API 왕복을 줄이세요.

🤖 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 around lines 30 - 46, Update the
invite flow around getInvitePreviewByCode and the redirect to
ROUTES.TOURNAMENT_JOIN_BY_LINK so the fetched preview is stored through the
shared query cache, such as queryClient.setQueryData(), under the same query key
and response shape consumed by JoinPreviewClient. Preserve the existing
invalid-invite, tournament mismatch, and joined-user behavior while allowing the
join page to reuse tournamentName, itemCount, participantCount, and joined
without requesting /tournaments/${id}/invite-preview again.

Comment thread apps/web/src/app/invite/[id]/page.tsx Outdated
Comment thread apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx Outdated
Comment thread apps/web/src/apis/getInvitePreviewByCode.ts
Comment thread apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx Outdated
Comment thread apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx Outdated
ychany added 3 commits August 3, 2026 17:15
두 개 이상 top-level 라우트(home, invite)에서 쓰이므로 라우트 전용 _apis 가 아닌
공용 src/apis 로 옮긴다. 응답 타입도 src/types/tournament.ts 로 함께 이동.
- 참여 성공 시 router.push -> replace 로 변경해 뒤로가기 재참여(409) 차단
- isAutoJoinFailed boolean 을 joining/retryable/blocked 상태로 통합
- 409 는 재시도가 무의미하므로 홈으로 가기 종료 화면 노출
@ychany
ychany merged commit c3b62c2 into dev Aug 3, 2026
6 of 7 checks passed
@ychany
ychany deleted the refactor/390-invite-rsc branch August 3, 2026 08:20
iOdiO89 pushed a commit that referenced this pull request Aug 4, 2026
* refactor: invite 진입 분기를 클라이언트에서 RSC redirect 로 이관

* refactor: 회원 자동 join 을 invite 에서 join 페이지로 이관

* fix: 리다이렉트 code 인코딩 및 자동 참여 실패 시 재시도 UI 추가

* style: prettier 포맷 적용

* refactor: getInvitePreviewByCode 를 src/apis 로 이동

두 개 이상 top-level 라우트(home, invite)에서 쓰이므로 라우트 전용 _apis 가 아닌
공용 src/apis 로 옮긴다. 응답 타입도 src/types/tournament.ts 로 함께 이동.

* fix: 회원 자동 참여 409 시 무한 스피너 갇힘 해소

- 참여 성공 시 router.push -> replace 로 변경해 뒤로가기 재참여(409) 차단
- isAutoJoinFailed boolean 을 joining/retryable/blocked 상태로 통합
- 409 는 재시도가 무의미하므로 홈으로 가기 종료 화면 노출
iOdiO89 added a commit that referenced this pull request Aug 4, 2026
충돌 해결:
- InviteClient.tsx: dev 의 삭제 수용 (#412 로 RSC 이관)
- JoinPreviewClient.tsx: dev 의 회원 자동 참여 구조 + 에러 처리는 usePostJoin 훅 레벨로
  (mutate 레벨 onError 는 전역 fallback 과 토스트가 겹치고 문구도 하드코딩이었음)
- usePostWishLink / usePostTournamentItemLink: 카탈로그 기반 문구 + dev 의 showErrorToast 옵션 결합
- error.tsx: dev 의 시안 디자인 유지, 문구는 서버 code 있으면 카탈로그로 대체
- types/tournament.ts: 양쪽 타입 추가분 합침
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Extra attention is needed WEB

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: invite 진입 분기 로직을 클라이언트에서 RSC로 이관

2 participants