Skip to content

refactor: 위시 및 토너먼트 준비 UX 개선#127

Merged
iOdiO89 merged 27 commits into
devfrom
feat/wish
Jun 2, 2026
Merged

refactor: 위시 및 토너먼트 준비 UX 개선#127
iOdiO89 merged 27 commits into
devfrom
feat/wish

Conversation

@iOdiO89

@iOdiO89 iOdiO89 commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

작업 요약

  • 위시리스트에 링크·이미지 등록·다중 삭제·수정 API를 연동
  • PROCESSING 상태 폴링과 sonner 토스트로 피드백을 통일
  • 토너먼트 준비 페이지는 장바구니 carousel·실패 아이템 처리·위시에서 후보 추가 플로우를 보강
  • 웹뷰 환경에서는 로그인 토큰을 쿠키에 저장해 API 인증이 동작하도록 함

작업 세부 내용

공통

  • Toast: sonner 기반 Toaster로 통일 (ActionSnackbar, SuccessToast, 구 Toast 제거)
  • ByLinkDialog: type에 따라 위시 / 토너먼트 후보 분기, 위시 성공 시 toast + /wishlist 이동
  • Carousel: embla-carousel 기반 공통 Carousel 컴포넌트 추가 (토너먼트 장바구니 등에서 사용)
  • 레이아웃: root layout에서 px, 페이지 배경색 제거 → 각 페이지가 레이아웃·배경 담당 (max-w-120 래퍼는 유지)
  • 이미지 OCR 엔드포인트: WISH_OCR/api/v1/wishlists/images로 수정

/login

  • API 클라이언트: 웹뷰일 때 쿠키 access_tokenAuthorization 헤더, X-Client-Type (app / web) 설정
  • 게스트·회원 로그인 성공 시 웹뷰access_token, refresh_token쿠키에 직접 저장
  • postGuestLogin 응답에 토큰 필드 반영

/wishlist

  • 타입: WishItemT.imageUrloptionalstring | null로 정리
  • 링크로 위시 등록: postWishLink API + usePostWishLink 훅 추가
  • 다중 삭제: deleteWishes API + 삭제 모드 UI (선택 삭제, 삭제 중 FAB에 Spinner)
  • PROCESSING 위시가 있으면 목록 5초마다 refetch
  • wish-grid 관련 컴포넌트를 wishlist/_components/wish-grid/colocation 이동
  • WishProcessingCard 추가 (담는 중 UI)
  • 위시 타입·목 데이터 구조 정리 (wishTypeswish, mock 정리)
  • 삭제/링크 담기 성공 시 toast 메시지

/wishlist/[id]

  • 위시 수정 API 요청을 FormData로 변경 (patchWish)
  • 수정 API pending 시 CTA에 Spinner 표시
  • imageUrl 타입 string | null 반영

/tournament/[id]/create

  • create 전용 컴포넌트·에셋·API를 해당 라우트 폴더로 이동 (colocation)
  • 후보 장바구니 carousel을 데스크탑에서도 슬라이드 가능하도록 + 화면 너비에 맞춘 반응형 슬라이드로 리팩터
  • 장바구니 레이아웃·인디케이터 위치 조정 (장바구니 이미지 바로 아래 뜨도록)
  • 아이템 개수 증가 시 마지막 아이템이 있는 바구니로 자동 스크롤 (기능 미완..)
  • 실패(FAILED) 아이템 탭 시 다이얼로그 + 삭제 API 연동 (deleteTournamentItem)

/tournament/[id]/create/by-wish

  • 위시로 토너먼트 후보 추가 onSuccess 보강: 쿼리 invalidate 후 /create로 이동

/tournament/[id]/item/[itemId]

  • ItemEditFormimageUrl string | null 타입 정리 (위시 수정과 동일 패턴)

Summary by CodeRabbit

  • 새로운 기능

    • 링크로 위시 추가 및 관련 후처리 UI/훅 도입
    • Embla 기반 캐러셀과 토너먼트 바구니 슬라이드 도입
    • 위시 처리 중 상태를 보여주는 카드 추가
  • 개선 사항

    • 로그인/토큰 쿠키 처리 및 관련 흐름 정비
    • 다중 위시 삭제 UX 및 로딩 상태 개선
    • 페이지 전반 레이아웃·패딩 조정, 자동 갱신 로직 도입
  • 기타

    • 토스트/스낵바 관련 출력 방식 및 일부 토스트 컴포넌트 변경, OCR 엔드포인트 경로 수정

iOdiO89 and others added 24 commits June 1, 2026 21:55
@iOdiO89 iOdiO89 self-assigned this Jun 1, 2026
@vercel

vercel Bot commented Jun 1, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
depromeet Ready Ready Preview, Comment Jun 2, 2026 4:35am

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

No description provided.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0e6f1f6d-0258-4b6d-8991-ab8b85657079

📥 Commits

Reviewing files that changed from the base of the PR and between e89636c and 2e57957.

📒 Files selected for processing (1)
  • apps/web/src/app/tournament/[id]/match/_hooks/useTournament.ts

Walkthrough

웹뷰 토큰 저장/요청 인터셉터 추가, 위시 링크·이미지 업로드 및 삭제 API/훅 추가, Embla 기반 캐러셀 도입으로 토너먼트 바구니 리팩토링, 위시 타입·그리드·토스트 및 레이아웃 스타일 정리 변경입니다.

Changes

웹뷰 토큰 관리 및 API 인증

Layer / File(s) Summary
클라이언트 API 인터셉터 및 토큰 처리
apps/web/src/apis/client.ts
요청 인터셉터에서 웹뷰 판별, 쿠키의 access_token을 Authorization 헤더로 자동 주입, 모든 요청에 X-Client-Type 헤더 설정. 토큰 리프레시 실패 시 /login 리다이렉트 주석 처리(제거).
로그인 훅에서 쿠키 기반 토큰 저장
apps/web/src/app/login/_hooks/*, apps/web/src/app/login/_components/LoginButton.tsx
usePostGuestLogin/usePostMemberLogin에서 웹뷰 환경일 때 access/refresh 토큰을 setCookie로 저장하고 훅 반환명을 post*/isPost*Pending 형태로 변경. LoginButton에서 새 반환명을 사용.
게스트 로그인에서 X-Client-Type 헤더 제거
apps/web/src/app/login/_apis/postGuestLogin.ts
postGuestLogin 요청에서 개별 X-Client-Type 헤더 전달 제거.

위시 링크 제출, 이미지 업로드, 삭제 기능

