refactor: 초대 진입 경로 통합 및 합류 에러 안내를 JoinErrorDialog 로 정리 - #436
Conversation
- LINK-001~003 의미가 서버에서 한 칸씩 당겨져 잘못된 문구가 노출되던 문제 수정 (LINK-004 결번) - 접두사 변경 반영: IMG_PROXY→PROXY, IMG_STORAGE→STORAGE, IMG_UPLOAD→UPLOAD, PRODUCT_IMAGE→PRODUCTIMAGE - 서버에서 삭제된 코드 제거: ITEM-001·002, ANN_IMAGE-001~005, IMG_STORAGE-002·003 - 신규 코드 추가: TOURNAMENT-033, WISH-009
usePostJoin 에 훅 레벨 onError 가 없어 전역 MutationCache 의 4xx fallback 과 JoinPreviewClient 의 mutate 레벨 onError 가 함께 토스트를 띄우고 있었다. 409(참여 불가 상태)는 onConflict 콜백으로 화면에 위임하고, 나머지는 getApiErrorMessage 문구로 훅에서 단독 처리한다.
- consts/errorCode.ts: ERROR_MESSAGE_MAP + fallback 문구 상수 - types/error.ts: ErrorCodeT, ApiErrorCodeT - utils/getErrorMessageByCode.ts: 순수 조회 헬퍼 - web 의 getApiErrorMessage 는 axios 파싱만 담당하고 문구 결정은 core 에 위임
카탈로그 94개 코드에 {도메인}_{의미} 이름을 부여해 분기에서 문자열 리터럴 대신 쓴다.
이름은 api-docs 원문 설명에서 따왔다.
- satisfies 로 서버가 코드를 삭제하면 컴파일 에러
- 완전성 검증 타입으로 카탈로그에만 있고 이름이 없는 코드도 컴파일 에러
500(COMMON-SERVER-ERROR)과 502(COMMON-RETRYABLE) 문구가 같아 사유가 구분되지 않았다. - COMMON-SERVER-ERROR 문구를 서버 사전에 맞춰 분리 - SERVER_ERROR_MESSAGE 는 code 없는 네트워크 오류용으로 별도 문구 - error.tsx 하드코딩 문구를 카탈로그 조회로 교체
개별 onError 가 일부 status 만 분기해 나머지 4xx 는 전역 fallback 도 양보돼 조용히 실패했다. - 토스트를 status 분기 밖으로 빼 4xx 전부 안내 - 401·5xx 는 전역(인터셉터·안전망)에 위임 - 링크 등록 403(게스트)도 이미지 등록과 동일하게 로그인 유도 - 5xx 를 다시 throw 하던 dead code 제거
- 토스트를 status 분기 밖으로 빼 4xx 전부 안내 (아이템 수정 400 등) - 401·5xx 는 전역에 위임 - 링크 등록도 403/404/409 에서 이미지 등록과 동일하게 이탈 - 5xx 를 다시 throw 하던 dead code 제거
훅 레벨 밖(mutate 레벨 onError·mutateAsync catch)에서 토스트하면 전역 fallback 이 양보하지 않아 두 번 뜬다. - 매치 기록·플레이 링크 문구를 훅 레벨 onError 로 이동 - 개별 onError 가 5xx 까지 토스트하던 훅에 401·5xx 가드 추가 - 초대 코드 다이얼로그가 서버 오류에도 '유효하지 않은 코드' 로 안내하던 문제 수정
- 프로필 수정·탈퇴 onError 에 401·5xx 가드 추가 (전역과 중복) - 소셜 로그인 실패가 5xx 면 액션 쿼리 없이 이동해 토스트 중복 방지 - OAuth URL 조회 실패 문구를 카탈로그(getApiErrorMessage)로 일원화
- 토스트를 status 분기 밖에 두는 이유와 예시 코드 - 5xx 도 code 로 문구가 갈린다는 점 명시
토큰은 유효하지만 쓸 수 없는 세션이라 토스트만으로는 빠져나갈 방법이 없었다. - 인터셉터에서 409 + USER-003 감지 시 쿠키·브릿지 정리 후 로그인 리다이렉트 - SSR 은 인터셉터가 없어 layout 가드에서 동일 처리 - 로그인 화면은 action 쿼리로 사유 안내
mutate 레벨 onError 는 전역 fallback 을 양보시키지 못해 4xx 에 토스트가 두 번 떴다.
- 해소된 항목 정리, 남은 미대응만 요약에 유지 - 전역 동작에 USER-003 세션 정리·문구 일원화 반영 - TOURNAMENT-005 를 IN_PROGRESS/COMPLETED 두 코드로 분리 요청 기록
인덱스 접근의 암묵적 undefined 대신 null 을 명시적으로 반환한다. 호출부는 모두 ?? fallback 을 쓰고 있어 동작은 동일.
서버가 detail 을 더 이상 내려주지 않으므로 응답 타입을 { data, code } 로 맞추고
문구는 @piki/core 카탈로그(code → 문구)에서 가져온다.
- 매핑 실패 시 fallback 은 웹 getApiErrorMessage 와 동일 (5xx: SERVER / 4xx: DEFAULT)
- Sentry 수집 메시지도 detail 대신 code 기준
"이 에러를 누가 처리하는가"가 개별 훅 17곳에 복사되어 있어, 전역이 새 케이스를 가져갈 때마다 누락이 생기는 구조였다. utils/apiError.ts 한 곳으로 모은다. - 개별 onError 의 401·5xx 가드를 isGlobalNetError 한 줄로 대체 - 전역 안전망에 탈퇴 계정(409 USER-003) 스킵 추가 — 인터셉터 리다이렉트와 토스트 중복 제거 - usePostJoin 은 409 콜백 미전달 시 generic 토스트로 fallback (무피드백 방지) - usePatchWish 의 409 는 USER-003 전용이라 replace 분기에서 제외
리다이렉트 과정에서 OAUTH-* code 가 유실돼 항상 generic 문구가 노출되고 있었다. getLoginPath 에 errorCode 를 실어 로그인 페이지가 카탈로그 문구를 띄우게 한다. - 세션 만료·네이티브 로그인 실패 문구도 하드코딩 대신 카탈로그 상수 사용
react-query 밖 호출이라 catch 전체를 만료·무효 안내로 흡수하고 있었다. 5xx·네트워크는 링크 문제가 아니므로 재시도 가능한 오류 화면으로 나눈다.
200(available: false) 경로지만 에러 응답(USER-004)과 같은 상황이라 문구가 갈리면 안 된다.
- CLAUDE.md: 응답 래퍼를 { data, code } 로, 문구 원천을 detail → code 카탈로그로 갱신
- error-handling-policy.md: 개별 onError 예시·체크리스트를 isGlobalNetError 기준으로 교체, 분류 유틸 표 추가
- api-status-audit.md: 대응 완료된 항목(useGetNotifications, PATCH wishlists 409) 반영
충돌 해결: - InviteClient.tsx: dev 의 삭제 수용 (#412 로 RSC 이관) - JoinPreviewClient.tsx: dev 의 회원 자동 참여 구조 + 에러 처리는 usePostJoin 훅 레벨로 (mutate 레벨 onError 는 전역 fallback 과 토스트가 겹치고 문구도 하드코딩이었음) - usePostWishLink / usePostTournamentItemLink: 카탈로그 기반 문구 + dev 의 showErrorToast 옵션 결합 - error.tsx: dev 의 시안 디자인 유지, 문구는 서버 code 있으면 카탈로그로 대체 - types/tournament.ts: 양쪽 타입 추가분 합침
RSC 이관 후에도 catch 전체가 '유효하지 않은 링크' 안내로 흡수되고 있었다. 5xx·네트워크는 rethrow 해 app/error.tsx 의 재시도 UI 를 쓴다.
mutate 레벨 onError 에 isGlobalNetError 가드 추가 — 5xx·네트워크는 전역 토스트가 단독 안내하고, 인라인 헬퍼텍스트는 4xx(URL 검증류)만 표시한다. 정책 문서에는 react-query 밖 수동 재조회 토스트(useTournament) 예외를 명시.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAPI 응답을 ChangesAPI 오류 계약 및 공통 처리
초대 참여 흐름
문서 및 보조 변경
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
⚔️ Resolve merge conflicts 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
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]/match/_hooks/useTournament.ts (1)
177-180: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win선택 락 복구 없이 일반 라운드 실패 후 재대결이 가능하도록 변경하세요.
일반 라운드
onError는syncWithServer()나setRemainingItems(...)만 호출합니다.VsSection은selectionEpoch를 콘스키드(key)로만 사용하는 반면, 이 분기에서는currentMatch가 바뀌지 않고 락 내부 상태 풀림도 없습니다. 동기화/복구 뒤에unlockSelection()을 호출해 현재 매치를 다시 선택 가능한 상태로 만드세요.🤖 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]/match/_hooks/useTournament.ts around lines 177 - 180, 일반 라운드의 onError 처리에서 syncWithServer() 또는 setRemainingItems(...)로 복구한 뒤 unlockSelection()을 호출하세요. currentMatch가 변경되지 않는 경우에도 선택 잠금이 해제되어 현재 매치를 다시 대결할 수 있도록 하며, 관련 로직은 useTournament의 해당 onError 분기만 수정하세요.
🧹 Nitpick comments (4)
docs/spec/api-status-audit.md (1)
225-227: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
InviteClient참조를 현재 컴포넌트 이름과 맞춰 두세요.공식 경로에서는
JoinPreviewClient를 사용하고InviteClient가 보이지 않습니다. Line 225와 409는JoinPreviewClient기준 문서로 갱신하십시오.[P3]
🤖 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/spec/api-status-audit.md` around lines 225 - 227, Update the API status audit references from InviteClient to JoinPreviewClient in the affected 400/404 and 409 entries, preserving the existing status behavior and wording.Source: Learnings
apps/web/src/app/play/[id]/_components/PlayClient.tsx (1)
18-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win프로젝트 모듈 import에 절대 경로를 사용하세요.
Line 18의
postFromPlayLink는PlayClient.tsx와 다른_apis디렉터리에 있습니다.@/app/play/[id]/_apis/postFromPlayLink로 변경하세요.As per coding guidelines, 프로젝트 모듈은
@/*절대 경로를 사용해야 합니다.수정 예시
-import { postFromPlayLink } from '../_apis/postFromPlayLink'; +import { postFromPlayLink } from '`@/app/play/`[id]/_apis/postFromPlayLink';🤖 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/play/`[id]/_components/PlayClient.tsx at line 18, Update the postFromPlayLink import in PlayClient.tsx to use the project’s `@/`* absolute alias, targeting `@/app/play/`[id]/_apis/postFromPlayLink instead of the relative path.Source: Coding guidelines
apps/web/src/app/tournament/[id]/result/_hooks/usePostPlayLink.ts (1)
7-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win상위 디렉터리 import를 절대 경로로 변경하세요.
../_apis/postPlayLink는 같은 디렉터리의 파일이 아닙니다. 프로젝트 모듈에는@/*절대 import를 사용하세요.코딩 가이드라인에 따라 상대 import는 같은 디렉터리 파일에만 사용합니다.
[ P3 ]
수정 예시
-import { postPlayLink } from '../_apis/postPlayLink'; +import { postPlayLink } from '`@/app/tournament/`[id]/result/_apis/postPlayLink';🤖 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]/result/_hooks/usePostPlayLink.ts at line 7, Update the import in usePostPlayLink to use the project’s `@/`* absolute module alias for postPlayLink instead of the parent-directory relative path ../_apis/postPlayLink.Source: Coding guidelines
apps/web/src/app/error.tsx (1)
13-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Props를ErrorProps로 변경하세요.
Props는 컴포넌트 전용 props 타입명 규칙을 따르지 않습니다.코딩 가이드라인에 따라 Props 타입명은
{ComponentName}Props형식을 사용합니다.[ P3 ]
수정 예시
-type Props = { +type ErrorProps = { error: Error & { digest?: string }; reset: () => void; }; -function Error({ error, reset }: Props) { +function Error({ error, reset }: ErrorProps) {🤖 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/error.tsx` around lines 13 - 21, Rename the component props type from Props to ErrorProps and update the Error component’s parameter annotation to use the renamed type, preserving the existing fields and behavior.Source: Coding guidelines
🤖 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/app/apis/postSocialLogin.ts`:
- Line 30: Update the response variable in postSocialLogin to allow a nullable
data payload, then in the successful 2xx branch validate both the response
object and data.data before returning. Reject `{ data: null, code: null }` as an
unsuccessful response so useSocialLogin receives only a valid
SocialLoginSuccessPayloadT.
In `@apps/web/e2e/README.md`:
- Line 74: Update the E2E response-body examples in the README to remove the
status field from payloads passed to createApiSuccess and createApiError,
documenting only the data and code values. Keep HTTP status configuration
represented through route.fulfill or res.writeHead, and retain the ENDPOINTS
constant guidance.
In `@apps/web/src/apis/server.ts`:
- Around line 34-41: Update the MEMBER direct-entry guard in the login page
component around the existing SESSION_EXPIRED exception to also allow
QUERY_ACTION.VALUE.WITHDRAWN_ACCOUNT, preserving the current redirect behavior
for all other authenticated sessions so deleted-account users can remain on the
login page and see the withdrawal notice.
In `@apps/web/src/app/play/`[id]/_components/PlayClient.tsx:
- Around line 127-131: Update the home-navigation control in PlayClient so it
does not nest the native Button rendered by Button inside Link. Render Link with
the existing button styling, or configure Button to render the link element
directly, while preserving the ROUTES.HOME destination, sizing, variant, and
Korean label.
- Line 112: 오류 화면의 페이지 제목인 `<p>` 요소를 `<h1>` 요소로 변경하세요. `heading-1-bold
text-text-neutral-primary` 클래스와 기존 텍스트는 그대로 유지하고, 해당 제목이 오류 페이지의 최상위 제목으로 렌더링되도록
`PlayClient`의 오류 화면 마크업만 수정하세요.
In
`@apps/web/src/app/tournament/`[id]/create/by-wish/_components/ByWishContent.tsx:
- Line 112: Replace the inline router.back() handler on NoWishDialog with the
shared navigation hook’s close/back action, reusing the hook and established
fallback policy already used by this flow. Keep hasNoSelectableWish controlling
the dialog’s open state and pass the hook-provided action to onOpenChange.
In
`@apps/web/src/app/tournament/`[id]/create/by-wish/_components/NoWishDialog.tsx:
- Around line 34-38: Remove the DialogClose wrapper around the wishlist CTA in
NoWishDialog, leaving ButtonLink with href ROUTES.WISHLIST as a standalone link
so clicking it performs only the intended navigation.
In `@apps/web/src/app/tournament/join/_hooks/usePostJoin.ts`:
- Around line 41-45: Update
apps/web/src/app/tournament/join/_hooks/usePostJoin.ts lines 41-45 to route
TOURNAMENT_NOT_PENDING and TOURNAMENT_INVITE_EXPIRED through distinct callbacks
instead of defaulting both to onUnavailable; update
apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx lines
52-57 to connect the not-pending callback to ALREADY_STARTED, while preserving
the expired-invite callback for LINK_EXPIRED.
In `@apps/web/src/app/tournament/join/`[id]/_components/JoinPreviewClient.tsx:
- Around line 158-160: Update the JoinErrorDialog usage in JoinPreviewClient so
PARTICIPANTS_FULL and LINK_EXPIRED remain non-dismissible and cannot clear
joinErrorType through Escape or backdrop clicks. Remove the onOpenChange handler
for this terminal join-failure path, or separate dialog visibility from the
error state if closing is required elsewhere, while preserving the
home-navigation CTA behavior.
In `@docs/spec/error-handling-policy.md`:
- Line 107: docs/spec/error-handling-policy.md의 blockquote에서 Line 107에 해당하는 빈 줄을
제거하거나 인접한 줄을 연속된 blockquote 형식으로 수정해 markdownlint MD028을 해결하세요.
- Line 15: Update the error-message rules near the documented policy statements
to describe the actual fallback behavior: use getApiErrorMessage(error), prefer
the catalog message when code is present and recognized, and use the generic
fallback when code is missing or unknown. Apply the same wording consistently to
the corresponding guidance around the other referenced section.
- Line 249: docs/spec/error-handling-policy.md의 249번째 fenced code block에 text 언어
식별자를 추가하세요. 디렉터리 트리 내용을 나타내는 해당 블록의 여는 fence를 ```text로 변경하고 블록 내용과 닫는 fence는 그대로
유지하세요.
In `@packages/core/src/utils/getErrorMessageByCode.ts`:
- Around line 10-14: Update getErrorMessageByCode to return a mapped message
only when code is an own property of ERROR_MESSAGE_MAP, otherwise return null;
add regression tests covering toString, constructor, and __proto__ as unknown
codes.
---
Outside diff comments:
In `@apps/web/src/app/tournament/`[id]/match/_hooks/useTournament.ts:
- Around line 177-180: 일반 라운드의 onError 처리에서 syncWithServer() 또는
setRemainingItems(...)로 복구한 뒤 unlockSelection()을 호출하세요. currentMatch가 변경되지 않는
경우에도 선택 잠금이 해제되어 현재 매치를 다시 대결할 수 있도록 하며, 관련 로직은 useTournament의 해당 onError 분기만
수정하세요.
---
Nitpick comments:
In `@apps/web/src/app/error.tsx`:
- Around line 13-21: Rename the component props type from Props to ErrorProps
and update the Error component’s parameter annotation to use the renamed type,
preserving the existing fields and behavior.
In `@apps/web/src/app/play/`[id]/_components/PlayClient.tsx:
- Line 18: Update the postFromPlayLink import in PlayClient.tsx to use the
project’s `@/`* absolute alias, targeting `@/app/play/`[id]/_apis/postFromPlayLink
instead of the relative path.
In `@apps/web/src/app/tournament/`[id]/result/_hooks/usePostPlayLink.ts:
- Line 7: Update the import in usePostPlayLink to use the project’s `@/`* absolute
module alias for postPlayLink instead of the parent-directory relative path
../_apis/postPlayLink.
In `@docs/spec/api-status-audit.md`:
- Around line 225-227: Update the API status audit references from InviteClient
to JoinPreviewClient in the affected 400/404 and 409 entries, preserving the
existing status behavior and wording.
🪄 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: e94873d1-8947-4388-a2aa-114575fe2eb7
⛔ Files ignored due to path filters (1)
apps/web/src/assets/images/sad-face.svgis excluded by!**/*.svg
📒 Files selected for processing (71)
CLAUDE.mdapps/app/apis/postSocialLogin.tsapps/app/hooks/useSocialLogin.tsapps/web/e2e/README.mdapps/web/e2e/fixtures/mockApiFixture.tsapps/web/e2e/helpers/apiResponse.tsapps/web/e2e/setup/mockApiServer.tsapps/web/next.config.mjsapps/web/src/apis/client.tsapps/web/src/apis/getInvitePreviewByCode.tsapps/web/src/apis/server.tsapps/web/src/app/archive/wish/[id]/_hooks/useDeleteWish.tsapps/web/src/app/archive/wish/[id]/_hooks/usePatchWish.tsapps/web/src/app/auth/callback/[provider]/_hooks/usePostSocialLogin.tsapps/web/src/app/error.tsxapps/web/src/app/home/_components/InviteTournamentDialog.tsxapps/web/src/app/home/_components/invite-code-dialog/InvalidCodeDialog.tsxapps/web/src/app/home/page.tsxapps/web/src/app/invite/[id]/_components/InviteInvalid.tsxapps/web/src/app/invite/[id]/page.tsxapps/web/src/app/login/_components/LoginButtons.tsxapps/web/src/app/login/page.tsxapps/web/src/app/mypage/edit/_hooks/usePatchMe.tsapps/web/src/app/mypage/withdraw/_hooks/useDeleteMe.tsapps/web/src/app/play/[id]/_components/PlayClient.tsxapps/web/src/app/tournament/[id]/_common/_hooks/useDeleteTournamentItem.tsapps/web/src/app/tournament/[id]/create/_components/invite-friends/InviteFriendsDialog.tsxapps/web/src/app/tournament/[id]/create/_components/participant-panel/ParticipantPanel.tsxapps/web/src/app/tournament/[id]/create/_hooks/usePatchInviteExpiry.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/by-wish/_components/ByWishContent.tsxapps/web/src/app/tournament/[id]/create/by-wish/_components/NoWishDialog.tsxapps/web/src/app/tournament/[id]/create/by-wish/_hooks/usePostTournamentItemsByWish.tsapps/web/src/app/tournament/[id]/item/[itemId]/_hooks/usePatchTournamentItem.tsapps/web/src/app/tournament/[id]/match/_hooks/usePostRecordMatch.tsapps/web/src/app/tournament/[id]/match/_hooks/useTournament.tsapps/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/usePostPlayLink.tsapps/web/src/app/tournament/join/[id]/_components/JoinErrorScreen.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/_hooks/useGetInvitePreview.tsapps/web/src/app/tournament/join/_hooks/usePostJoin.tsapps/web/src/components/common/join-error-dialog/index.tsxapps/web/src/components/common/join-error-dialog/joinErrorDialog.const.tsapps/web/src/components/get-item-dialog/ByLinkDialog.tsxapps/web/src/components/tournament-error-dialog/index.tsxapps/web/src/consts/api.tsapps/web/src/consts/queryAction.tsapps/web/src/consts/route.tsapps/web/src/hooks/useNativeLoginResult.tsapps/web/src/hooks/useNicknameValidation.tsapps/web/src/hooks/usePostTournamentOCR.tsapps/web/src/hooks/usePostWishLink.tsapps/web/src/hooks/usePostWishOCR.tsapps/web/src/types/api.tsapps/web/src/utils/apiError.tsapps/web/src/utils/getApiErrorMessage.tsapps/web/src/utils/getRouteType.tsapps/web/src/utils/loginRedirect.tsapps/web/src/utils/queryClient.tsdocs/spec/api-status-audit.mddocs/spec/error-handling-policy.mddocs/spec/ownership.mdpackages/core/src/consts/errorCode.tspackages/core/src/index.tspackages/core/src/types/error.tspackages/core/src/utils/getErrorMessageByCode.ts
💤 Files with no reviewable changes (10)
- apps/web/src/app/tournament/join/_hooks/useGetInvitePreview.ts
- apps/web/src/app/home/page.tsx
- apps/web/src/app/tournament/join/_apis/getInvitePreview.ts
- apps/web/src/app/home/_components/invite-code-dialog/InvalidCodeDialog.tsx
- apps/web/src/app/invite/[id]/_components/InviteInvalid.tsx
- apps/web/src/consts/api.ts
- apps/web/src/utils/getRouteType.ts
- apps/web/src/components/tournament-error-dialog/index.tsx
- apps/web/src/app/invite/[id]/page.tsx
- apps/web/src/consts/route.ts
| } | ||
|
|
||
| let data: { data: SocialLoginSuccessPayloadT; detail?: string } | null = null; | ||
| let data: { data: SocialLoginSuccessPayloadT; code: ApiErrorCodeT | null } | null = null; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
2xx 응답의 data를 실제로 검증하세요.
현재 타입은 모든 JSON 응답에서 data가 SocialLoginSuccessPayloadT라고 선언합니다. 그러나 공통 계약의 실패 응답은 data: null입니다. if (!data)는 JSON 객체만 확인하므로 { data: null, code: null }인 2xx 응답을 통과시킬 수 있습니다. 이후 useSocialLogin이 결과를 구조 분해하면서 로그인 오류가 후속 예외로 바뀝니다.
data를 nullable로 선언하고 성공 분기에서 data.data를 확인한 뒤 반환하세요.
수정 예시
-let data: { data: SocialLoginSuccessPayloadT; code: ApiErrorCodeT | null } | null = null;
+let data: { data: SocialLoginSuccessPayloadT | null; code: ApiErrorCodeT | null } | null = null;
+ const payload = data?.data;
- if (!data) {
+ if (!payload) {
...
}
- return data.data;
+ return payload;[P3]
🤖 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/app/apis/postSocialLogin.ts` at line 30, Update the response variable in
postSocialLogin to allow a nullable data payload, then in the successful 2xx
branch validate both the response object and data.data before returning. Reject
`{ data: null, code: null }` as an unsuccessful response so useSocialLogin
receives only a valid SocialLoginSuccessPayloadT.
| ``` | ||
|
|
||
| 경로는 항상 `@/consts/api` 의 `ENDPOINTS` 상수를 쓰세요. 응답은 팀 규약 `{ status, data, detail, code }` 로 자동 래핑되므로 **data 안에 들어갈 내용만** 넘기면 됩니다. | ||
| 경로는 항상 `@/consts/api` 의 `ENDPOINTS` 상수를 쓰세요. 응답은 팀 규약 `{ status, data, code }` 로 자동 래핑되므로 **data 안에 들어갈 내용만** 넘기면 됩니다. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
E2E 응답 본문 예시에서 status를 제거하세요.
createApiSuccess와 createApiError는 { data, code }만 반환합니다. HTTP status는 route.fulfill 또는 res.writeHead의 실제 응답 상태로 설정됩니다. 현재 문서는 테스트 작성자에게 응답 본문이 { status, data, code }라고 안내합니다.
-응답은 팀 규약 `{ status, data, code }` 로 자동 래핑되므로 **data 안에 들어갈 내용만** 넘기면 됩니다.
+응답 본문은 팀 규약 `{ data, code }` 로 자동 래핑되며, HTTP status는 실제 응답 상태로 설정됩니다. **data 안에 들어갈 내용만** 넘기면 됩니다.[P3]
📝 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.
| 경로는 항상 `@/consts/api` 의 `ENDPOINTS` 상수를 쓰세요. 응답은 팀 규약 `{ status, data, code }` 로 자동 래핑되므로 **data 안에 들어갈 내용만** 넘기면 됩니다. | |
| 응답 본문은 팀 규약 `{ data, code }` 로 자동 래핑되며, HTTP status는 실제 응답 상태로 설정됩니다. **data 안에 들어갈 내용만** 넘기면 됩니다. |
🤖 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/e2e/README.md` at line 74, Update the E2E response-body examples in
the README to remove the status field from payloads passed to createApiSuccess
and createApiError, documenting only the data and code values. Keep HTTP status
configuration represented through route.fulfill or res.writeHead, and retain the
ENDPOINTS constant guidance.
| async (error: AxiosError<ApiErrorResponseT>) => { | ||
| /** 탈퇴한 계정인 경우 로그아웃 후 로그인 페이지로 리다이렉트 */ | ||
| if (error.response?.status === 409 && error.response.data?.code === ERROR_CODE.USER_DELETED) { | ||
| const { redirect } = await import('next/navigation'); | ||
|
|
||
| redirect(getLoginPath(null, QUERY_ACTION.VALUE.WITHDRAWN_ACCOUNT)); | ||
| } | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
탈퇴 계정 세션이 로그인 화면에 도달하도록 수정하세요.
serverApi는 쿠키를 삭제할 수 없지만 WITHDRAWN_ACCOUNT 액션으로 로그인 페이지를 리다이렉트합니다. 현재 apps/web/src/app/login/page.tsx Line 23은 SESSION_EXPIRED만 예외 처리합니다. 따라서 남아 있는 MEMBER 토큰이 있으면 로그인 페이지가 홈으로 즉시 리다이렉트됩니다. 탈퇴 안내 토스트가 표시되지 않고 홈 API 호출이 다시 실패할 수 있습니다.
WITHDRAWN_ACCOUNT도 MEMBER 직접 진입 가드의 예외로 처리하세요.
수정 예시
- if (role === 'MEMBER' && action !== QUERY_ACTION.VALUE.SESSION_EXPIRED) redirect(ROUTES.HOME);
+ if (
+ role === 'MEMBER' &&
+ action !== QUERY_ACTION.VALUE.SESSION_EXPIRED &&
+ action !== QUERY_ACTION.VALUE.WITHDRAWN_ACCOUNT
+ )
+ redirect(ROUTES.HOME);[P1]
🤖 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/server.ts` around lines 34 - 41, Update the MEMBER
direct-entry guard in the login page component around the existing
SESSION_EXPIRED exception to also allow QUERY_ACTION.VALUE.WITHDRAWN_ACCOUNT,
preserving the current redirect behavior for all other authenticated sessions so
deleted-account users can remain on the login page and see the withdrawal
notice.
| return ( | ||
| <main className="flex min-h-dvh flex-col items-center justify-center gap-6 bg-bg-layer-basement px-5 pt-padding-top"> | ||
| <div className="flex flex-col items-center gap-2"> | ||
| <p className="heading-1-bold text-text-neutral-primary">오류가 발생했어요</p> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
페이지 제목을 <h1>로 변경하세요.
Line 112의 <p>는 오류 화면의 페이지 제목입니다. <h1>로 렌더링해야 문서 구조와 스크린 리더 탐색이 올바르게 동작합니다.
As per coding guidelines, 제목에는 <h1> 또는 <h2>를 사용해야 합니다.
수정 예시
-<p className="heading-1-bold text-text-neutral-primary">오류가 발생했어요</p>
+<h1 className="heading-1-bold text-text-neutral-primary">오류가 발생했어요</h1>📝 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.
| <p className="heading-1-bold text-text-neutral-primary">오류가 발생했어요</p> | |
| <h1 className="heading-1-bold text-text-neutral-primary">오류가 발생했어요</h1> |
🤖 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/play/`[id]/_components/PlayClient.tsx at line 112, 오류 화면의
페이지 제목인 `<p>` 요소를 `<h1>` 요소로 변경하세요. `heading-1-bold text-text-neutral-primary`
클래스와 기존 텍스트는 그대로 유지하고, 해당 제목이 오류 페이지의 최상위 제목으로 렌더링되도록 `PlayClient`의 오류 화면 마크업만
수정하세요.
Source: Coding guidelines
| <Link href={ROUTES.HOME} className="w-full"> | ||
| <Button size="lg" variant="secondary" className="w-full"> | ||
| 홈으로 가기 | ||
| </Button> | ||
| </Link> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Link와 Button을 중첩하지 마세요.
현재 구조는 <a><button>...</button></a>입니다. 두 interactive element를 중첩하면 키보드 및 스크린 리더 동작이 일관되지 않을 수 있습니다. Link에 버튼 스타일을 적용하거나, Button이 link element를 직접 렌더링하도록 변경하세요.
제공된 Button 구현은 native <button>을 렌더링합니다.
As per coding guidelines, semantic HTML을 사용해야 합니다.
🤖 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/play/`[id]/_components/PlayClient.tsx around lines 127 -
131, Update the home-navigation control in PlayClient so it does not nest the
native Button rendered by Button inside Link. Render Link with the existing
button styling, or configure Button to render the link element directly, while
preserving the ROUTES.HOME destination, sizing, variant, and Korean label.
Source: Coding guidelines
| {joinErrorType && ( | ||
| <JoinErrorDialog type={joinErrorType} open onOpenChange={() => setJoinErrorType(null)} /> | ||
| )} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
[P2] 참여 불가 상태를 닫아 재시도 상태로 바꾸지 마세요.
Escape 또는 dim 클릭은 onOpenChange를 통해 joinErrorType을 null로 만듭니다. 회원 화면은 이후 남아 있는 isPostJoinError 때문에 retryable UI를 표시합니다. 게스트 화면은 만료되었거나 정원이 찬 링크를 다시 제출할 수 있습니다.
이 경로의 PARTICIPANTS_FULL과 LINK_EXPIRED는 홈 이동 CTA를 가진 종료 상태입니다. 이 참가 경로에서는 onOpenChange를 전달하지 마세요. 닫힘이 필요하면 다이얼로그 표시 상태와 오류 상태를 분리하세요.
수정 예시
- <JoinErrorDialog type={joinErrorType} open onOpenChange={() => setJoinErrorType(null)} />
+ <JoinErrorDialog type={joinErrorType} open />PR 목표의 “진입 실패 시 닫히지 않는 다이얼로그 화면” 요구와 다릅니다.
[P2]
Also applies to: 209-211
🤖 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 158 - 160, Update the JoinErrorDialog usage in JoinPreviewClient so
PARTICIPANTS_FULL and LINK_EXPIRED remain non-dismissible and cannot clear
joinErrorType through Escape or backdrop clicks. Remove the onOpenChange handler
for this terminal join-failure path, or separate dialog visibility from the
error state if closing is required elsewhere, while preserving the
home-navigation CTA behavior.
| - **401 (인증 만료)** → **전역 인터셉터**가 토큰 refresh 후 자동 재시도, 실패 시 로그인 redirect. | ||
| - **인증/권한 게이팅** → **layout 가드(SSR)** 가 진입 시점에 차단. | ||
| - **조용히 실패 금지** → 개별 onError가 없어도 **전역 안전망**이 최소 토스트를 보장. | ||
| - **문구는 `code`, 동작은 `status`** → 사용자 문구는 100% 에러 코드 카탈로그. 분기(이동/다이얼로그)는 status가 1차, code는 같은 status에서 동작이 갈릴 때만. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
오류 문구 규칙을 fallback 동작과 일치시키세요.
Line 15와 Line 146은 사용자 문구가 항상 code에서 온다고 설명합니다. 그러나 Line 258-268은 코드가 없거나 알 수 없을 때 generic fallback을 사용합니다.
다음처럼 규칙을 명확히 작성하세요. getApiErrorMessage(error)를 사용하고, code가 있으면 카탈로그를 우선하며, 없으면 generic fallback을 사용합니다.
제안 수정
-문구는 항상 `code` — `getApiErrorMessage(error)` 하나로 통일.
+문구는 `getApiErrorMessage(error)`로 통일한다. `code`가 있으면 카탈로그를 우선하고, 없으면 generic fallback을 사용한다.Based on learnings: 코드 리뷰 코멘트에는 P1~P5 PN 태그를 사용합니다.
[P3]
Also applies to: 146-150
🤖 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/spec/error-handling-policy.md` at line 15, Update the error-message
rules near the documented policy statements to describe the actual fallback
behavior: use getApiErrorMessage(error), prefer the catalog message when code is
present and recognized, and use the generic fallback when code is missing or
unknown. Apply the same wording consistently to the corresponding guidance
around the other referenced section.
Source: Learnings
| > if (getApiErrorStatus(error) === 404) router.replace(...); // 이탈이 필요한 status 만 추가 동작 | ||
| > } | ||
| > ``` | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Blockquote 내부의 빈 줄을 정리하세요.
Line 107의 빈 줄이 blockquote 내부에 있어 markdownlint MD028을 발생시킵니다. 빈 줄을 제거하거나 blockquote 형식을 연속해서 유지하세요.
Based on learnings: 코드 리뷰 코멘트에는 P1~P5 PN 태그를 사용합니다.
[P3]
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)
[warning] 107-107: Blank line inside blockquote
(MD028, no-blanks-blockquote)
🤖 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/spec/error-handling-policy.md` at line 107,
docs/spec/error-handling-policy.md의 blockquote에서 Line 107에 해당하는 빈 줄을 제거하거나 인접한
줄을 연속된 blockquote 형식으로 수정해 markdownlint MD028을 해결하세요.
Sources: Learnings, Linters/SAST tools
| 에러 → 사용자 문구 변환을 한 곳으로 일원화한다. | ||
| 문구 카탈로그는 web·app 공유를 위해 `@piki/core`에 두고, web은 axios 파싱만 담당한다. | ||
|
|
||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
코드 블록에 언어 식별자를 추가하세요.
Line 249의 fenced code block에 언어 식별자가 없어 markdownlint MD040을 발생시킵니다. 디렉터리 트리에는 text를 사용하세요.
제안 수정
-```
+```textBased on learnings: 코드 리뷰 코멘트에는 P1~P5 PN 태그를 사용합니다.
[P3]
📝 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.
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)
[warning] 249-249: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 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/spec/error-handling-policy.md` at line 249,
docs/spec/error-handling-policy.md의 249번째 fenced code block에 text 언어 식별자를 추가하세요.
디렉터리 트리 내용을 나타내는 해당 블록의 여는 fence를 ```text로 변경하고 블록 내용과 닫는 fence는 그대로 유지하세요.
Sources: Learnings, Linters/SAST tools
| export const getErrorMessageByCode = (code: ApiErrorCodeT | null | undefined): string | null => { | ||
| if (!code) return null; | ||
|
|
||
| return (ERROR_MESSAGE_MAP as Record<string, string>)[code] ?? null; | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
[P3] 알 수 없는 코드가 fallback을 우회합니다.
Line 13은 일반 객체를 문자열 인덱스로 읽습니다. code가 'toString', 'constructor', '__proto__'이면 상속된 함수 또는 객체를 반환합니다. 이는 알 수 없는 코드에서 null을 반환한다는 함수 계약을 깨뜨립니다. apps/web/src/app/login/_components/LoginButtons.tsx의 Line 69는 URL의 code를 직접 전달하므로, 조작된 로그인 URL이 toast.error에 문자열이 아닌 값을 전달할 수 있습니다.
자체 속성인 경우에만 값을 반환하세요. 'toString', 'constructor', '__proto__' 회귀 테스트도 추가하세요.
수정 예시
export const getErrorMessageByCode = (code: ApiErrorCodeT | null | undefined): string | null => {
- if (!code) return null;
+ if (!code || !Object.prototype.hasOwnProperty.call(ERROR_MESSAGE_MAP, code)) return null;
return (ERROR_MESSAGE_MAP as Record<string, string>)[code] ?? null;
};📝 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.
| export const getErrorMessageByCode = (code: ApiErrorCodeT | null | undefined): string | null => { | |
| if (!code) return null; | |
| return (ERROR_MESSAGE_MAP as Record<string, string>)[code] ?? null; | |
| }; | |
| export const getErrorMessageByCode = (code: ApiErrorCodeT | null | undefined): string | null => { | |
| if (!code || !Object.prototype.hasOwnProperty.call(ERROR_MESSAGE_MAP, code)) return null; | |
| return (ERROR_MESSAGE_MAP as Record<string, string>)[code] ?? null; | |
| }; |
🤖 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 `@packages/core/src/utils/getErrorMessageByCode.ts` around lines 10 - 14,
Update getErrorMessageByCode to return a mapped message only when code is an own
property of ERROR_MESSAGE_MAP, otherwise return null; add regression tests
covering toString, constructor, and __proto__ as unknown codes.
작업 요약
/tournament/join/[id]하나로 통합합니다 (검증 조회 1번, 리다이렉트 0홉)JoinErrorDialog모음집으로 재편합니다by-invite-code의 409/400 을 서버 에러 코드 기준으로 분기해 사유별 안내를 노출합니다작업 세부 내용
머지 순서: 이 브랜치는 #343 작업(
feat/343-error-code) 위에 스택되어 있습니다. 해당 PR 머지 후 이 PR 의 diff 가 아래 범위로 줄어듭니다.진입 경로 통합
/invite/[id]라우트 삭제 — 코드 검증·joined리다이렉트를 join RSC 로 이관하고,by-invite-code응답을 참여 화면에 그대로 넘겨{id}/invite-preview재조회를 제거했습니다/invite/:id → /tournament/join/:id리다이렉트는 유지합니다 (next.config, query 보존 — 앱 수정 불필요)buildInviteUrl)·ROUTES·getRouteType에서 invite 경로를 정리했습니다에러 안내 다이얼로그 재편
InviteInvalid·InvalidCodeDialog·tournament-error-dialog를 삭제하고, 소셜 합류 에러만 모은components/common/join-error-dialog를 신설했습니다 (ALREADY_STARTED·ALREADY_ENDED·LINK_EXPIRED·INVALID_CODE·PARTICIPANTS_FULL)NO_WISH_EXISTS는 by-wish 전용NoWishDialog로 콜로케이션하고, 소비처 없는REQUEST_FAILED타입은 제거했습니다onOpenChange미전달 시 딤·ESC 로 닫히지 않는 종료 다이얼로그를 지원합니다 — 진입 실패 화면은 빈 배경 + 다이얼로그만 노출됩니다에러 코드 분기
TOURNAMENT-005(PENDING 아님) → 이미 시작 안내,TOURNAMENT-021(초대 만료) → 링크 만료 안내 — 링크 진입·코드 입력이 같은 기준으로 판정합니다INVALID_CODE안내, CTA 는 홈 이동으로 통일해 빈 화면에 갇히지 않게 했습니다TOURNAMENT-005가 진행 중/완료를 한 코드로 덮는 문제는 서버 분리 후 후속 이슈에서 처리합니다기타
api-status-audit.md(by-invite-code·{id}/invite-preview항목),ownership.md(/invite행 제거)ALREADY_STARTEDdescription 의 아이템 추가 맥락 문구연관 이슈
closes #434
Summary by CodeRabbit
새로운 기능
버그 수정