Skip to content

refactor: 초대 진입 경로 통합 및 합류 에러 안내를 JoinErrorDialog 로 정리 - #436

Draft
iOdiO89 wants to merge 45 commits into
devfrom
feat/434-social-entry
Draft

refactor: 초대 진입 경로 통합 및 합류 에러 안내를 JoinErrorDialog 로 정리#436
iOdiO89 wants to merge 45 commits into
devfrom
feat/434-social-entry

Conversation

@iOdiO89

@iOdiO89 iOdiO89 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

작업 요약

  • 초대 링크·홈 코드 입력의 진입 경로를 /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 보존 — 앱 수정 불필요)
  • 링크 생성부(buildInviteUrlROUTES·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 로 닫히지 않는 종료 다이얼로그를 지원합니다 — 진입 실패 화면은 빈 배경 + 다이얼로그만 노출됩니다

에러 코드 분기

  • 409: TOURNAMENT-005(PENDING 아님) → 이미 시작 안내, TOURNAMENT-021(초대 만료) → 링크 만료 안내 — 링크 진입·코드 입력이 같은 기준으로 판정합니다
  • 400(코드 불일치)·코드-URL 불일치: INVALID_CODE 안내, CTA 는 홈 이동으로 통일해 빈 화면에 갇히지 않게 했습니다
  • TOURNAMENT-005 가 진행 중/완료를 한 코드로 덮는 문제는 서버 분리 후 후속 이슈에서 처리합니다

기타

  • 소셜 게스트 기본 이미지 S3 주소 변경
  • 문서 갱신: api-status-audit.md (by-invite-code·{id}/invite-preview 항목), ownership.md (/invite 행 제거)
  • 디자인 확인 대기 2건: 링크 진입 400 에 코드 입력용 문구가 노출되는 것, ALREADY_STARTED description 의 아이템 추가 맥락 문구

연관 이슈

closes #434

Summary by CodeRabbit

  • 새로운 기능

    • 초대 링크가 통합된 참여 경로로 연결되며, 만료·잘못된 코드·인원 초과 등 상황별 안내 화면을 제공합니다.
    • 위시 항목이 없을 때 위시 추가 화면으로 이동할 수 있는 안내를 제공합니다.
    • 플레이 링크 오류 시 재시도 및 홈 이동 기능을 제공합니다.
  • 버그 수정

    • API 오류 메시지를 오류 코드에 맞게 일관되게 표시합니다.
    • 탈퇴한 계정의 세션을 정리하고 로그인 화면으로 안내합니다.
    • 네트워크 오류와 서버 오류를 구분해 중복 알림을 줄였습니다.

iOdiO89 and others added 30 commits August 1, 2026 08:32
- 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) 반영
iOdiO89 added 15 commits August 4, 2026 10:45
충돌 해결:
- 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) 예외를 명시.
@iOdiO89 iOdiO89 self-assigned this Aug 4, 2026
@vercel

vercel Bot commented Aug 4, 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 4, 2026 9:29am

@iOdiO89 iOdiO89 changed the title draft: refactor: 초대 진입 경로 통합 및 합류 에러 안내를 JoinErrorDialog 로 정리 refactor: 초대 진입 경로 통합 및 합류 에러 안내를 JoinErrorDialog 로 정리 Aug 4, 2026
@iOdiO89
iOdiO89 marked this pull request as draft August 4, 2026 09:29
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

API 응답을 { data, code } 구조로 통합했습니다. 공통 오류 코드 카탈로그와 상태 기반 처리 유틸리티를 추가했습니다. 초대 참여 경로를 /tournament/join/[id]로 통합하고 오류 다이얼로그를 단일화했습니다. 여러 mutation의 오류 처리를 공통 규칙으로 변경했습니다.

Changes

API 오류 계약 및 공통 처리