Layer / File(s) Summary
위시 링크 제출 API 및 훅
apps/web/src/apis/postWishLink.ts, apps/web/src/hooks/usePostWishLink.ts, apps/web/src/types/wish.ts
postWishLink API 추가 및 usePostWishLink 훅 구현. 성공 시 ['wishlists'] 쿼리 무효화 후 /wishlist로 라우팅, 응답 타입 PostWishLinkResponseT 추가.
위시 이미지 업로드 및 PATCH 요청
apps/web/src/app/wishlist/[id]/_apis/patchWish.ts, apps/web/src/app/wishlist/[id]/_hooks/usePatchWish.ts, apps/web/src/app/wishlist/[id]/_types/wish.ts, apps/web/src/app/wishlist/[id]/_components/ItemEditForm.tsx
patchWish가 FormData를 받도록 변경. usePatchWish에서 선택 필드를 FormData로 구성해 전송. 타입에서 image?: File로 교체, 저장 버튼 로딩 UI 적용.
위시 배치 삭제 및 쿼리 무효화
apps/web/src/app/wishlist/_apis/deleteWishes.ts, apps/web/src/app/wishlist/_hooks/useDeleteWishes.ts, apps/web/src/app/wishlist/_components/WishlistContent.tsx, apps/web/src/app/wishlist/_components/WishlistFabArea.tsx
deleteWishes API 추가 및 useDeleteWishes 훅으로 배치 삭제 구현. 성공 시 관련 쿼리 무효화, 삭제 진행 상태 노출 및 버튼 스피너 처리.
위시리스트 자동 갱신 폴링
apps/web/src/app/wishlist/_hooks/useGetWishlist.ts
쿼리 데이터에 PROCESSING 상태가 있으면 5초마다 자동 리패치, 없으면 폴링 중단.
링크 다이얼로그 위시 분기 처리
apps/web/src/components/common/get-item-dialog/ByLinkDialog.tsx
ByLinkDialog가 wish 타입일 때 postWishLinkMutation을 호출하고 onSettled/onSuccess로 모달 종료·라우팅·토스트를 처리.
위시 타입 정의 통합
apps/web/src/app/wishlist/_types/wish.ts, apps/web/src/app/wishlist/_types/wishTypes.ts, apps/web/src/app/wishlist/_apis/getWishlist.ts
위시 타입을 _types/wish.ts로 통합하고 매핑 로직의 status 처리 및 imageUrl 기본값을 표준화.
위시그리드 상태 분기 및 카드 렌더링
apps/web/src/app/wishlist/_components/wish-grid/*
WishGrid 상태 분기(FAILED/PROCESSING) 확장, WishProcessingCard 추가, 카드 레이아웃을 aspect 기반으로 변경하고 Link 래핑 제거.
위시 페이지 PROCESSING 상태 리다이렉트
apps/web/src/app/wishlist/[id]/page.tsx, apps/web/src/app/tournament/[id]/item/[itemId]/page.tsx
PROCESSING 상태 접근 시 alert 제거하고 즉시 지정 페이지로 리다이렉트.

Embla 캐러셀 컴포넌트 및 토너먼트 바구니 리팩토링

Layer / File(s) Summary
Embla 기반 캐러셀 컴포넌트 구현
apps/web/src/components/carousel/index.tsx
범용 Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext, useCarousel, CarouselApi 타입 추가. Embla( useEmblaCarousel )로 scroll/select/reInit 관리 및 키보드 접근성 처리.
토너먼트 바구니 캐러셀 Embla 마이그레이션
apps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/TournamentItemBasketCarousel.tsx
기존 수동 translateX 핸들링 제거, Carousel API로 슬라이드 구성. activeBasketCount 계산, ITEMS_PER_BASKET 단위 슬라이스, 초기 reInit/스냅 보정, 아이템 증가 시 마지막 바구니 자동 스크롤.
바구니 상수 및 유틸함수 재구성
apps/web/src/app/tournament/[id]/create/_consts/tournamentItemBasket.ts, apps/web/src/app/tournament/[id]/create/_utils/tournamentItemBasket.ts
ITEMS_PER_BASKET/BASKET_COUNT/슬라이드 퍼센트 및 EMPTY_BASKET_IMAGES 추가. getActiveBasketCount/getBasketIndexForLastItem 유틸 추가. 기존 상수 모듈에서 제거.
바구니 아이템 클릭 및 실패 모달 처리
apps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/*
FAILED 아이템 클릭 시 failedItem 상태로 모달에 tournamentId/tournamentItemId 전달. TournamentItemFailedModal props 확장 및 삭제/편집 핸들러 재구성(useDeleteTournamentItem 사용).
바구니 그리드 및 임시 슬롯 레이아웃
apps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/*, apps/web/src/app/tournament/[id]/create/_components/product-image/*
그리드 클래스 변경 및 EmptyBasketSlot 고정 68px 크기, ProductImage sm dimension 72→68으로 조정.
토너먼트 생성 페이지 레이아웃 재구성
apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx, apps/web/src/app/tournament/[id]/create/page.tsx
상위 레이아웃 클래스 재구성(h-dvh, bg-bg-layer-basement, pt-20 pb-8) 및 import 경로 정리.

페이지 레이아웃 및 토스트 정리

Layer / File(s) Summary
페이지 좌우 패딩 및 배경 표준화
apps/web/src/app/home/page.tsx, apps/web/src/app/login/page.tsx, apps/web/src/app/page.tsx, apps/web/src/app/wishlist/_components/WishlistLayout.tsx, apps/web/src/app/tournament/[id]/item/[itemId]/_components/EditContent.tsx
여러 페이지 최상위 컨테이너에 px-5 및 일부에 bg-white 추가로 레이아웃 일관화.
레이아웃 레벨 Toaster 제거 및 단순화
apps/web/src/app/layout.tsx
Toaster import/렌더 제거, Providers 내 children 래퍼 className에서 배경/가로 패딩 제거(스크롤/세이프 영역 유지).
toast 컴포넌트 정리 및 Toaster 시그니처 변경
apps/web/src/components/common/toast/*
ActionSnackbar/SuccessToast/Toast 파일 제거 및 Toaster 시그니처에서 mobileOffset 분리 제거, Sonner의 mobileOffset 병합 로직 삭제.
API 엔드포인트 및 설정 업데이트
apps/web/next.config.js, apps/web/package.json, apps/web/src/consts/api.ts
next.config.js에 allowedDevOrigins: [] 추가(주석 포함). shadcn 버전 ^4.8.0 → ^4.9.0. ENDPOINTS.WISH_OCR 경로를 /api/v1/wishlists/images로 변경.

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

Possibly related PRs

Suggested reviewers

  • kanghaeun
  • ychany
  • soyeong0115

"나는 바쁜 토끼, 코드 풀리퀘스트 속에,
쿠키와 토큰을 당근처럼 쌓고,
위시 링크 한 줌 던지면 저장하러 뛰고,
캐러셀 위에서 폴짝폴짝 춤추네 🐇✨
잘됐어요, 리뷰도 맛있게!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 변경 사항의 핵심인 위시리스트 및 토너먼트 준비 단계의 UX 개선을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/wish

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 and usage tips.

@github-actions github-actions Bot added the refactor Extra attention is needed label Jun 1, 2026
@github-actions
github-actions Bot requested a review from kanghaeun June 1, 2026 16:33

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/src/app/wishlist/[id]/_components/ItemEditForm.tsx (1)

107-114: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

저장 중에는 버튼도 비활성화해 주세요.

지금은 로딩 스피너만 바뀌고 disabled에는 isPatchWishPending가 반영되지 않아, 저장 중에도 CTA가 계속 눌립니다. handleSave에서 early return 하더라도 UI 상태와 실제 동작이 어긋납니다.

제안 코드
         <Button
           variant="primary"
           size="lg"
           onClick={handleSave}
-          disabled={isDeleteWishPending || !isValid}
+          disabled={isDeleteWishPending || isPatchWishPending || !isValid}
           className="flex-1"
         >
           {isPatchWishPending ? <Spinner size={20} /> : '저장하기'}
         </Button>
🤖 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/wishlist/`[id]/_components/ItemEditForm.tsx around lines 107
- 114, The save Button currently only disables based on isDeleteWishPending and
isValid, so during a patch operation the spinner shows but the CTA remains
clickable; update the Button's disabled prop to also include isPatchWishPending
(e.g., disabled={isDeleteWishPending || isPatchWishPending || !isValid}) and
ensure any other click handlers like handleSave still rely on the same pending
flag(s) to prevent concurrent submits; locate the Button rendering inside
ItemEditForm and the isPatchWishPending state/prop to make this change.
apps/web/src/app/wishlist/_components/wish-grid/index.tsx (1)

22-46: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

READY 카드 분기에서도 map() 콜백의 top-level에 key가 필요합니다.
지금은 return <>{card}</>;로 fragment를 top-level로 반환하고 있어 key가 없어 React 경고가 나고, 폴링/리렌더 시 항목 재사용이 불안정해질 수 있습니다.

🧩 수정 예시
-        const card = <WishCard name={item.name} price={item.price} imageUrl={item.imageUrl} />;
+        const card = (
+          <WishCard
+            key={item.id}
+            name={item.name}
+            price={item.price}
+            imageUrl={item.imageUrl}
+          />
+        );
@@
-        return <>{card}</>;
+        return card;
🤖 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/wishlist/_components/wish-grid/index.tsx` around lines 22 -
46, The READY branch returns a top-level fragment without a key, causing React
list key warnings and unstable re-use; modify the non-delete-mode return so the
top-level element has a key (e.g., replace the fragment return with a keyed
wrapper such as a React.Fragment with key={item.id} or a div/span keyed by
item.id) so that the mapping callback for WishCard (item) always provides a
stable key; update the return that currently yields <> {card} </> to return a
keyed element using item.id.
🧹 Nitpick comments (7)
apps/web/src/app/design-system/image/_components/ProductImageSection.tsx (1)

5-5: ⚡ Quick win

공용 섹션에서 라우트 전용 _components 직접 참조를 피해주세요.

design-system 컴포넌트가 app/tournament/[id]/create/_components를 직접 import하면
페이지 colocation 경계가 깨집니다. 공용으로 쓰는 ProductImagecomponents/common (또는
공용 API 레이어)에서 가져오도록 유지하는 편이 안전합니다.

As per coding guidelines, "apps/web/src/app/**/_components/**/*.tsx: Store
single-page-exclusive components in the page's own _components/ folder" and
"apps/web/src/components/common/**/*.tsx: For components shared across top-level App Router routes or globally used UI, place in components/common/{component-name}/ folder."

