feat: 서버 에러 코드 기반 문구 카탈로그 도입 및 4xx/5xx 처리 계층 정리 - #432
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.
|
|
Warning Review limit reached
Next review available in: 38 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughAPI 응답을 Changes에러 계약과 메시지 카탈로그
전역 세션 및 오류 인터셉터
기능별 오류 처리
인증 및 응답 타입
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant API
participant clientApi
participant ErrorUtils
participant FeatureHook
participant UI
API->>clientApi: { data, code } 오류 응답
clientApi->>ErrorUtils: 상태와 API 코드 추출
ErrorUtils->>FeatureHook: 오류 분류 결과
FeatureHook->>UI: 토스트, 이동 또는 다이얼로그
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 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: 12
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/home/_components/InviteTournamentDialog.tsx (1)
52-59: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win409 상태만으로 초대 만료를 판별하지 마세요.
getApiErrorStatus(error) === 409는 만료 외에도TOURNAMENT_ALREADY_PARTICIPANT같은 conflict 오류까지 만료 화면으로 보냅니다.getApiErrorCode(error)로ERROR_CODE.TOURNAMENT_INVITE_EXPIRED만 확인한 뒤, 초대 만료 다이얼로그를 노출하세요. 나머지 409는 해당 상태에 맞는 안내 흐름으로 분기해야 합니다.
apps/web/src/app/home/_components/InviteTournamentDialog.tsx#L52-L59:getApiErrorCode(error)로 초대 만료 code를 확인한 뒤에만setIsTournamentErrorDialogOpen(true)를 호출하세요.apps/web/src/app/invite/[id]/page.tsx#L33-L34:getApiErrorCode(error)로 초대 만료 code를 확인한 뒤에만showExpiredDialog를true로 설정하세요.🤖 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/InviteTournamentDialog.tsx` around lines 52 - 59, Update the error branching in InviteTournamentDialog.tsx around setIsTournamentErrorDialogOpen so it checks getApiErrorCode(error) against ERROR_CODE.TOURNAMENT_INVITE_EXPIRED before showing the expiration dialog; route other 409 errors through their appropriate existing flow. Apply the same code-specific expiration check in apps/web/src/app/invite/[id]/page.tsx at lines 33-34 before setting showExpiredDialog to true.
🤖 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: Validate the parsed 2xx response in the postSocialLogin flow before
treating it as a successful payload: only return the login data when code is
null and both accessToken and refreshToken are present strings. Reject null or
structurally incomplete data so callers use the existing error collection and
fallback-message path.
In `@apps/web/e2e/README.md`:
- Line 74: Update the response-wrapping guidance near the ENDPOINTS reference to
remove status from the response body contract, documenting that mocked responses
contain only { data, code } while HTTP status is supplied separately through
route.fulfill. Align the wording with the createApiSuccess and createApiError
helper behavior.
In `@apps/web/src/app/auth/callback/`[provider]/_hooks/usePostSocialLogin.ts:
- Around line 36-45: Update the onError handler in usePostSocialLogin so errors
already handled by the clientApi interceptor—401 responses, 409 USER_DELETED
responses, and 5xx or network errors—return without calling the local
SOCIAL_LOGIN_ERROR redirect. Preserve local login-page handling only for other
4xx errors, including extracting the API error code for those cases.
In `@apps/web/src/app/error.tsx`:
- Around line 18-19: 오류 문구 해석을 code-aware 공통 resolver로 통일하세요.
apps/web/src/app/error.tsx#L18-L19의 DEFAULT_DESCRIPTION를 제거하고,
apps/web/src/app/error.tsx#L3 및 `#L26에서` getApiErrorMessage 또는 동일한 공통 resolver를
사용해 카탈로그 fallback을 적용하세요.
apps/web/src/app/login/_components/LoginButtons.tsx#L4-L10의 직접적인
ERROR_MESSAGE_MAP 조회 import를 제거하고, `#L59-L69의` 세션 만료·탈퇴 계정·소셜 로그인 오류를 같은 resolver로
처리하세요.
In `@apps/web/src/app/mypage/edit/_hooks/usePatchMe.ts`:
- Line 32: Update the shared getApiErrorMessage resolver to return a non-empty
response detail after attempting the registered code lookup and before using the
generic status fallback, preserving the code → detail → generic fallback order.
The affected call sites require no direct changes:
apps/web/src/app/mypage/edit/_hooks/usePatchMe.ts:32,
apps/web/src/hooks/useNicknameValidation.ts:27-28,
apps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentItemLink.ts:38,
apps/web/src/app/tournament/[id]/create/_hooks/usePatchInviteExpiry.ts:31,
apps/web/src/app/tournament/[id]/match/_hooks/usePostRecordMatch.ts:31,
apps/web/src/app/tournament/[id]/result/_hooks/usePostPlayLink.ts:21, and
apps/web/src/app/tournament/join/_hooks/usePostJoin.ts:58 will receive the
detail through the centralized resolver.
In `@apps/web/src/app/play/`[id]/_components/PlayClient.tsx:
- Around line 127-131: PlayClient의 홈 이동 UI에서 Link와 Button의 중첩된 상호작용 요소를 제거하세요.
해당 블록은 ButtonLink를 사용하거나 Link 하나에 기존 버튼 스타일을 적용해 단일 포커스·활성화 요소로 렌더링되도록 수정하고,
ROUTES.HOME 이동과 현재 시각적 스타일은 유지하세요.
In
`@apps/web/src/app/tournament/`[id]/create/by-wish/_components/ByWishContent.tsx:
- Around line 110-113: Update the TournamentErrorDialog onOpenChange handler in
ByWishContent so it does not call router.back() directly when the recovery
button closes the dialog; let buttonLink handle recovery navigation, and if a
close fallback is required, manage it separately through the shared navigation
hook.
In `@apps/web/src/app/tournament/join/_hooks/usePostJoin.ts`:
- Around line 41-50: Update the conflict-handler selection in usePostJoin so
onUnavailable is not used as the default for unknown or missing 409 codes. Map
only the explicitly supported ERROR_CODE values to onAlreadyJoined or
onParticipantsFull, and route all other cases to the existing generic error
fallback around the later error-handling path.
In `@apps/web/src/hooks/useNativeLoginResult.ts`:
- Line 35: Update the native login error handling around toast.error so it
defensively validates the bridge payload before use. Display payload.detail only
when it is a string matching an allowed value in ERROR_MESSAGE_MAP; otherwise
retain DEFAULT_ERROR_MESSAGE, preventing raw provider or bridge messages from
being shown. Preserve the existing catalog messages for OAUTH-* and USER-*
errors, and do not add a payload code unless backward compatibility and
app-version registration are handled.
In `@apps/web/src/utils/getApiErrorMessage.ts`:
- Around line 11-24: Update getApiErrorMessage so that after
getErrorMessageByCode returns no message, it returns
error.response?.data?.detail only when detail is a string, before applying the
existing status-based generic fallback. Extend ApiErrorResponseT in api.ts with
an optional detail field while preserving the code → detail → generic priority.
In `@docs/spec/api-status-audit.md`:
- Line 11: Update the serverApi interceptor description in the API status audit
to state that it redirects to the login page for USER-003 409 responses, while
general 4xx/5xx errors continue to be thrown. Keep the existing clientApi
behavior description unchanged.
In `@docs/spec/error-handling-policy.md`:
- Line 107: 문서의 markdownlint 경고를 수정하세요. 빈 blockquote 행을 제거하고, 폴더 구조를 보여주는 fenced
code block에 text 언어 식별자를 지정하세요.
---
Outside diff comments:
In `@apps/web/src/app/home/_components/InviteTournamentDialog.tsx`:
- Around line 52-59: Update the error branching in InviteTournamentDialog.tsx
around setIsTournamentErrorDialogOpen so it checks getApiErrorCode(error)
against ERROR_CODE.TOURNAMENT_INVITE_EXPIRED before showing the expiration
dialog; route other 409 errors through their appropriate existing flow. Apply
the same code-specific expiration check in apps/web/src/app/invite/[id]/page.tsx
at lines 33-34 before setting showExpiredDialog to true.
🪄 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: f91b9161-2bdc-4c60-943f-267cc4b46cf1
📒 Files selected for processing (54)
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/src/apis/client.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/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/_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/_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/plate-share-dialog/PlateShareDialog.tsxapps/web/src/app/tournament/[id]/result/_hooks/usePostPlayLink.tsapps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsxapps/web/src/app/tournament/join/_hooks/usePostJoin.tsapps/web/src/components/get-item-dialog/ByLinkDialog.tsxapps/web/src/components/tournament-error-dialog/index.tsxapps/web/src/consts/queryAction.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/types/tournament.tsapps/web/src/utils/apiError.tsapps/web/src/utils/getApiErrorMessage.tsapps/web/src/utils/loginRedirect.tsapps/web/src/utils/queryClient.tsdocs/spec/api-status-audit.mddocs/spec/error-handling-policy.mdpackages/core/src/consts/errorCode.tspackages/core/src/index.tspackages/core/src/types/error.tspackages/core/src/utils/getErrorMessageByCode.ts
code 가 null 이고 토큰 한 쌍이 문자열일 때만 성공으로 취급한다.
2xx 인데 { data: null, code: 'OAUTH-001' } 같은 본문이 오면
그대로 통과해 호출부가 빈 값을 구조 분해하던 문제.
onUnavailable 을 기본값으로 두면 code 누락·신규 code 가 전부 만료 다이얼로그로 흘렀다. 아는 code 에만 전용 UX 를 연결하고 나머지는 generic 토스트로 보낸다.
앱이 보낸 detail 을 전부 버려 OAUTH-* 구분이 사라져 있었다. 브릿지 payload 는 런타임 검증이 없으므로 ERROR_MESSAGE_MAP 값과 일치할 때만 쓰고, SDK 예외 원문은 기본 문구로 대체한다.
/auth/login/*·/auth/guest 는 아직 세션이 없어 refresh 가 성공할 수 없는데도 401 마다 갱신을 한 번 시도하고 있었다. 이 401 은 세션 만료가 아니라 로그인 실패라 개별 onError 가 카탈로그 문구로 안내한다는 근거도 주석에 남긴다.
- 목 응답 래핑을 { data, code } 로 정정 (status 는 실제 status code)
- serverApi 응답 인터셉터 존재·로그인 요청 refresh 제외 반영
- join 409 code 분기 최신화, MD028·언어 없는 코드펜스 수정
There was a problem hiding this comment.
Actionable comments posted: 1
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/apis/client.ts (1)
78-98: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winWebView 로그아웃 메시지 실패 경로도 처리하세요.
apps/web/src/apis/client.ts의USER_DELETED409 처리에서WebBridge.postMessage(...)은 앱 응답을 기다리는 로그아웃 호출입니다.false가 반환되면 호출부에서 pending 요청/로딩 상태를 직접 정리해야 합니다.🤖 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/client.ts` around lines 78 - 98, Update the USER_DELETED handling around WebBridge.postMessage so its return value is checked; when the logout message returns false, explicitly clear the pending request and loading state through the existing caller cleanup mechanism before rejecting the error. Preserve the successful WebView logout flow and the current cookie, Sentry, and redirect behavior.Source: Coding guidelines
🧹 Nitpick comments (1)
apps/web/src/apis/getWishlist.ts (1)
7-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAPI 응답 타입 이름을 규약에 맞추세요.
GetWishlistApiResponseT는getWishlist의 응답 타입입니다. 이름을GetWishlistResponseT로 변경하세요.현재 가져오는
GetWishlistResponseT는 위시 항목 구조를 나타냅니다. 이 타입은WishlistItemT처럼 도메인 의미가 있는 이름으로 변경하세요. 이후WishContent등의 소비자 타입 참조도 함께 변경하세요.코딩 가이드라인에 따라 API 요청·응답 타입은 함수명 기반
RequestT와ResponseT형식을 사용해야 합니다.🤖 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/getWishlist.ts` around lines 7 - 14, Rename the imported wishlist item type GetWishlistResponseT to WishlistItemT and update all consumers such as WishContent accordingly; rename the API wrapper type GetWishlistApiResponseT to GetWishlistResponseT while preserving its existing response and pagination structure, and update all references to the wrapper.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.
Outside diff comments:
In `@apps/web/src/apis/client.ts`:
- Around line 78-98: Update the USER_DELETED handling around
WebBridge.postMessage so its return value is checked; when the logout message
returns false, explicitly clear the pending request and loading state through
the existing caller cleanup mechanism before rejecting the error. Preserve the
successful WebView logout flow and the current cookie, Sentry, and redirect
behavior.
---
Nitpick comments:
In `@apps/web/src/apis/getWishlist.ts`:
- Around line 7-14: Rename the imported wishlist item type GetWishlistResponseT
to WishlistItemT and update all consumers such as WishContent accordingly;
rename the API wrapper type GetWishlistApiResponseT to GetWishlistResponseT
while preserving its existing response and pagination structure, and update all
references to the wrapper.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 39031890-b55e-4494-8aa5-8ffaf3f5d4ea
📒 Files selected for processing (13)
apps/app/apis/postSocialLogin.tsapps/web/e2e/README.mdapps/web/src/apis/client.tsapps/web/src/apis/getWishlist.tsapps/web/src/app/archive/wish/[id]/_hooks/usePatchWish.tsapps/web/src/app/archive/wish/_components/WishContent.tsxapps/web/src/app/auth/callback/[provider]/_hooks/usePostSocialLogin.tsapps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsxapps/web/src/app/tournament/[id]/item/[itemId]/_hooks/usePatchTournamentItem.tsapps/web/src/app/tournament/join/_hooks/usePostJoin.tsapps/web/src/hooks/useNativeLoginResult.tsdocs/spec/api-status-audit.mddocs/spec/error-handling-policy.md
🚧 Files skipped from review as they are similar to previous changes (8)
- apps/web/e2e/README.md
- apps/web/src/app/archive/wish/[id]/_hooks/usePatchWish.ts
- apps/web/src/app/auth/callback/[provider]/_hooks/usePostSocialLogin.ts
- apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx
- apps/web/src/app/tournament/[id]/item/[itemId]/_hooks/usePatchTournamentItem.ts
- apps/web/src/app/tournament/join/_hooks/usePostJoin.ts
- docs/spec/api-status-audit.md
- docs/spec/error-handling-policy.md
5ab334d to
9ddbf0b
Compare
# Conflicts: # apps/web/src/app/tournament/[id]/match/_hooks/useTournament.ts
작업 요약
@piki/core의 에러 코드 카탈로그(ERROR_MESSAGE_MAP)로 일원화합니다status·detail필드를 제거하고{ data, code }로 정리합니다onError, 5xx·401·탈퇴 계정은 전역 안전망이 단독 처리하도록 계층을 나눕니다USER-003) 요청을 인터셉터에서 감지해 세션을 정리하고 로그인으로 유도합니다작업 세부 내용
1. 에러 코드 카탈로그 (
@piki/core)서버는
code만 내려주고 문구는 100% 프론트가 관리하는 구조로 바꿨습니다. web·app 이 같은 문구를 쓰도록packages/core에 두었습니다.ERROR_MESSAGE_MAP— code → 사용자 문구 싱글 소스. 원본은 api-docs 의info.descriptionAPPLE-*,EXTRACTOR-*,SNAPSHOT-*)는 제외ERROR_CODE— 의미 기반 상수. code 분기 시'WISH-004'대신ERROR_CODE.WISH_NOT_FOUND를 쓰기 위한 것satisfies Record<string, keyof typeof ERROR_MESSAGE_MAP>+UnnamedErrorCodeT어서션으로 카탈로그에 있는데 이름이 없는 코드를 컴파일 타임에 잡습니다getErrorMessageByCode(code)— 순수 조회 헬퍼. 미매핑이면null을 반환하고, fallback 선택은 호출부 책임ApiErrorCodeT = ErrorCodeT | (string & {})— 알려진 코드는 자동완성되면서 서버가 새로 추가한 코드도 받습니다2. 응답 래퍼 정리 —
{ data, code }ApiResponseT<T>에서status·detail삭제, 성공의code는null로 좁힘ApiErrorResponseT.code는ApiErrorCodeTdetail이 사라졌으므로, 이를 읽던 모든 지점을getApiErrorMessage(error)로 교체했습니다 (닉네임 중복 안내, OCR, 링크 담기 등)extra의detail→apiCodecreateApiSuccess/createApiError)도 새 래퍼에 맞췄습니다. status 는 본문이 아니라 실제 HTTP status 로 내려갑니다3. 에러 분류 유틸 신설 —
utils/apiError.ts전역이 가져가는 에러 조건이 개별 훅마다 흩어져 있어 누락 시 조용히 중복 처리되는 문제가 있었습니다. 판별을 한곳으로 모았습니다.
isGlobalNetError— 전역 안전망이 덮는 에러(5xx·네트워크·401·탈퇴 계정). 새 케이스가 생기면 여기만 고칩니다isServerOrNetworkError/isWithdrawnAccountError/getApiErrorStatus/getApiErrorCode개별
onError는if (isGlobalNetError(error)) return;로 시작해 4xx 전부를 책임집니다.4. 4xx 무피드백 · 토스트 중복 제거
기존에는 특정 status 만 골라 처리해서, 그 밖의 4xx 는 사용자에게 아무 안내가 없었습니다.
toast.error(getApiErrorMessage(error))를 분기 밖으로 올렸습니다. status 분기는 추가 동작(리다이렉트 등)이 필요한 경우에만 남겼습니다ByLinkDialog— 5xx 는 전역 토스트가 안내하므로 인라인 helper text 는 4xx 만 표시합니다5. 링크 진입 실패에서 서버 오류 분리
5xx 를 "링크 만료" 로 안내하던 지점들을 갈랐습니다.
invite/[id]/page.tsx— 5xx·네트워크는throw해서 에러 바운더리(재시도 UI)로 넘기고, 409 만 만료 다이얼로그를 띄웁니다play/[id]—'error'상태를 추가해 만료 안내와 서버 오류 화면(다시 시도 / 홈으로)을 분리했습니다6. 토너먼트 참여(409) 를 code 로 분기
usePostJoin이 409 를 code 로 나눠 콜백을 호출합니다.TOURNAMENT-022(이미 참여) → 히스토리replace로 토너먼트 진입TOURNAMENT-030(인원 초과) →PARTICIPANTS_FULL다이얼로그 (신규 타입 추가)TournamentErrorDialog는type을TournamentErrorTypeT로 빼고 CONTENT 를Record로 타입 고정했습니다.7. 탈퇴 계정(
USER-003) 처리apis/client.ts— 409 +USER_DELETED면 Sentry user 해제 · 토큰 쿠키 삭제 · 웹뷰면 로그아웃 브릿지 전송 후 로그인으로 이동apis/server.ts— SSR 도 인터셉터에서redirect하도록 이관 (기존엔 호출부에 흩어져 있었음)action=withdrawn-account를 받아 카탈로그 문구를 토스트합니다8. 소셜 로그인 실패 code 전달
getLoginPath(redirect, action, errorCode)로 code 를 쿼리에 실어 로그인 페이지가OAUTH-*구분을 유지한 채 문구를 띄웁니다. 5xx 는 전역이 이미 토스트하므로 action 없이 이동해 중복을 막습니다.apps/app의postSocialLogin도detail대신 code 로 문구를 결정합니다.9.
app/error.tsx서버 code 가 실려오면 카탈로그 문구로 대체합니다 (
COMMON-SERVER-ERROR/RETRYABLE/SERVER-BUSY구분).10. 문서
docs/spec/error-handling-policy.md— 계층별 처리 주체,statusvscode분기 기준, 개발 체크리스트 갱신docs/spec/api-status-audit.md— 엔드포인트별 status 대응 현황 최신화CLAUDE.md— 응답 규약·에러 처리 표준 섹션을 현행 코드에 맞춤스크린샷
연관 이슈
closes #343
Summary by CodeRabbit
개선 사항
문서