Layer / File(s) Summary
오류 응답 계약과 메시지 카탈로그
packages/core/..., apps/web/src/types/api.ts, apps/web/src/utils/apiError.ts, apps/web/src/utils/getApiErrorMessage.ts, apps/web/e2e/...
응답에서 statusdetail을 제거했습니다. code 기반 타입, 메시지 카탈로그, fallback 메시지, E2E fixture를 추가했습니다.
전역 오류 처리와 인증 상태 연계
apps/web/src/apis/..., apps/web/src/utils/queryClient.ts, apps/web/src/utils/loginRedirect.ts, apps/web/src/app/login/..., apps/web/src/app/error.tsx
탈퇴 계정, 서버 오류, 네트워크 오류를 공통 처리합니다. 로그인 경로에 오류 코드를 전달하고 카탈로그 메시지를 표시합니다.
화면별 mutation 오류 처리 통합
apps/web/src/app/**/_hooks/*, apps/web/src/hooks/*, apps/web/src/components/get-item-dialog/ByLinkDialog.tsx
Axios 응답과 detail 직접 접근을 제거했습니다. 공통 오류 상태·메시지 유틸리티와 전역 네트워크 오류 규칙을 적용했습니다.

초대 참여 흐름

Layer / File(s) Summary
초대 참여 경로와 오류 UI 통합
apps/web/src/app/tournament/join/..., apps/web/src/components/common/join-error-dialog/..., apps/web/next.config.mjs, apps/web/src/consts/{api,route}.ts
초대 미리보기를 코드 기반 SSR 조회로 변경했습니다. 기존 /invite/:id 요청은 새 참여 경로로 리디렉션합니다. 참여 상태별 공통 오류 다이얼로그를 추가했습니다.
참여 요청 상태 처리
apps/web/src/app/tournament/join/_hooks/usePostJoin.ts, apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx
이미 참여함, 인원 초과, 링크 만료를 선택적 콜백으로 처리합니다. 참여 오류 상태와 재시도 흐름을 관리합니다.

문서 및 보조 변경

Layer / File(s) Summary
정책과 감사 문서 갱신
CLAUDE.md, docs/spec/*, apps/web/e2e/README.md
새 응답 구조와 status/code 우선 처리 규칙을 문서화했습니다. API 상태 감사 내용을 갱신했습니다.
보조 화면과 fixture 갱신
apps/web/e2e/fixtures/*, apps/web/src/app/tournament/[id]/create/..., apps/web/src/app/tournament/[id]/result/...
목 API 응답과 일부 토스트·레이아웃·이미지 경로를 갱신했습니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • TeamPiKi/client#432: 동일한 API 오류 코드 카탈로그와 { data, code } 응답 규약을 광범위하게 다룹니다.
  • TeamPiKi/client#412: 초대 진입 경로와 관련 UI를 함께 변경하며, 이 PR은 해당 흐름을 /tournament/join/[id]로 통합합니다.
  • TeamPiKi/client#319: E2E API 응답 helper와 mock fixture의 변경 지점이 직접 연결됩니다.

Suggested labels: refactor, feature, WEB

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 탈퇴 계정 처리와 다수의 일반 mutation 오류 처리 변경은 초대 진입 경로 요구사항과 직접 관련이 없습니다. 초대 진입 경로와 직접 관련 없는 탈퇴 계정 및 일반 오류 처리 변경을 별도 PR로 분리하거나 범위를 명시하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 초대 진입 경로 통합과 JoinErrorDialog 기반 오류 안내 정리를 정확히 요약합니다.
Linked Issues check ✅ Passed 단일 join 경로, 중복 조회 제거, 구경로 리다이렉트, 오류 다이얼로그 통합, 코드 분기 및 문서 갱신을 모두 반영했습니다.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/434-social-entry
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/434-social-entry

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: 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

선택 락 복구 없이 일반 라운드 실패 후 재대결이 가능하도록 변경하세요.

일반 라운드 onErrorsyncWithServer()setRemainingItems(...)만 호출합니다. VsSectionselectionEpoch를 콘스키드(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의 postFromPlayLinkPlayClient.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

PropsErrorProps로 변경하세요.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 28adc48 and 9bb7b5c.

⛔ Files ignored due to path filters (1)
  • apps/web/src/assets/images/sad-face.svg is excluded by !**/*.svg