🤖 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/design-system/image/_components/ProductImageSection.tsx` at
line 5, ProductImageSection.tsx is importing ProductImage directly from a
page-local _components folder which breaks colocation rules; update the
reference to use the shared/common component instead: ensure ProductImage is
exported from the shared components layer (e.g.,
components/common/product-image) or add a re-export there, then change the
import in ProductImageSection (the ProductImage symbol) to import from that
common module so the design-system only depends on shared UI, not page-specific
_components.
apps/web/src/app/tournament/[id]/create/by-wish/_hooks/usePostTournamentItemsByWish.ts (1)

16-18: ⚡ Quick win

쿼리 무효화 완료(필요 시 active 기준)까지 대기 후 이동하는 게 더 안전합니다.

TanStack Query v5에서 queryClient.invalidateQueries()는 Promise를 반환하므로 await하면(기본적으로 active query refetch 완료 후) 네비게이션 순간에 stale UI가 잠깐 보일 가능성을 줄일 수 있습니다. 단, 대기 시간만큼 이동이 느려질 수 있으니 UX 요구에 맞게 적용하세요.

🔧 Suggested fix
-    onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ['tournament', tournamentId] });
+    onSuccess: async () => {
+      await queryClient.invalidateQueries({ queryKey: ['tournament', tournamentId] });
       router.push(`/tournament/${tournamentId}/create`);
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/tournament/`[id]/create/by-wish/_hooks/usePostTournamentItemsByWish.ts
around lines 16 - 18, The onSuccess handler should await
queryClient.invalidateQueries to ensure invalidation (and any active refetches)
completes before navigation; make the onSuccess function async, call await
queryClient.invalidateQueries({ queryKey: ['tournament', tournamentId] }) and
only after that call router.push(`/tournament/${tournamentId}/create`) so stale
UI is less likely to flash during navigation (keep the same queryKey and
router.push usage).
apps/web/src/app/wishlist/_components/WishTab.tsx (1)

7-7: ⚡ Quick win

상대 경로 import 대신 alias import를 써주세요.

이 파일도 WishTabT를 상대 경로로 가져오고 있어서 apps/web/src 전역 import 규칙과 어긋납니다.

