Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No description provided. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
Walkthrough웹뷰 토큰 저장/요청 인터셉터 추가, 위시 링크·이미지 업로드 및 삭제 API/훅 추가, Embla 기반 캐러셀 도입으로 토너먼트 바구니 리팩토링, 위시 타입·그리드·토스트 및 레이아웃 스타일 정리 변경입니다. Changes웹뷰 토큰 관리 및 API 인증
위시 링크 제출, 이미지 업로드, 삭제 기능
Embla 캐러셀 컴포넌트 및 토너먼트 바구니 리팩토링
페이지 레이아웃 및 토스트 정리
Estimated code review effort: Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 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 winREADY 카드 분기에서도
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 경계가 깨집니다. 공용으로 쓰는ProductImage는components/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 incomponents/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
⛔ Files ignored due to path filters (10)
apps/web/src/app/tournament/[id]/create/_assets/basket-gray.pngis excluded by!**/*.pngapps/web/src/app/tournament/[id]/create/_assets/img01.pngis excluded by!**/*.pngapps/web/src/app/tournament/[id]/create/_assets/img02.pngis excluded by!**/*.pngapps/web/src/app/tournament/[id]/create/_assets/img03.pngis excluded by!**/*.pngapps/web/src/app/tournament/[id]/create/_assets/img04.pngis excluded by!**/*.pngapps/web/src/app/tournament/[id]/create/_assets/img05.pngis excluded by!**/*.pngapps/web/src/app/tournament/[id]/create/_assets/img06.pngis excluded by!**/*.pngapps/web/src/assets/icons/fill/warning.svgis excluded by!**/*.svgapps/web/src/assets/images/basket-gray.pngis excluded by!**/*.pngpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (70)
apps/web/next.config.jsapps/web/package.jsonapps/web/src/apis/client.tsapps/web/src/apis/postWishLink.tsapps/web/src/app/design-system/image/_components/ProductImageSection.tsxapps/web/src/app/home/page.tsxapps/web/src/app/layout.tsxapps/web/src/app/login/_apis/postGuestLogin.tsapps/web/src/app/login/_components/LoginButton.tsxapps/web/src/app/login/_hooks/usePostGuestLogin.tsapps/web/src/app/login/_hooks/usePostMemberLogin.tsapps/web/src/app/login/page.tsxapps/web/src/app/page.tsxapps/web/src/app/tournament/[id]/_common/_apis/deleteTournamentItem.tsapps/web/src/app/tournament/[id]/_common/_hooks/useDeleteTournamentItem.tsapps/web/src/app/tournament/[id]/create/_components/TournamentCreateClient.tsxapps/web/src/app/tournament/[id]/create/_components/product-image/fallback/LgErrorFallback.tsxapps/web/src/app/tournament/[id]/create/_components/product-image/fallback/LoadingFallback.tsxapps/web/src/app/tournament/[id]/create/_components/product-image/fallback/SmErrorFallback.tsxapps/web/src/app/tournament/[id]/create/_components/product-image/index.tsxapps/web/src/app/tournament/[id]/create/_components/product-image/productImage.const.tsapps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/EmptyBasketSlot.tsxapps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/TournamentBasketItem.tsxapps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/TournamentItemBasket.tsxapps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/TournamentItemBasketCarousel.tsxapps/web/src/app/tournament/[id]/create/_components/tournamentItemBasket/TournamentItemFailedDrawer.tsxapps/web/src/app/tournament/[id]/create/_consts/tournamentItemBasket.tsapps/web/src/app/tournament/[id]/create/_consts/tournamentItemBasketConsts.tsapps/web/src/app/tournament/[id]/create/_hooks/useBasketCarousel.tsapps/web/src/app/tournament/[id]/create/_hooks/useTournamentItemBasketCarousel.tsapps/web/src/app/tournament/[id]/create/_utils/tournamentItemBasket.tsapps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsxapps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectCard.tsxapps/web/src/app/tournament/[id]/create/by-wish/_hooks/usePostTournamentItemsByWish.tsapps/web/src/app/tournament/[id]/create/by-wish/page.tsxapps/web/src/app/tournament/[id]/create/page.tsxapps/web/src/app/tournament/[id]/item/[itemId]/_components/EditContent.tsxapps/web/src/app/tournament/[id]/item/[itemId]/_components/ItemEditForm.tsxapps/web/src/app/tournament/[id]/item/[itemId]/page.tsxapps/web/src/app/wishlist/[id]/_apis/patchWish.tsapps/web/src/app/wishlist/[id]/_components/ItemEditForm.tsxapps/web/src/app/wishlist/[id]/_hooks/usePatchWish.tsapps/web/src/app/wishlist/[id]/_types/wish.tsapps/web/src/app/wishlist/[id]/page.tsxapps/web/src/app/wishlist/_apis/deleteWishes.tsapps/web/src/app/wishlist/_apis/getWishlist.tsapps/web/src/app/wishlist/_components/WishTab.tsxapps/web/src/app/wishlist/_components/WishlistContent.tsxapps/web/src/app/wishlist/_components/WishlistFabArea.tsxapps/web/src/app/wishlist/_components/WishlistLayout.tsxapps/web/src/app/wishlist/_components/WishlistTabContent.tsxapps/web/src/app/wishlist/_components/wish-grid/WishCard.tsxapps/web/src/app/wishlist/_components/wish-grid/WishFailedCard.tsxapps/web/src/app/wishlist/_components/wish-grid/WishProcessingCard.tsxapps/web/src/app/wishlist/_components/wish-grid/index.tsxapps/web/src/app/wishlist/_hooks/useDeleteWishes.tsapps/web/src/app/wishlist/_hooks/useGetWishlist.tsapps/web/src/app/wishlist/_mocks/wishMocks.tsapps/web/src/app/wishlist/_types/wish.tsapps/web/src/app/wishlist/_types/wishTypes.tsapps/web/src/app/wishlist/page.tsxapps/web/src/components/carousel/index.tsxapps/web/src/components/common/get-item-dialog/ByLinkDialog.tsxapps/web/src/components/common/toast/ActionSnackbar.tsxapps/web/src/components/common/toast/SuccessToast.tsxapps/web/src/components/common/toast/Toast.tsxapps/web/src/components/common/toast/index.tsxapps/web/src/consts/api.tsapps/web/src/hooks/usePostWishLink.tsapps/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
| 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)); |
There was a problem hiding this comment.
게스트 토큰을 회원 로그인 요청 전에 쿠키로 저장하지 마세요.
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).
| 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) { |
There was a problem hiding this comment.
/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.
| 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') | ||
| )} |
There was a problem hiding this comment.
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
)}📝 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.
| 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.
| /** 담기 완료 시 마지막 아이템이 있는 바구니로 이동 */ | ||
| 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]); |
There was a problem hiding this comment.
초기 데이터 로드도 “새 아이템 추가”로 오인합니다.
첫 렌더에서 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.
| const { mutate: deleteWishesMutation, isPending: isDeleteWishesPending } = useMutation({ | ||
| mutationFn: (wishIds: number[]) => deleteWishes(wishIds), |
There was a problem hiding this comment.
🧩 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:
- 1: https://tanstack.com/query/v5/docs/framework/react/reference/useMutation
- 2: https://github.com/tanstack/query/blob/main/docs/framework/react/reference/useMutation.md
- 3: https://tanstack.com/query/v5/docs/framework/react/guides/mutations
- 4: https://tkdodo.eu/blog/mastering-mutations-in-react-query
🏁 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/finally의finally로selectedIds/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.
| onError: error => { | ||
| if (!isAxiosError<ApiErrorResponseT>(error) || !error.response) return; | ||
|
|
||
| // TODO: 에러처리 | ||
| }, |
There was a problem hiding this comment.
삭제 실패가 조용히 무시됩니다.
현재 에러 경로가 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.
| 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] |
There was a problem hiding this comment.
세로 모드 키보드 탐색이 빠져 있습니다.
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.
| 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> |
There was a problem hiding this comment.
이전/다음 버튼에 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.
| postWishLinkMutation(trimmedUrl, { | ||
| onSettled: () => { | ||
| onOpenChange(false); | ||
| resetState(); | ||
| }, | ||
| onSuccess: () => { | ||
| router.push('/wishlist'); | ||
| toast.success('위시에 상품을 담았어요', { | ||
| classNames: { toast: '!bottom-[122px]' }, | ||
| }); | ||
| }, | ||
| }); | ||
| else | ||
| postTournamentItemLinkMutation(trimmedUrl, { | ||
| onSettled: () => { | ||
| onOpenChange(false); | ||
| resetState(); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
실패 시에도 다이얼로그를 닫아 입력값이 사라집니다.
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.
| 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.
There was a problem hiding this comment.
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 winProps 타입 이름을
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}Propspattern 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
cvain 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
📒 Files selected for processing (2)
apps/web/src/app/error.tsxapps/web/src/app/login/_hooks/usePostMemberLogin.ts
💤 Files with no reviewable changes (1)
- apps/web/src/app/login/_hooks/usePostMemberLogin.ts
작업 요약
작업 세부 내용
공통
sonner기반Toaster로 통일 (ActionSnackbar,SuccessToast, 구Toast제거)type에 따라 위시 / 토너먼트 후보 분기, 위시 성공 시 toast +/wishlist이동embla-carousel기반 공통Carousel컴포넌트 추가 (토너먼트 장바구니 등에서 사용)layout에서px, 페이지 배경색 제거 → 각 페이지가 레이아웃·배경 담당 (max-w-120래퍼는 유지)WISH_OCR→/api/v1/wishlists/images로 수정/login
access_token→Authorization헤더,X-Client-Type(app/web) 설정access_token,refresh_token을 쿠키에 직접 저장postGuestLogin응답에 토큰 필드 반영/wishlist
WishItemT.imageUrl을optional→string | null로 정리postWishLinkAPI +usePostWishLink훅 추가deleteWishesAPI + 삭제 모드 UI (선택 삭제, 삭제 중 FAB에 Spinner)wish-grid관련 컴포넌트를wishlist/_components/wish-grid/로 colocation 이동WishProcessingCard추가 (담는 중 UI)wishTypes→wish, mock 정리)/wishlist/[id]
patchWish)imageUrl타입string | null반영/tournament/[id]/create
기능 미완..)deleteTournamentItem)/tournament/[id]/create/by-wish
/create로 이동/tournament/[id]/item/[itemId]
ItemEditForm등 imageUrlstring | null타입 정리 (위시 수정과 동일 패턴)Summary by CodeRabbit
새로운 기능
개선 사항
기타