📒 Files selected for processing (71)
  • CLAUDE.md
  • apps/app/apis/postSocialLogin.ts
  • apps/app/hooks/useSocialLogin.ts
  • apps/web/e2e/README.md
  • apps/web/e2e/fixtures/mockApiFixture.ts
  • apps/web/e2e/helpers/apiResponse.ts
  • apps/web/e2e/setup/mockApiServer.ts
  • apps/web/next.config.mjs
  • apps/web/src/apis/client.ts
  • apps/web/src/apis/getInvitePreviewByCode.ts
  • apps/web/src/apis/server.ts
  • apps/web/src/app/archive/wish/[id]/_hooks/useDeleteWish.ts
  • apps/web/src/app/archive/wish/[id]/_hooks/usePatchWish.ts
  • apps/web/src/app/auth/callback/[provider]/_hooks/usePostSocialLogin.ts
  • apps/web/src/app/error.tsx
  • apps/web/src/app/home/_components/InviteTournamentDialog.tsx
  • apps/web/src/app/home/_components/invite-code-dialog/InvalidCodeDialog.tsx
  • apps/web/src/app/home/page.tsx
  • apps/web/src/app/invite/[id]/_components/InviteInvalid.tsx
  • apps/web/src/app/invite/[id]/page.tsx
  • apps/web/src/app/login/_components/LoginButtons.tsx
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/mypage/edit/_hooks/usePatchMe.ts
  • apps/web/src/app/mypage/withdraw/_hooks/useDeleteMe.ts
  • apps/web/src/app/play/[id]/_components/PlayClient.tsx
  • apps/web/src/app/tournament/[id]/_common/_hooks/useDeleteTournamentItem.ts
  • apps/web/src/app/tournament/[id]/create/_components/invite-friends/InviteFriendsDialog.tsx
  • apps/web/src/app/tournament/[id]/create/_components/participant-panel/ParticipantPanel.tsx
  • apps/web/src/app/tournament/[id]/create/_hooks/usePatchInviteExpiry.ts
  • apps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentItemLink.ts
  • apps/web/src/app/tournament/[id]/create/_hooks/usePostTournamentStart.ts
  • apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx
  • apps/web/src/app/tournament/[id]/create/by-wish/_components/NoWishDialog.tsx
  • apps/web/src/app/tournament/[id]/create/by-wish/_hooks/usePostTournamentItemsByWish.ts
  • apps/web/src/app/tournament/[id]/item/[itemId]/_hooks/usePatchTournamentItem.ts
  • apps/web/src/app/tournament/[id]/match/_hooks/usePostRecordMatch.ts
  • apps/web/src/app/tournament/[id]/match/_hooks/useTournament.ts
  • apps/web/src/app/tournament/[id]/result/_components/group-result-entry-card/GroupResultEntryCard.tsx
  • apps/web/src/app/tournament/[id]/result/_components/plate-share-dialog/PlateShareDialog.tsx
  • apps/web/src/app/tournament/[id]/result/_hooks/usePostPlayLink.ts
  • apps/web/src/app/tournament/join/[id]/_components/JoinErrorScreen.tsx
  • apps/web/src/app/tournament/join/[id]/_components/JoinPreviewClient.tsx
  • apps/web/src/app/tournament/join/[id]/page.tsx
  • apps/web/src/app/tournament/join/_apis/getInvitePreview.ts
  • apps/web/src/app/tournament/join/_hooks/useGetInvitePreview.ts
  • apps/web/src/app/tournament/join/_hooks/usePostJoin.ts
  • apps/web/src/components/common/join-error-dialog/index.tsx
  • apps/web/src/components/common/join-error-dialog/joinErrorDialog.const.ts
  • apps/web/src/components/get-item-dialog/ByLinkDialog.tsx
  • apps/web/src/components/tournament-error-dialog/index.tsx
  • apps/web/src/consts/api.ts
  • apps/web/src/consts/queryAction.ts
  • apps/web/src/consts/route.ts
  • apps/web/src/hooks/useNativeLoginResult.ts
  • apps/web/src/hooks/useNicknameValidation.ts
  • apps/web/src/hooks/usePostTournamentOCR.ts
  • apps/web/src/hooks/usePostWishLink.ts
  • apps/web/src/hooks/usePostWishOCR.ts
  • apps/web/src/types/api.ts
  • apps/web/src/utils/apiError.ts
  • apps/web/src/utils/getApiErrorMessage.ts
  • apps/web/src/utils/getRouteType.ts
  • apps/web/src/utils/loginRedirect.ts
  • apps/web/src/utils/queryClient.ts
  • docs/spec/api-status-audit.md
  • docs/spec/error-handling-policy.md
  • docs/spec/ownership.md
  • packages/core/src/consts/errorCode.ts
  • packages/core/src/index.ts
  • packages/core/src/types/error.ts
  • packages/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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