As per coding guidelines, apps/web/src/**/*.{ts,tsx}: Use absolute path alias @/* for imports instead of relative paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/wishlist/_components/WishTab.tsx` at line 7, The import in
WishTab.tsx is using a relative path for the WishTabT type; update the import to
use the project alias (e.g., `@/`...) instead of the relative '../_types/wish' so
it conforms to the apps/web/src alias rule; change the import statement that
references WishTabT to use the absolute alias path and ensure any TypeScript
path mappings remain valid.
apps/web/src/app/wishlist/page.tsx (1)

11-11: ⚡ Quick win

절대 경로 alias로 통일해주세요.

여기서만 상대 경로를 쓰면 이 파일의 import 규칙이 다시 섞입니다. @/app/wishlist/_types/wish처럼 절대 경로로 맞추는 편이 좋겠습니다.

As per coding guidelines, apps/web/src/**/*.{ts,tsx}: Use absolute path alias @/* for imports instead of relative paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/wishlist/page.tsx` at line 11, The import in page.tsx uses a
relative path for WishTabT; change it to the project absolute alias form by
importing WishTabT from '`@/app/wishlist/_types/wish`' so the file follows the
codebase rule to use the `@/`* alias (update the import statement that currently
references './_types/wish' to use the '`@/app/wishlist/_types/wish`' path).
apps/web/src/app/wishlist/_apis/getWishlist.ts (1)

8-8: ⚡ Quick win

이 타입 import도 @/* alias로 바꿔주세요.

apps/web/src 내부 파일인데 상대 경로를 다시 도입하고 있습니다. 같은 PR의 다른 import 스타일과도 일관되지 않습니다.

As per coding guidelines, apps/web/src/**/*.{ts,tsx}: Use absolute path alias @/* for imports instead of relative paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/wishlist/_apis/getWishlist.ts` at line 8, The import in
getWishlist.ts uses a relative path for the WishlistEntryT type; replace that
relative import with the project alias form (use the '`@/`...' alias) so it
matches the other imports in this PR and the repo guideline. Update the import
statement that references WishlistEntryT to use the `@/`* alias form consistent
with the codebase.
apps/web/src/app/wishlist/_components/WishlistTabContent.tsx (1)

4-4: ⚡ Quick win

여기도 import 스타일이 섞였습니다.

바로 위 줄은 @/* alias를 쓰고 있는데 WishItemT만 상대 경로라서 일관성이 깨집니다. 같은 alias 규칙으로 맞춰주세요.

As per coding guidelines, apps/web/src/**/*.{ts,tsx}: Use absolute path alias @/* for imports instead of relative paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/wishlist/_components/WishlistTabContent.tsx` at line 4, The
import for WishItemT in WishlistTabContent.tsx uses a relative path which breaks
the project's import alias convention; update the import of WishItemT to use the
project's absolute alias (`@/`...) instead of '../_types/wish' so it matches the
surrounding `@/`* imports and adheres to the rule for apps/web/*.ts(x) files
(refer to symbol WishItemT in WishlistTabContent.tsx to locate the import).
apps/web/src/app/wishlist/_components/wish-grid/index.tsx (1)

5-6: ⚡ Quick win

상대 경로 import는 @/ 별칭으로 통일해 주세요.

이 파일만 상대 경로를 두면 apps/web/src 하위 import 규칙이 다시 흔들립니다.

♻️ 정리 예시
-import type { WishItemT } from '../../_types/wish';
-import WishProcessingCard from './WishProcessingCard';
+import type { WishItemT } from '`@/app/wishlist/_types/wish`';
+import WishProcessingCard from '`@/app/wishlist/_components/wish-grid/WishProcessingCard`';

As per coding guidelines, "Use absolute path alias @/* for imports instead of relative paths."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/wishlist/_components/wish-grid/index.tsx` around lines 5 -
6, Replace the relative imports with the project alias: change the import of
WishItemT and WishProcessingCard to use the '`@/`...' absolute alias (referencing
the symbols WishItemT and WishProcessingCard in this file) so all imports under
apps/web/src use the `@/` path convention consistently; update the import paths
accordingly and ensure any related type/component references still resolve with
the alias.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/app/login/_hooks/usePostMemberLogin.ts`:
- Line 28: Remove the stray console.log in usePostMemberLogin (the
console.log(data) inside the usePostMemberLogin hook); delete that debug
statement and, if you need to keep a runtime notice, replace it with the project
logger (e.g., processLogger.warn or processLogger.error) or appropriate error
handling inside the usePostMemberLogin function so lint rules and repository
guidelines are respected.
- Around line 18-23: The code saves guest tokens to cookies before calling
postMemberLogin, which causes the request to be sent with guest Authorization
via the HTTP interceptor; change the flow so that when isWebview() and
guestData.accessToken/refreshToken exist you do NOT call setCookie there,
instead keep guest tokens only in memory and call postMemberLogin(randomId)
first, then on successful member login update cookies via
setCookie('access_token', ...) and setCookie('refresh_token', ...); update the
logic in usePostMemberLogin to move setCookie calls to the post-login success
handler (and ensure postMemberLogin and the interceptor in apis client use the
fresh member token thereafter).

In `@apps/web/src/app/tournament/`[id]/_common/_hooks/useDeleteTournamentItem.ts:
- Around line 42-45: The branch in useDeleteTournamentItem that handles status
=== 403 || 404 || 409 currently only shows a toast and conditionally
router.replace, leaving the UI stale when the user is already on
tournamentCreatePage; update that branch to also re-sync state by invalidating
the tournament query or refreshing the router: call
queryClient.invalidateQueries(['tournament', tournamentId]) (or
router.refresh()) after showing the toast (or in the pathname ===
tournamentCreatePage case) so the modal/basket reflects the server-side change;
reference useDeleteTournamentItem, tournamentId, queryClient.invalidateQueries,
and router.refresh when making this change.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournamentItemBasket/TournamentBasketItem.tsx:
- Around line 14-17: The className expression passes a boolean when item.status
=== 'READY' so 'cursor-pointer' is not added; update the conditional in the cn
call (where className uses cn and references item.status) to return the string
'cursor-pointer' when item.status is 'READY' or 'FAILED' (e.g., use a ternary or
explicit boolean check like (item.status === 'READY' || item.status ===
'FAILED') ? 'cursor-pointer' : '') so the class is actually applied to the
clickable element.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournamentItemBasket/TournamentItemBasketCarousel.tsx:
- Around line 87-90: When changing activeBasketCount you must clear the parent
carouselApi before Carousel is remounted to avoid briefly referencing the old
API; add an effect (preferably useLayoutEffect) that watches activeBasketCount
and calls setCarouselApi(undefined) so carouselApi is reset prior to the new
Carousel instance mounting, then continue to pass setApi={setCarouselApi} to the
Carousel; reference the Carousel component, the activeBasketCount key, the
carouselApi state, and setCarouselApi when making this change.
- Around line 31-46: The effect currently treats any increase in items.length
(including initial fetch 0->N) as a user-add and forces carouselApi.scrollTo;
add an initialization guard so the first population is ignored: create a ref
like initialLoadRef (useRef(true)) and in the useEffect (the one referencing
prevItemCountRef, isCarouselEnabled, carouselApi, getBasketIndexForLastItem)
skip the scroll when initialLoadRef.current is true and items.length >
prevCount, then set initialLoadRef.current = false; keep updating
prevItemCountRef.current as before so subsequent genuine additions trigger
carouselApi.scrollTo.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournamentItemBasket/TournamentItemFailedDrawer.tsx:
- Around line 31-37: The delete handler allows duplicate DELETE requests because
it lacks a pending guard; update useDeleteTournamentItem to return the pending
flag (e.g., isDeleteTournamentItemPending) and in TournamentItemFailedDrawer’s
handleDeleteTournamentItem check that flag at the start and return early if
true, and also pass that flag to the delete button/trigger to disable it while
pending; apply the same pattern to the other handler referenced (lines 57–65) so
both the UI and handler prevent rapid double submits.

In
`@apps/web/src/app/tournament/`[id]/create/by-wish/_components/WishSelectCard.tsx:
- Around line 15-20: The root button in WishSelectCard (the element with props
onClick={onSelect} and aria-pressed={isSelected}) is missing the Tailwind
cursor-pointer class; update the button's className to include "cursor-pointer"
(or apply the common Button cva used across the app) so clickable elements show
the pointer cursor and conform to the coding guideline.

In `@apps/web/src/app/wishlist/_components/wish-grid/WishCard.tsx`:
- Around line 15-17: The Image usage in the WishCard component is missing a
sizes attribute when using fill, causing next/image to assume 100vw and select
oversized variants; update the <Image> element in WishCard (where imageUrl,
name, and fill are used) to include an appropriate responsive sizes string that
reflects the card's rendered width across breakpoints (e.g., a mobile 100vw and
desktop fraction like 25vw) so the CDN serves correctly sized images and
bandwidth is reduced.

In `@apps/web/src/app/wishlist/_hooks/useDeleteWishes.ts`:
- Around line 24-28: The onError handler in useDeleteWishes currently returns
early after checking isAxiosError<ApiErrorResponseT> and error.response, so
failures are silently ignored; update this onError to show a user-facing error
toast using the app's notification utility (e.g., toast.error or existing
notifier) and include the server message if present (use
error.response.data?.message) or fallback to error.message; keep the existing
type check (isAxiosError<ApiErrorResponseT>) and call the notifier inside the
same onError block so delete failures surface to users.
- Around line 15-16: The delete flow currently resets UI state immediately
because handleConfirmDelete calls deleteWishes via the mutation without awaiting
its completion and the useMutation onError is unimplemented; change to use the
mutation's async API (call deleteWishesMutation.mutateAsync or await the
returned promise) so you only clear selectedIds and exit isDeleteMode after the
mutation resolves successfully, and implement onError in useMutation to restore
selectedIds/isDeleteMode and surface an error (e.g., set an error toast or
state) when deleteWishes fails; reference functions/vars: handleConfirmDelete,
deleteWishesMutation (mutate vs mutateAsync), isDeleteWishesPending, and the
onError handler to locate the spots to change.

In `@apps/web/src/components/carousel/index.tsx`:
- Around line 161-180: The carousel navigation buttons (CarouselPrevious and the
corresponding CarouselNext) currently render plain <button> elements that
default to type="submit" inside forms; update both components (CarouselPrevious
and CarouselNext) to explicitly set type="button" on their <button> elements so
clicks don’t trigger unintended form submissions, keeping all existing props,
disabled handling, onClick (scrollPrev/scrollNext), and accessibility text
unchanged.
- Around line 74-84: handleKeyDown currently only maps ArrowLeft/ArrowRight so
vertical carousels cannot be keyboard-navigated; update handleKeyDown (the
React.useCallback that references scrollPrev and scrollNext) to also handle
ArrowUp and ArrowDown when orientation === 'vertical' (or accept both mappings
regardless of orientation) by mapping ArrowUp -> scrollPrev and ArrowDown ->
scrollNext and calling event.preventDefault() the same way as for left/right.

In `@apps/web/src/components/common/get-item-dialog/ByLinkDialog.tsx`:
- Around line 49-67: The dialog cleanup currently runs in the onSettled handler
so failures also close the modal and clear input; move the cleanup calls
(onOpenChange(false) and resetState()) from onSettled into onSuccess for both
postWishLinkMutation and postTournamentItemLinkMutation so the modal and URL are
only cleared when the request succeeds; keep router.push('/wishlist') and
toast.success(...) in postWishLinkMutation.onSuccess as-is and leave onSettled
for any non-cleanup side effects if needed.

---

Outside diff comments:
In `@apps/web/src/app/wishlist/_components/wish-grid/index.tsx`:
- Around line 22-46: The READY branch returns a top-level fragment without a
key, causing React list key warnings and unstable re-use; modify the
non-delete-mode return so the top-level element has a key (e.g., replace the
fragment return with a keyed wrapper such as a React.Fragment with key={item.id}
or a div/span keyed by item.id) so that the mapping callback for WishCard (item)
always provides a stable key; update the return that currently yields <> {card}
</> to return a keyed element using item.id.

In `@apps/web/src/app/wishlist/`[id]/_components/ItemEditForm.tsx:
- Around line 107-114: The save Button currently only disables based on
isDeleteWishPending and isValid, so during a patch operation the spinner shows
but the CTA remains clickable; update the Button's disabled prop to also include
isPatchWishPending (e.g., disabled={isDeleteWishPending || isPatchWishPending ||
!isValid}) and ensure any other click handlers like handleSave still rely on the
same pending flag(s) to prevent concurrent submits; locate the Button rendering
inside ItemEditForm and the isPatchWishPending state/prop to make this change.

---

Nitpick comments:
In `@apps/web/src/app/design-system/image/_components/ProductImageSection.tsx`:
- Line 5: ProductImageSection.tsx is importing ProductImage directly from a
page-local _components folder which breaks colocation rules; update the
reference to use the shared/common component instead: ensure ProductImage is
exported from the shared components layer (e.g.,
components/common/product-image) or add a re-export there, then change the
import in ProductImageSection (the ProductImage symbol) to import from that
common module so the design-system only depends on shared UI, not page-specific
_components.

In
`@apps/web/src/app/tournament/`[id]/create/by-wish/_hooks/usePostTournamentItemsByWish.ts:
- Around line 16-18: The onSuccess handler should await
queryClient.invalidateQueries to ensure invalidation (and any active refetches)
completes before navigation; make the onSuccess function async, call await
queryClient.invalidateQueries({ queryKey: ['tournament', tournamentId] }) and
only after that call router.push(`/tournament/${tournamentId}/create`) so stale
UI is less likely to flash during navigation (keep the same queryKey and
router.push usage).

In `@apps/web/src/app/wishlist/_apis/getWishlist.ts`:
- Line 8: The import in getWishlist.ts uses a relative path for the
WishlistEntryT type; replace that relative import with the project alias form
(use the '`@/`...' alias) so it matches the other imports in this PR and the repo
guideline. Update the import statement that references WishlistEntryT to use the
`@/`* alias form consistent with the codebase.

In `@apps/web/src/app/wishlist/_components/wish-grid/index.tsx`:
- Around line 5-6: Replace the relative imports with the project alias: change
the import of WishItemT and WishProcessingCard to use the '`@/`...' absolute alias
(referencing the symbols WishItemT and WishProcessingCard in this file) so all
imports under apps/web/src use the `@/` path convention consistently; update the
import paths accordingly and ensure any related type/component references still
resolve with the alias.

In `@apps/web/src/app/wishlist/_components/WishlistTabContent.tsx`:
- Line 4: The import for WishItemT in WishlistTabContent.tsx uses a relative
path which breaks the project's import alias convention; update the import of
WishItemT to use the project's absolute alias (`@/`...) instead of
'../_types/wish' so it matches the surrounding `@/`* imports and adheres to the
rule for apps/web/*.ts(x) files (refer to symbol WishItemT in
WishlistTabContent.tsx to locate the import).

In `@apps/web/src/app/wishlist/_components/WishTab.tsx`:
- Line 7: The import in WishTab.tsx is using a relative path for the WishTabT
type; update the import to use the project alias (e.g., `@/`...) instead of the
relative '../_types/wish' so it conforms to the apps/web/src alias rule; change
the import statement that references WishTabT to use the absolute alias path and
ensure any TypeScript path mappings remain valid.

In `@apps/web/src/app/wishlist/page.tsx`:
- Line 11: The import in page.tsx uses a relative path for WishTabT; change it
to the project absolute alias form by importing WishTabT from
'`@/app/wishlist/_types/wish`' so the file follows the codebase rule to use the
`@/`* alias (update the import statement that currently references './_types/wish'
to use the '`@/app/wishlist/_types/wish`' path).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ae3439f0-4cc3-49c6-a9be-1b9f80d6eb0d

📥 Commits

Reviewing files that changed from the base of the PR and between 11075fe and 4820189.

⛔ Files ignored due to path filters (10)
  • apps/web/src/app/tournament/[id]/create/_assets/basket-gray.png is excluded by !**/*.png
  • apps/web/src/app/tournament/[id]/create/_assets/img01.png is excluded by !**/*.png
  • apps/web/src/app/tournament/[id]/create/_assets/img02.png is excluded by !**/*.png
  • apps/web/src/app/tournament/[id]/create/_assets/img03.png is excluded by !**/*.png
  • apps/web/src/app/tournament/[id]/create/_assets/img04.png is excluded by !**/*.png
  • apps/web/src/app/tournament/[id]/create/_assets/img05.png is excluded by !**/*.png
  • apps/web/src/app/tournament/[id]/create/_assets/img06.png is excluded by !**/*.png
  • apps/web/src/assets/icons/fill/warning.svg is excluded by !**/*.svg
  • apps/web/src/assets/images/basket-gray.png is excluded by !**/*.png
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (70)
  • apps/web/next.config.js
  • apps/web/package.json
  • apps/web/src/apis/client.ts
  • apps/web/src/apis/postWishLink.ts
  • apps/web/src/app/design-system/image/_components/ProductImageSection.tsx
  • apps/web/src/app/home/page.tsx
  • apps/web/src/app/layout.tsx
  • apps/web/src/app/login/_apis/postGuestLogin.ts
  • apps/web/src/app/login/_components/LoginButton.tsx
  • apps/web/src/app/login/_hooks/usePostGuestLogin.ts
  • apps/web/src/app/login/_hooks/usePostMemberLogin.ts
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/page.tsx
  • apps/web/src/app/tournament/[id]/_common/_apis/deleteTournamentItem.ts
  • apps/web/src/app/tournament/[id]/_common/_hooks/useDeleteTournamentItem.ts
  • apps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsx
  • apps/web/src/app/tournament/[id]/create/_components/product-image/fallback/LgErrorFallback.tsx
  • apps/web/src/app/tournament/[id]/create/_components/product-image/fallback/LoadingFallback.tsx
  • apps/web/src/app/tournament/[id]/create/_components/product-image/fallback/SmErrorFallback.tsx
  • apps/web/src/app/tournament/[id]/create/_components/product-image/index.tsx
  • apps/web/src/app/tournament/[id]/create/_components/product-image/productImage.const.ts
  • apps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/EmptyBasketSlot.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/TournamentBasketItem.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/TournamentItemBasket.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/TournamentItemBasketCarousel.tsx
  • apps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/TournamentItemFailedDrawer.tsx
  • apps/web/src/app/tournament/[id]/create/_consts/tournamentItemBasket.ts
  • apps/web/src/app/tournament/[id]/create/_consts/tournamentItemBasketConsts.ts
  • apps/web/src/app/tournament/[id]/create/_hooks/useBasketCarousel.ts
  • apps/web/src/app/tournament/[id]/create/_hooks/useTournamentItemBasketCarousel.ts
  • apps/web/src/app/tournament/[id]/create/_utils/tournamentItemBasket.ts
  • apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx
  • apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectCard.tsx
  • apps/web/src/app/tournament/[id]/create/by-wish/_hooks/usePostTournamentItemsByWish.ts
  • apps/web/src/app/tournament/[id]/create/by-wish/page.tsx
  • apps/web/src/app/tournament/[id]/create/page.tsx
  • apps/web/src/app/tournament/[id]/item/[itemId]/_components/EditContent.tsx
  • apps/web/src/app/tournament/[id]/item/[itemId]/_components/ItemEditForm.tsx
  • apps/web/src/app/tournament/[id]/item/[itemId]/page.tsx
  • apps/web/src/app/wishlist/[id]/_apis/patchWish.ts
  • apps/web/src/app/wishlist/[id]/_components/ItemEditForm.tsx
  • apps/web/src/app/wishlist/[id]/_hooks/usePatchWish.ts
  • apps/web/src/app/wishlist/[id]/_types/wish.ts
  • apps/web/src/app/wishlist/[id]/page.tsx
  • apps/web/src/app/wishlist/_apis/deleteWishes.ts
  • apps/web/src/app/wishlist/_apis/getWishlist.ts
  • apps/web/src/app/wishlist/_components/WishTab.tsx
  • apps/web/src/app/wishlist/_components/WishlistContent.tsx
  • apps/web/src/app/wishlist/_components/WishlistFabArea.tsx
  • apps/web/src/app/wishlist/_components/WishlistLayout.tsx
  • apps/web/src/app/wishlist/_components/WishlistTabContent.tsx
  • apps/web/src/app/wishlist/_components/wish-grid/WishCard.tsx
  • apps/web/src/app/wishlist/_components/wish-grid/WishFailedCard.tsx
  • apps/web/src/app/wishlist/_components/wish-grid/WishProcessingCard.tsx
  • apps/web/src/app/wishlist/_components/wish-grid/index.tsx
  • apps/web/src/app/wishlist/_hooks/useDeleteWishes.ts
  • apps/web/src/app/wishlist/_hooks/useGetWishlist.ts
  • apps/web/src/app/wishlist/_mocks/wishMocks.ts
  • apps/web/src/app/wishlist/_types/wish.ts
  • apps/web/src/app/wishlist/_types/wishTypes.ts
  • apps/web/src/app/wishlist/page.tsx
  • apps/web/src/components/carousel/index.tsx
  • apps/web/src/components/common/get-item-dialog/ByLinkDialog.tsx
  • apps/web/src/components/common/toast/ActionSnackbar.tsx
  • apps/web/src/components/common/toast/SuccessToast.tsx
  • apps/web/src/components/common/toast/Toast.tsx
  • apps/web/src/components/common/toast/index.tsx
  • apps/web/src/consts/api.ts
  • apps/web/src/hooks/usePostWishLink.ts
  • apps/web/src/types/wish.ts
💤 Files with no reviewable changes (9)
  • apps/web/src/app/wishlist/[id]/page.tsx
  • apps/web/src/components/common/toast/Toast.tsx
  • apps/web/src/app/wishlist/_types/wishTypes.ts
  • apps/web/src/app/tournament/[id]/create/_consts/tournamentItemBasketConsts.ts
  • apps/web/src/components/common/toast/SuccessToast.tsx
  • apps/web/src/app/wishlist/_mocks/wishMocks.ts
  • apps/web/src/app/tournament/[id]/item/[itemId]/page.tsx
  • apps/web/src/app/tournament/[id]/create/_hooks/useTournamentItemBasketCarousel.ts
  • apps/web/src/components/common/toast/ActionSnackbar.tsx

Comment on lines +18 to +23
if (isWebview() && guestData.accessToken && guestData.refreshToken) {
setCookie('access_token', guestData.accessToken);
setCookie('refresh_token', guestData.refreshToken);
}

// TODO: 게스트 로그인 응답에서 가져온 nickname으로 회원 로그인 요청 보내야 함. 현재 409 상태라 임시로 랜덤 닉네임 사용
await postMemberLogin(Math.random().toString(36).slice(2, 12));
return postMemberLogin(Math.random().toString(36).slice(2, 12));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