2xx 응답의 data를 실제로 검증하세요.

현재 타입은 모든 JSON 응답에서 dataSocialLoginSuccessPayloadT라고 선언합니다. 그러나 공통 계약의 실패 응답은 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.

Comment thread apps/web/e2e/README.md
```

경로는 항상 `@/consts/api` 의 `ENDPOINTS` 상수를 쓰세요. 응답은 팀 규약 `{ status, data, detail, code }` 로 자동 래핑되므로 **data 안에 들어갈 내용만** 넘기면 됩니다.
경로는 항상 `@/consts/api` 의 `ENDPOINTS` 상수를 쓰세요. 응답은 팀 규약 `{ status, data, code }` 로 자동 래핑되므로 **data 안에 들어갈 내용만** 넘기면 됩니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

E2E 응답 본문 예시에서 status를 제거하세요.

createApiSuccesscreateApiError{ 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.

Suggested change
경로는 항상 `@/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.

Comment on lines +34 to +41
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));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
<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

Comment on lines +127 to +131
<Link href={ROUTES.HOME} className="w-full">
<Button size="lg" variant="secondary" className="w-full">
홈으로 가기
</Button>
</Link>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

LinkButton을 중첩하지 마세요.

현재 구조는 <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

Comment on lines +158 to +160
{joinErrorType && (
<JoinErrorDialog type={joinErrorType} open onOpenChange={() => setJoinErrorType(null)} />
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

[P2] 참여 불가 상태를 닫아 재시도 상태로 바꾸지 마세요.

Escape 또는 dim 클릭은 onOpenChange를 통해 joinErrorTypenull로 만듭니다. 회원 화면은 이후 남아 있는 isPostJoinError 때문에 retryable UI를 표시합니다. 게스트 화면은 만료되었거나 정원이 찬 링크를 다시 제출할 수 있습니다.

이 경로의 PARTICIPANTS_FULLLINK_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에서 동작이 갈릴 때만.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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 만 추가 동작
> }
> ```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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 파싱만 담당한다.

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

코드 블록에 언어 식별자를 추가하세요.

Line 249의 fenced code block에 언어 식별자가 없어 markdownlint MD040을 발생시킵니다. 디렉터리 트리에는 text를 사용하세요.

제안 수정
-```
+```text

Based 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.

Suggested change
```
🧰 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

Comment on lines +10 to +14
export const getErrorMessageByCode = (code: ApiErrorCodeT | null | undefined): string | null => {
if (!code) return null;

return (ERROR_MESSAGE_MAP as Record<string, string>)[code] ?? null;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: 상세 에러 코드 도입에 맞춰 소셜 진입 흐름 에러 처리 정리

1 participant