게스트 토큰을 회원 로그인 요청 전에 쿠키로 저장하지 마세요.

Line 18에서 access_token을 먼저 저장하면, 바로 아래 postMemberLogin() 요청이 apps/web/src/apis/client.ts Lines 35-40의 인터셉터를 타면서 Authorization: Bearer <guest token>을 붙여 보내게 됩니다. 웹뷰에서 회원 로그인 요청이 게스트 인증 상태로 오염될 수 있으니, 게스트 로그인 응답은 메모리에서만 사용하고 최종 회원 로그인 성공 후에만 쿠키를 갱신하는 편이 안전합니다.

제안된 수정
   const { mutate: postMemberLoginMutation, isPending: isPostMemberLoginPending } = useMutation({
     mutationFn: async () => {
-      const guestData = await postGuestLogin();
-
-      if (isWebview() && guestData.accessToken && guestData.refreshToken) {
-        setCookie('access_token', guestData.accessToken);
-        setCookie('refresh_token', guestData.refreshToken);
-      }
+      await postGuestLogin();

       return postMemberLogin(Math.random().toString(36).slice(2, 12));
     },
🤖 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/login/_hooks/usePostMemberLogin.ts` around lines 18 - 23,
The code saves guest tokens to cookies before calling postMemberLogin, which
causes the request to be sent with guest Authorization via the HTTP interceptor;
change the flow so that when isWebview() and guestData.accessToken/refreshToken
exist you do NOT call setCookie there, instead keep guest tokens only in memory
and call postMemberLogin(randomId) first, then on successful member login update
cookies via setCookie('access_token', ...) and setCookie('refresh_token', ...);
update the logic in usePostMemberLogin to move setCookie calls to the post-login
success handler (and ensure postMemberLogin and the interceptor in apis client
use the fresh member token thereafter).

Comment thread apps/web/src/app/login/_hooks/usePostMemberLogin.ts Outdated
Comment on lines 42 to 45
if (status === 403 || status === 404 || status === 409) {
toast.error(clientErrorMessage);
router.replace(`/tournament/${tournamentId}/create`);
if (pathname !== tournamentCreatePage) router.replace(tournamentCreatePage);
} else if (status === 500) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

/create 페이지에서는 404/409 후 상태 동기화가 빠집니다.

이 분기 때문에 현재 경로가 이미 /tournament/${tournamentId}/create면 토스트만 띄우고 끝납니다. 그런데 이 케이스는 서버 상태가 이미 바뀌었다는 뜻이라서, 모달이 닫힌 뒤에도 바구니에 실패 아이템이 그대로 남는 stale UI가 됩니다. 여기서는 최소한 ['tournament', tournamentId]를 다시 invalidate 하거나 router.refresh()로 현재 페이지를 동기화해 주세요.

예시 수정
         if (status === 403 || status === 404 || status === 409) {
           toast.error(clientErrorMessage);
-          if (pathname !== tournamentCreatePage) router.replace(tournamentCreatePage);
+          queryClient.invalidateQueries({ queryKey: ['tournament', tournamentId] });
+
+          if (pathname !== tournamentCreatePage) {
+            router.replace(tournamentCreatePage);
+          }
         } else if (status === 500) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/tournament/`[id]/_common/_hooks/useDeleteTournamentItem.ts
around lines 42 - 45, The branch in useDeleteTournamentItem that handles status
=== 403 || 404 || 409 currently only shows a toast and conditionally
router.replace, leaving the UI stale when the user is already on
tournamentCreatePage; update that branch to also re-sync state by invalidating
the tournament query or refreshing the router: call
queryClient.invalidateQueries(['tournament', tournamentId]) (or
router.refresh()) after showing the toast (or in the pathname ===
tournamentCreatePage case) so the modal/basket reflects the server-side change;
reference useDeleteTournamentItem, tournamentId, queryClient.invalidateQueries,
and router.refresh when making this change.

Comment on lines +14 to +17
className={cn(
'relative box-border size-[68px] shrink-0 overflow-hidden shadow-[0_0_8px_rgba(0,0,0,0.16)]',
item.status === 'READY' || (item.status === 'FAILED' && 'cursor-pointer')
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

cursor-pointer 조건식이 READY 상태에서 깨집니다.

지금 식은 READY일 때 문자열이 아니라 boolean true를 넘겨서 cursor-pointer가 붙지 않습니다. 의도가 READY/FAILED 둘 다 포인터를 주는 거라면 삼항식으로 바꾸는 편이 안전합니다.

예시 수정
       className={cn(
         'relative box-border size-[68px] shrink-0 overflow-hidden shadow-[0_0_8px_rgba(0,0,0,0.16)]',
-        item.status === 'READY' || (item.status === 'FAILED' && 'cursor-pointer')
+        item.status === 'READY' || item.status === 'FAILED' ? 'cursor-pointer' : undefined
       )}
As per coding guidelines, "Clickable elements must have `cursor-pointer` class (Tailwind)".
📝 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
className={cn(
'relative box-border size-[68px] shrink-0 overflow-hidden shadow-[0_0_8px_rgba(0,0,0,0.16)]',
item.status === 'READY' || (item.status === 'FAILED' && 'cursor-pointer')
)}
className={cn(
'relative box-border size-[68px] shrink-0 overflow-hidden shadow-[0_0_8px_rgba(0,0,0,0.16)]',
item.status === 'READY' || item.status === 'FAILED' ? 'cursor-pointer' : undefined
)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournamentItemBasket/TournamentBasketItem.tsx
around lines 14 - 17, The className expression passes a boolean when item.status
=== 'READY' so 'cursor-pointer' is not added; update the conditional in the cn
call (where className uses cn and references item.status) to return the string
'cursor-pointer' when item.status is 'READY' or 'FAILED' (e.g., use a ternary or
explicit boolean check like (item.status === 'READY' || item.status ===
'FAILED') ? 'cursor-pointer' : '') so the class is actually applied to the
clickable element.

Comment on lines +31 to +46
/** 담기 완료 시 마지막 아이템이 있는 바구니로 이동 */
useEffect(() => {
if (!isCarouselEnabled) {
prevItemCountRef.current = items.length;
return;
}

if (!carouselApi) return;

const prevCount = prevItemCountRef.current;
if (items.length > prevCount) {
carouselApi.scrollTo(getBasketIndexForLastItem(items.length));
}

prevItemCountRef.current = items.length;
}, [items.length, carouselApi, isCarouselEnabled]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

초기 데이터 로드도 “새 아이템 추가”로 오인합니다.

첫 렌더에서 items가 비어 있다가 fetch 후 채워지면 0 -> N 증가도 그대로 잡혀서, 사용자가 아무 동작을 하지 않았는데 마지막 바구니로 강제 이동합니다. 최초 동기화는 제외하거나, 실제 사용자 추가 이후에만 이 효과가 실행되도록 분기해 주세요.

수정 예시
   useEffect(() => {
     if (!isCarouselEnabled) {
       prevItemCountRef.current = items.length;
       return;
     }

     if (!carouselApi) return;

     const prevCount = prevItemCountRef.current;
-    if (items.length > prevCount) {
+    if (prevCount > 0 && items.length > prevCount) {
       carouselApi.scrollTo(getBasketIndexForLastItem(items.length));
     }

     prevItemCountRef.current = items.length;
   }, [items.length, carouselApi, isCarouselEnabled]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/tournament/`[id]/create/_components/tournamentItemBasket/TournamentItemBasketCarousel.tsx
around lines 31 - 46, The effect currently treats any increase in items.length
(including initial fetch 0->N) as a user-add and forces carouselApi.scrollTo;
add an initialization guard so the first population is ignored: create a ref
like initialLoadRef (useRef(true)) and in the useEffect (the one referencing
prevItemCountRef, isCarouselEnabled, carouselApi, getBasketIndexForLastItem)
skip the scroll when initialLoadRef.current is true and items.length >
prevCount, then set initialLoadRef.current = false; keep updating
prevItemCountRef.current as before so subsequent genuine additions trigger
carouselApi.scrollTo.

Comment on lines +15 to +16
const { mutate: deleteWishesMutation, isPending: isDeleteWishesPending } = useMutation({
mutationFn: (wishIds: number[]) => deleteWishes(wishIds),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In TanStack Query v5, does useMutation's mutatefunction return a Promise? What is the recommended way to wait for a mutation to finish before running cleanup logic:mutateormutateAsync?

💡 Result:

In TanStack Query v5, the mutate function returned by useMutation does not return a Promise; it returns void [1][2]. Regarding the recommended way to wait for a mutation to finish, the answer depends on your specific needs: 1. Recommended for most cases (mutate): For standard scenarios, it is recommended to use mutate and handle side effects via the provided lifecycle callbacks—onSuccess, onError, or onSettled [3][4]. This approach is preferred because React Query manages error handling internally, preventing unhandled promise rejections [4]. 2. When to use mutateAsync: You should use mutateAsync only when you specifically need a Promise—for example, if you need to perform actions that cannot be easily handled in the callbacks, such as orchestrating multiple concurrent mutations or chaining complex, dependent asynchronous operations [4]. Unlike mutate, mutateAsync returns a Promise that resolves on success or throws on error, requiring you to handle the error logic manually (e.g., using try/catch) to avoid unhandled promise rejections [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/web/src/app/wishlist/_hooks/useDeleteWishes.ts"
echo "== file = $FILE =="
if [ ! -f "$FILE" ]; then
  echo "File not found"
  exit 1
fi

nl -ba "$FILE" | sed -n '1,220p'

Repository: depromeet/PIKI-Client

Length of output: 192


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/web/src/app/wishlist/_hooks/useDeleteWishes.ts"
echo "== file = $FILE =="
if [ ! -f "$FILE" ]; then
  echo "File not found"
  exit 1
fi
cat -n "$FILE" | sed -n '1,220p'

Repository: depromeet/PIKI-Client

Length of output: 2327


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/web/src/app/wishlist/_hooks/useDeleteWishes.ts"
echo "== file = $FILE =="
if [ ! -f "$FILE" ]; then
  echo "File not found"
  exit 1
fi
cat -n "$FILE" | sed -n '1,220p'

Repository: depromeet/PIKI-Client

Length of output: 2327


삭제 요청 직후 UI 상태를 초기화하고 있어요 (mutation 완료 전에 finally가 실행됨)

  • handleConfirmDelete (45-52)에서 mutate를 await 하지 않은 채 try/finallyfinallyselectedIds/isDeleteMode를 즉시 초기화합니다. TanStack Query v5에서 mutate는 Promise가 아니라서 mutation 완료 전에 cleanup이 돌며, 실패 시 재시도 컨텍스트가 사라지고 isDeleteWishesPending 기반 UX도 끊길 수 있습니다.
  • onError (24-28)가 // TODO: 에러처리로 비어 있어 실패했을 때 사용자 피드백/상태 처리가 없습니다.
🛠️ 제안 수정
-  const { mutate: deleteWishesMutation, isPending: isDeleteWishesPending } = useMutation({
+  const { mutateAsync: deleteWishesMutation, isPending: isDeleteWishesPending } = useMutation({
     mutationFn: (wishIds: number[]) => deleteWishes(wishIds),
     onSuccess: (_, wishIds) => {
       queryClient.invalidateQueries({ queryKey: ['wishlists'] });
       wishIds.forEach(wishId => {
         queryClient.invalidateQueries({ queryKey: ['wish', wishId] });
       });
       toast.success('선택한 위시를 삭제했어요');
     },
     onError: error => {
       if (!isAxiosError<ApiErrorResponseT>(error) || !error.response) return;
 
       // TODO: 에러처리
     },
   });
 
   const handleConfirmDelete = async () => {
     if (selectedIds.size === 0) return;
-    try {
-      deleteWishesMutation(Array.from(selectedIds));
-    } finally {
-      setSelectedIds(new Set());
-      setIsDeleteMode(false);
-    }
+    await deleteWishesMutation(Array.from(selectedIds));
+    setSelectedIds(new Set());
+    setIsDeleteMode(false);
   };
🤖 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/wishlist/_hooks/useDeleteWishes.ts` around lines 15 - 16,
The delete flow currently resets UI state immediately because
handleConfirmDelete calls deleteWishes via the mutation without awaiting its
completion and the useMutation onError is unimplemented; change to use the
mutation's async API (call deleteWishesMutation.mutateAsync or await the
returned promise) so you only clear selectedIds and exit isDeleteMode after the
mutation resolves successfully, and implement onError in useMutation to restore
selectedIds/isDeleteMode and surface an error (e.g., set an error toast or
state) when deleteWishes fails; reference functions/vars: handleConfirmDelete,
deleteWishesMutation (mutate vs mutateAsync), isDeleteWishesPending, and the
onError handler to locate the spots to change.

Comment on lines +24 to +28
onError: error => {
if (!isAxiosError<ApiErrorResponseT>(error) || !error.response) return;

// TODO: 에러처리
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

삭제 실패가 조용히 무시됩니다.

현재 에러 경로가 TODO만 남기고 종료돼서 실패해도 사용자 피드백이 없습니다. 최소한 에러 토스트는 바로 추가하는 편이 좋겠습니다.

🛠️ 제안 수정
     onError: error => {
-      if (!isAxiosError<ApiErrorResponseT>(error) || !error.response) return;
-
-      // TODO: 에러처리
+      if (!isAxiosError<ApiErrorResponseT>(error) || !error.response) {
+        toast.error('위시 삭제에 실패했어요');
+        return;
+      }
+
+      toast.error(error.response.data.detail ?? '위시 삭제에 실패했어요');
     },
🤖 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/wishlist/_hooks/useDeleteWishes.ts` around lines 24 - 28,
The onError handler in useDeleteWishes currently returns early after checking
isAxiosError<ApiErrorResponseT> and error.response, so failures are silently
ignored; update this onError to show a user-facing error toast using the app's
notification utility (e.g., toast.error or existing notifier) and include the
server message if present (use error.response.data?.message) or fallback to
error.message; keep the existing type check (isAxiosError<ApiErrorResponseT>)
and call the notifier inside the same onError block so delete failures surface
to users.

Comment on lines +74 to +84
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowLeft') {
event.preventDefault();
scrollPrev();
} else if (event.key === 'ArrowRight') {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

세로 모드 키보드 탐색이 빠져 있습니다.

orientation="vertical"도 공개 API인데 현재는 좌/우 키만 처리합니다. 이 상태면 세로 캐러셀은 키보드로 이동할 수 없습니다. 방향별로 ArrowUp/ArrowDown도 같이 매핑해 주세요.

🤖 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/components/carousel/index.tsx` around lines 74 - 84,
handleKeyDown currently only maps ArrowLeft/ArrowRight so vertical carousels
cannot be keyboard-navigated; update handleKeyDown (the React.useCallback that
references scrollPrev and scrollNext) to also handle ArrowUp and ArrowDown when
orientation === 'vertical' (or accept both mappings regardless of orientation)
by mapping ArrowUp -> scrollPrev and ArrowDown -> scrollNext and calling
event.preventDefault() the same way as for left/right.

Comment on lines +161 to +180
function CarouselPrevious({ className, ...props }: React.ComponentProps<'button'>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();

return (
<button
data-slot="carousel-previous"
className={cn(
'absolute touch-manipulation rounded-full',
orientation === 'horizontal'
? 'top-1/2 -left-12 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
{`<`}
<span className="sr-only">Previous slide</span>
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

이전/다음 버튼에 type="button"이 필요합니다.

공용 컴포넌트라 폼 내부에서 재사용될 수 있는데, 지금은 기본값인 submit으로 동작해서 슬라이드 버튼 클릭이 의도치 않은 폼 제출을 유발할 수 있습니다.

수정 예시
 function CarouselPrevious({ className, ...props }: React.ComponentProps<'button'>) {
   const { orientation, scrollPrev, canScrollPrev } = useCarousel();

   return (
     <button
+      type="button"
       data-slot="carousel-previous"
       className={cn(
         'absolute touch-manipulation rounded-full',
         orientation === 'horizontal'
           ? 'top-1/2 -left-12 -translate-y-1/2'
@@
 function CarouselNext({ className, ...props }: React.ComponentProps<'button'>) {
   const { orientation, scrollNext, canScrollNext } = useCarousel();

   return (
     <button
+      type="button"
       data-slot="carousel-next"
       className={cn(
         'absolute touch-manipulation rounded-full',
         orientation === 'horizontal'
           ? 'top-1/2 -right-12 -translate-y-1/2'

Also applies to: 184-203

🤖 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/components/carousel/index.tsx` around lines 161 - 180, The
carousel navigation buttons (CarouselPrevious and the corresponding
CarouselNext) currently render plain <button> elements that default to
type="submit" inside forms; update both components (CarouselPrevious and
CarouselNext) to explicitly set type="button" on their <button> elements so
clicks don’t trigger unintended form submissions, keeping all existing props,
disabled handling, onClick (scrollPrev/scrollNext), and accessibility text
unchanged.

Comment on lines +49 to +67
postWishLinkMutation(trimmedUrl, {
onSettled: () => {
onOpenChange(false);
resetState();
},
onSuccess: () => {
router.push('/wishlist');
toast.success('위시에 상품을 담았어요', {
classNames: { toast: '!bottom-[122px]' },
});
},
});
else
postTournamentItemLinkMutation(trimmedUrl, {
onSettled: () => {
onOpenChange(false);
resetState();
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

실패 시에도 다이얼로그를 닫아 입력값이 사라집니다.

onSettled에서 onOpenChange(false)resetState()를 호출하면 요청이 실패해도 모달이 닫히고 URL이 초기화됩니다. 서버 검증 실패나 일시 오류에서 재시도 경로가 끊기므로, 정리 로직은 onSuccess로 옮기고 실패 시에는 입력값을 유지하는 편이 안전합니다.

제안 코드
     if (type === 'wish')
       postWishLinkMutation(trimmedUrl, {
-        onSettled: () => {
-          onOpenChange(false);
-          resetState();
-        },
         onSuccess: () => {
+          onOpenChange(false);
+          resetState();
           router.push('/wishlist');
           toast.success('위시에 상품을 담았어요', {
             classNames: { toast: '!bottom-[122px]' },
           });
         },
       });
     else
       postTournamentItemLinkMutation(trimmedUrl, {
-        onSettled: () => {
+        onSuccess: () => {
           onOpenChange(false);
           resetState();
         },
       });
📝 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
postWishLinkMutation(trimmedUrl, {
onSettled: () => {
onOpenChange(false);
resetState();
},
onSuccess: () => {
router.push('/wishlist');
toast.success('위시에 상품을 담았어요', {
classNames: { toast: '!bottom-[122px]' },
});
},
});
else
postTournamentItemLinkMutation(trimmedUrl, {
onSettled: () => {
onOpenChange(false);
resetState();
},
});
postWishLinkMutation(trimmedUrl, {
onSuccess: () => {
onOpenChange(false);
resetState();
router.push('/wishlist');
toast.success('위시에 상품을 담았어요', {
classNames: { toast: '!bottom-[122px]' },
});
},
});
else
postTournamentItemLinkMutation(trimmedUrl, {
onSuccess: () => {
onOpenChange(false);
resetState();
},
});
🤖 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/components/common/get-item-dialog/ByLinkDialog.tsx` around lines
49 - 67, The dialog cleanup currently runs in the onSettled handler so failures
also close the modal and clear input; move the cleanup calls
(onOpenChange(false) and resetState()) from onSettled into onSuccess for both
postWishLinkMutation and postTournamentItemLinkMutation so the modal and URL are
only cleared when the request succeeds; keep router.push('/wishlist') and
toast.success(...) in postWishLinkMutation.onSuccess as-is and leave onSettled
for any non-cleanup side effects if needed.

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

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/error.tsx (1)

5-8: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Props 타입 이름을 ErrorProps로 변경해주세요.

코딩 가이드라인에 따르면 Props 타입은 {ComponentName}Props 패턴을 따라야 합니다. 현재 Props로 명명되어 있지만 ErrorProps로 변경해야 합니다.

📝 제안하는 수정안
-type Props = {
+type ErrorProps = {
   error: Error & { digest?: string; statusCode?: number };
   reset: () => void;
 };

 // TODO: 임시 에러 페이지 - 디자인 변경 필요
-export default function Error({ error, reset }: Props) {
+export default function Error({ error, reset }: ErrorProps) {

As per coding guidelines: "Props type names must follow pattern {ComponentName}Props" and the retrieved learning confirms props types should use {ComponentName}Props pattern without T suffix.

🤖 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 5 - 8, Rename the Props type to
ErrorProps to follow the {ComponentName}Props pattern: change the type
declaration named Props to ErrorProps and update all usages (e.g., component
parameter/annotation or any references to Props) within this file so they now
refer to ErrorProps; ensure any exports or imports that referenced Props are
updated accordingly to avoid type errors.
🧹 Nitpick comments (1)
apps/web/src/app/error.tsx (1)

45-51: ⚖️ Poor tradeoff

공통 Button 컴포넌트 사용을 고려해보세요.

현재 네이티브 <button>과 인라인 Tailwind 클래스를 사용하고 있습니다. 가이드라인에 따르면 공통 Button 컴포넌트의 cva를 사용하면 커서 스타일과 디자인 시스템 일관성이 자동으로 처리됩니다.

네이티브 버튼도 정상 작동하지만, 디자인 시스템 일관성을 위해 공통 Button 컴포넌트 사용을 권장합니다.

As per coding guidelines: "use cva in shared Button component" for automatic cursor-pointer handling and design system consistency.

🤖 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 45 - 51, Replace the native <button>
in the error UI with the shared Button component that uses cva to ensure cursor
and design consistency: import and render the shared Button (named Button)
instead of the native element, pass the same onClick handler (reset) and any
text children ("다시 시도"), and move/translate the existing Tailwind classes into
the Button's props (className or variant/size) so the visual styling is
preserved while leveraging the shared component's automatic cursor and
design-system behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/web/src/app/error.tsx`:
- Around line 5-8: Rename the Props type to ErrorProps to follow the
{ComponentName}Props pattern: change the type declaration named Props to
ErrorProps and update all usages (e.g., component parameter/annotation or any
references to Props) within this file so they now refer to ErrorProps; ensure
any exports or imports that referenced Props are updated accordingly to avoid
type errors.

---

Nitpick comments:
In `@apps/web/src/app/error.tsx`:
- Around line 45-51: Replace the native <button> in the error UI with the shared
Button component that uses cva to ensure cursor and design consistency: import
and render the shared Button (named Button) instead of the native element, pass
the same onClick handler (reset) and any text children ("다시 시도"), and
move/translate the existing Tailwind classes into the Button's props (className
or variant/size) so the visual styling is preserved while leveraging the shared
component's automatic cursor and design-system behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0faa9cf-8f79-43c7-98c6-a07782053af3

📥 Commits

Reviewing files that changed from the base of the PR and between 4820189 and e89636c.

📒 Files selected for processing (2)
  • apps/web/src/app/error.tsx
  • apps/web/src/app/login/_hooks/usePostMemberLogin.ts
💤 Files with no reviewable changes (1)
  • apps/web/src/app/login/_hooks/usePostMemberLogin.ts

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

Labels

refactor Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants