From 57b6f8d95131a4b999e4f6fece521e3abb23b5b9 Mon Sep 17 00:00:00 2001 From: joyeongchan Date: Mon, 2 Nov 2026 19:53:18 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=ED=86=A0=EB=84=88=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20=EC=9C=84=EC=8B=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=EB=A5=BC=20=ED=8F=B4=EB=8D=94=20=EC=B9=A9=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - by-wish 화면에 폴더 칩 · 위시 그리드 · 위시 없음 모달 적용 - 폴더 칩 컴포넌트 · 모달 · 폴더별 위시 훅을 by-wish-v2 에서 이관 - 기존 규칙 유지 - READY 위시만 표시 · 이미 후보에 담긴 상품 제외 - 다음 버튼은 기존 후보 담기 API 와 성공 · 에러 처리 그대로 - 선택 상태에 위시 id 와 상품 id 를 함께 저장 - 원인: 칩을 오가며 고른 위시도 함께 담아야 함 - 쓰지 않게 된 WishSelectCard · WishSelectHeader · useWishSelection 삭제 --- .../_components/ByWishV2Content.tsx | 99 ------------ .../by-wish/_components/ByWishContent.tsx | 145 +++++++++--------- .../_components/FolderChipFilter.tsx | 0 .../_components/NoWishDialog.tsx | 0 .../by-wish/_components/WishSelectCard.tsx | 51 ------ .../by-wish/_components/WishSelectHeader.tsx | 42 ----- .../_hooks/useFolderWishes.ts | 0 .../create/by-wish/_hooks/useWishSelection.ts | 34 ---- 8 files changed, 69 insertions(+), 302 deletions(-) delete mode 100644 apps/web/src/app/tournament/[id]/create/by-wish-v2/_components/ByWishV2Content.tsx rename apps/web/src/app/tournament/[id]/create/{by-wish-v2 => by-wish}/_components/FolderChipFilter.tsx (100%) rename apps/web/src/app/tournament/[id]/create/{by-wish-v2 => by-wish}/_components/NoWishDialog.tsx (100%) delete mode 100644 apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectCard.tsx delete mode 100644 apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectHeader.tsx rename apps/web/src/app/tournament/[id]/create/{by-wish-v2 => by-wish}/_hooks/useFolderWishes.ts (100%) delete mode 100644 apps/web/src/app/tournament/[id]/create/by-wish/_hooks/useWishSelection.ts diff --git a/apps/web/src/app/tournament/[id]/create/by-wish-v2/_components/ByWishV2Content.tsx b/apps/web/src/app/tournament/[id]/create/by-wish-v2/_components/ByWishV2Content.tsx deleted file mode 100644 index c0355d7aa..000000000 --- a/apps/web/src/app/tournament/[id]/create/by-wish-v2/_components/ByWishV2Content.tsx +++ /dev/null @@ -1,99 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { toast } from 'sonner'; - -import BottomCta from '@/components/bottom-cta'; -import Button from '@/components/button'; -import WishGrid from '@/components/common/wish-grid'; -import { ROUTES } from '@/consts/route'; -import { useBackWithFallback } from '@/hooks/useBackWithFallback'; -import { useGetFolders } from '@/hooks/useGetFolders'; -import useIntersectionObserver from '@/hooks/useIntersectionObserver'; - -import { useGetTournament } from '../../../_common/_hooks/useGetTournament'; -import { MAX_SELECT } from '../../by-wish/_consts/selectLimits'; -import { useFolderWishes } from '../_hooks/useFolderWishes'; -import FolderChipFilter from './FolderChipFilter'; -import NoWishDialog from './NoWishDialog'; - -type ByWishV2ContentProps = { - tournamentId: number; -}; - -function ByWishV2Content({ tournamentId }: ByWishV2ContentProps) { - const backWithFallback = useBackWithFallback(); - const [activeFolderId, setActiveFolderId] = useState(null); - const [selectedWishIds, setSelectedWishIds] = useState>(new Set()); - const { tournamentData } = useGetTournament(tournamentId); - - const pending = 'pending' in tournamentData ? tournamentData.pending : null; - const addableCount = Math.max(MAX_SELECT - (pending?.items.length ?? 0), 0); - const isOverSelectionLimit = selectedWishIds.size > addableCount; - - const { foldersData } = useGetFolders(); - const { allWishesData, wishesData, fetchNextPage, hasNextPage, isFetchingNextPage } = - useFolderWishes(activeFolderId); - const bottomRef = useIntersectionObserver(fetchNextPage, hasNextPage && !isFetchingNextPage); - - const hasNoWish = allWishesData.length === 0; - - const handleToggleSelect = (wishId: number) => { - const isSelected = selectedWishIds.has(wishId); - if (!isSelected && selectedWishIds.size >= addableCount) { - toast.warning(`최대 ${addableCount}개까지 상품을 담을 수 있어요.`, { id: 'max-select' }); - return; - } - - setSelectedWishIds(prev => { - const next = new Set(prev); - if (isSelected) next.delete(wishId); - else next.add(wishId); - return next; - }); - }; - - const handleBackToCreate = () => backWithFallback(ROUTES.TOURNAMENT_CREATE(tournamentId)); - - if (hasNoWish) return ; - - return ( - <> -
- {foldersData.length > 0 && ( - - )} -

- {selectedWishIds.size}/{addableCount}개 선택 -

-
- -
- -
-
- - - - - - ); -} - -export default ByWishV2Content; diff --git a/apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx b/apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx index d068c1fd7..4526d71d6 100644 --- a/apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx +++ b/apps/web/src/app/tournament/[id]/create/by-wish/_components/ByWishContent.tsx @@ -1,137 +1,130 @@ 'use client'; -import { useEffect } from 'react'; +import { useState } from 'react'; import { toast } from 'sonner'; import BottomCta from '@/components/bottom-cta'; import Button from '@/components/button'; +import WishGrid from '@/components/common/wish-grid'; import { Header, HeaderIcon } from '@/components/header'; -import TournamentErrorDialog from '@/components/tournament-error-dialog'; import { ITEM_STATUS } from '@/consts/item'; import { ROUTES } from '@/consts/route'; import { useBackWithFallback } from '@/hooks/useBackWithFallback'; -import { useGetWishlist } from '@/hooks/useGetWishlist'; +import { useGetFolders } from '@/hooks/useGetFolders'; +import useIntersectionObserver from '@/hooks/useIntersectionObserver'; import { useGetTournament } from '../../../_common/_hooks/useGetTournament'; import { MAX_SELECT } from '../_consts/selectLimits'; +import { useFolderWishes } from '../_hooks/useFolderWishes'; import { usePostTournamentItemsByWish } from '../_hooks/usePostTournamentItemsByWish'; -import useWishSelection from '../_hooks/useWishSelection'; -import WishSelectCard from './WishSelectCard'; -import WishSelectHeader from './WishSelectHeader'; +import FolderChipFilter from './FolderChipFilter'; +import NoWishDialog from './NoWishDialog'; type ByWishContentProps = { tournamentId: number; }; -const noop = () => {}; - function ByWishContent({ tournamentId }: ByWishContentProps) { const backWithFallback = useBackWithFallback(); - const { selectedIds, isMaxExceeded, handleSelect } = useWishSelection(MAX_SELECT); - const { wishlistData, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetWishlist(); + const [activeFolderId, setActiveFolderId] = useState(null); + /** 칩을 오가며 고른 위시도 담을 수 있게 wishId 와 itemId 를 함께 들고 있음 */ + const [selectedItemIds, setSelectedItemIds] = useState>(new Map()); const { tournamentData } = useGetTournament(tournamentId); const pending = 'pending' in tournamentData ? tournamentData.pending : null; + const existingItemCount = pending?.items.length ?? 0; + const existingItemIds = new Set(pending?.items.map(({ itemId }) => itemId)); + const addableCount = Math.max(MAX_SELECT - existingItemCount, 0); + const isOverSelectionLimit = selectedItemIds.size > addableCount; + + const { foldersData } = useGetFolders(); + const { allWishesData, wishesData, fetchNextPage, hasNextPage, isFetchingNextPage } = + useFolderWishes(activeFolderId); + const bottomRef = useIntersectionObserver(fetchNextPage, hasNextPage && !isFetchingNextPage); + const { postTournamentItemsByWishMutation, isPostTournamentItemsByWishPending } = - usePostTournamentItemsByWish(tournamentId, pending?.items.length ?? 0); + usePostTournamentItemsByWish(tournamentId, existingItemCount); - const existingItemIds = new Set(pending?.items.map(i => i.itemId) ?? []); - const items = wishlistData.filter( + const selectableWishes = wishesData.filter( ({ item }) => item.status === ITEM_STATUS.READY && item.id !== null && !existingItemIds.has(item.id) ); - - const isWishlistLoaded = !hasNextPage && !isFetchingNextPage; - - const hasNoWish = isWishlistLoaded && wishlistData.length === 0; - const hasNoSelectableWish = isWishlistLoaded && !hasNoWish && items.length === 0; - - useEffect(() => { - if (!isMaxExceeded) return; - toast.warning(`최대 ${MAX_SELECT}개까지 상품을 담을 수 있어요.`); - }, [isMaxExceeded]); - - // 위시에서 가져오기는 전체 목록 기준으로 카운트를 표시해야 하므로 마운트 시 전체 로드 - useEffect(() => { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage(); + const hasNoWish = allWishesData.length === 0; + const hasNoSelectableWish = !hasNextPage && selectableWishes.length === 0; + + const handleToggleSelect = (wishId: number) => { + const isSelected = selectedItemIds.has(wishId); + if (!isSelected && selectedItemIds.size >= addableCount) { + toast.warning(`최대 ${addableCount}개까지 상품을 담을 수 있어요.`, { id: 'max-select' }); + return; } - }, [hasNextPage, isFetchingNextPage, fetchNextPage]); - const handleNext = () => { - const itemIds = items - .filter(({ wish }) => selectedIds.includes(wish.id)) - .flatMap(({ item }) => item.id ?? []); - postTournamentItemsByWishMutation(itemIds); + const itemId = selectableWishes.find(({ wish }) => wish.id === wishId)?.item.id; + setSelectedItemIds(prev => { + const next = new Map(prev); + if (isSelected) next.delete(wishId); + else if (typeof itemId === 'number') next.set(wishId, itemId); + return next; + }); }; + const handleNext = () => postTournamentItemsByWishMutation(Array.from(selectedItemIds.values())); + const handleBackToCreate = () => backWithFallback(ROUTES.TOURNAMENT_CREATE(tournamentId)); - if (hasNoWish) { - return ( -
- -
- ); - } + if (hasNoWish) return ; return ( -
-
+
+
} center={

내 위시에서 가져오기

} /> -
-
+
+ {foldersData.length > 0 && ( + + )} +

+ {selectedItemIds.size}/{addableCount}개 선택 +

+
+ +
{hasNoSelectableWish ? ( -
-
-

- 담을 수 있는 위시가 없어요 -

+
+

+ 담을 수 있는 위시가 없어요 +

+ {activeFolderId === null && (

위시가 모두 후보에 담겨 있어요.

-
+ )}
) : ( -
- {items.map(({ wish, item }) => ( - handleSelect(wish.id)} - /> - ))} -
+ )} +
- ); -} - -export default WishSelectCard; diff --git a/apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectHeader.tsx b/apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectHeader.tsx deleted file mode 100644 index eb48cf3e0..000000000 --- a/apps/web/src/app/tournament/[id]/create/by-wish/_components/WishSelectHeader.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { MAX_SELECT } from '../_consts/selectLimits'; - -type WishSelectHeaderProps = { - selectedCount: number; - totalCount: number; - tournamentCandidateCount: number; - isMaxExceeded: boolean; -}; - -function WishSelectHeader({ - selectedCount, - totalCount, - tournamentCandidateCount, - isMaxExceeded, -}: WishSelectHeaderProps) { - const renderSubtitle = () => { - if (isMaxExceeded) - return ( -

- 최대 {MAX_SELECT}개까지 선택할 수 있어요 -

- ); - - return ( -

- {`후보가 ${tournamentCandidateCount}개 담겨있어요.\n후보로 추가할 상품을 선택해주세요.`} -

- ); - }; - - return ( -
- {renderSubtitle()} -

- {selectedCount} - /{totalCount}개 선택 -

-
- ); -} - -export default WishSelectHeader; diff --git a/apps/web/src/app/tournament/[id]/create/by-wish-v2/_hooks/useFolderWishes.ts b/apps/web/src/app/tournament/[id]/create/by-wish/_hooks/useFolderWishes.ts similarity index 100% rename from apps/web/src/app/tournament/[id]/create/by-wish-v2/_hooks/useFolderWishes.ts rename to apps/web/src/app/tournament/[id]/create/by-wish/_hooks/useFolderWishes.ts diff --git a/apps/web/src/app/tournament/[id]/create/by-wish/_hooks/useWishSelection.ts b/apps/web/src/app/tournament/[id]/create/by-wish/_hooks/useWishSelection.ts deleted file mode 100644 index d01a59984..000000000 --- a/apps/web/src/app/tournament/[id]/create/by-wish/_hooks/useWishSelection.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useEffect, useState } from 'react'; - -// 위시 아이템 선택 상태 및 최대 선택 초과 토스트 노출 여부를 관리하는 훅 -const useWishSelection = (maxSelect: number) => { - const [selectedIds, setSelectedIds] = useState([]); - // isAtMax(계산값)와 분리: 초과 시도 시에만 true, 선택 해제 시 즉시 false - const [isMaxExceeded, setIsMaxExceeded] = useState(false); - - const isAtMax = selectedIds.length >= maxSelect; - - const handleSelect = (id: number) => { - if (selectedIds.includes(id)) { - setSelectedIds(prev => prev.filter(i => i !== id)); - setIsMaxExceeded(false); - return; - } - if (isAtMax) { - setIsMaxExceeded(true); - return; - } - setSelectedIds(prev => [...prev, id]); - }; - - // 토스트 2초 후 자동 소멸 - useEffect(() => { - if (!isMaxExceeded) return; - const timer = setTimeout(() => setIsMaxExceeded(false), 2000); - return () => clearTimeout(timer); - }, [isMaxExceeded]); - - return { selectedIds, isMaxExceeded, isAtMax, handleSelect }; -}; - -export default useWishSelection; From 095dab2d8b794165d7b217562575b7617031a99e Mon Sep 17 00:00:00 2001 From: joyeongchan Date: Mon, 2 Nov 2026 19:53:18 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20=ED=86=A0=EB=84=88=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20=EC=9C=84=EC=8B=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=84=EC=8B=9C=20=EA=B2=BD=EB=A1=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - by-wish-v2 페이지 · TOURNAMENT_ADD_ITEM_BY_WISH_V2 삭제 - getRouteType 의 v2 경로와 테스트 정리 --- .../[id]/create/by-wish-v2/page.tsx | 33 ------------------- apps/web/src/consts/route.ts | 3 -- apps/web/src/utils/getRouteType.test.ts | 1 - apps/web/src/utils/getRouteType.ts | 3 +- 4 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 apps/web/src/app/tournament/[id]/create/by-wish-v2/page.tsx diff --git a/apps/web/src/app/tournament/[id]/create/by-wish-v2/page.tsx b/apps/web/src/app/tournament/[id]/create/by-wish-v2/page.tsx deleted file mode 100644 index 86e13d38e..000000000 --- a/apps/web/src/app/tournament/[id]/create/by-wish-v2/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { notFound } from 'next/navigation'; - -import { Header, HeaderIcon } from '@/components/header'; - -import ByWishV2Content from './_components/ByWishV2Content'; - -type ByWishV2PageProps = { - params: Promise<{ id: string }>; -}; - -/** TEMP: 폴더 칩 작업용 임시 경로. 완료 후 by-wish 로 합치고 삭제함 */ -async function ByWishV2Page({ params }: ByWishV2PageProps) { - if (process.env.NEXT_PUBLIC_STAGE === 'production') notFound(); - - const { id } = await params; - const tournamentId = Number(id); - - return ( -
-
-
} - center={ -

내 위시에서 가져오기

- } - /> -
- -
- ); -} - -export default ByWishV2Page; diff --git a/apps/web/src/consts/route.ts b/apps/web/src/consts/route.ts index 3b751f971..231aff358 100644 --- a/apps/web/src/consts/route.ts +++ b/apps/web/src/consts/route.ts @@ -38,9 +38,6 @@ export const ROUTES = { : `/tournament/${tournamentId}/create`, TOURNAMENT_ADD_ITEM_BY_WISH: (tournamentId: number) => `/tournament/${tournamentId}/create/by-wish`, - /** TEMP: 폴더 칩 작업 완료 후 TOURNAMENT_ADD_ITEM_BY_WISH 로 합침 */ - TOURNAMENT_ADD_ITEM_BY_WISH_V2: (tournamentId: number) => - `/tournament/${tournamentId}/create/by-wish-v2`, TOURNAMENT_ITEM_EDIT: (tournamentId: number, itemId: number) => `/tournament/${tournamentId}/item/${itemId}`, TOURNAMENT_LOADING: (tournamentId: number) => `/tournament/${tournamentId}/loading`, diff --git a/apps/web/src/utils/getRouteType.test.ts b/apps/web/src/utils/getRouteType.test.ts index 1b8360569..f03ab0b06 100644 --- a/apps/web/src/utils/getRouteType.test.ts +++ b/apps/web/src/utils/getRouteType.test.ts @@ -40,7 +40,6 @@ describe('getRouteType', () => { it('선택 하위 세그먼트는 정해진 것만 붙는다', () => { expect(getRouteType('/tournament/1/create')).toBe('AUTHORIZED'); expect(getRouteType('/tournament/1/create/by-wish')).toBe('AUTHORIZED'); - expect(getRouteType('/tournament/1/create/by-wish-v2')).toBe('AUTHORIZED'); expect(getRouteType('/tournament/1/create/other')).toBeNull(); expect(getRouteType('/tournament/1/result')).toBe('AUTHORIZED'); diff --git a/apps/web/src/utils/getRouteType.ts b/apps/web/src/utils/getRouteType.ts index 432f99421..1f065551e 100644 --- a/apps/web/src/utils/getRouteType.ts +++ b/apps/web/src/utils/getRouteType.ts @@ -3,8 +3,7 @@ import type { RouteTypeT } from '@/types/route'; /** 토너먼트 참여 권한이 필요한 동적 라우트 패턴 */ const AUTHORIZED_ROUTE_PATTERNS = [ - /** TEMP: by-wish-v2 는 폴더 칩 작업용 */ - /^\/tournament\/[^/]+\/create(?:\/by-wish(?:-v2)?)?$/, + /^\/tournament\/[^/]+\/create(?:\/by-wish)?$/, /^\/tournament\/[^/]+\/item\/[^/]+$/, /^\/tournament\/[^/]+\/loading$/, /^\/tournament\/[^/]+\/match$/, From baa03740fd468ad8abe3115d222f8e7534e2bf9e Mon Sep 17 00:00:00 2001 From: joyeongchan Date: Mon, 2 Nov 2026 19:53:18 +0900 Subject: [PATCH 3/3] =?UTF-8?q?test:=20=ED=86=A0=EB=84=88=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20=EC=9C=84=EC=8B=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=ED=8F=B4=EB=8D=94=20=EC=B9=A9=20e2e=20=EC=8B=9C?= =?UTF-8?q?=EB=82=98=EB=A6=AC=EC=98=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 칩을 오가며 고른 위시를 함께 담기 - 이미 후보에 담긴 상품 제외 - 기존 담기 시나리오에 폴더 목록 목 추가 --- .../tournament/tournamentItemAdd.spec.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/apps/web/e2e/specs/tournament/tournamentItemAdd.spec.ts b/apps/web/e2e/specs/tournament/tournamentItemAdd.spec.ts index 4d4593a2b..eda42a284 100644 --- a/apps/web/e2e/specs/tournament/tournamentItemAdd.spec.ts +++ b/apps/web/e2e/specs/tournament/tournamentItemAdd.spec.ts @@ -1,6 +1,7 @@ import { ENDPOINTS } from '@/consts/api'; import { expect, test } from '@e2e/fixtures/mockApiFixture'; +import { MOCK_FOLDER, MOCK_FOLDERS } from '@e2e/mocks/folder'; import { MOCK_MEMBER_ME } from '@e2e/mocks/me'; import { MOCK_TOURNAMENT_ITEMS, @@ -48,6 +49,7 @@ test('위시에서 상품 4개를 가져오면 장바구니에 담긴다', async api.get(ENDPOINTS.USER, MOCK_MEMBER_ME); api.get(ENDPOINTS.TOURNAMENT(1), MOCK_TOURNAMENT_PENDING); api.getPage(ENDPOINTS.WISHLISTS, MOCK_WISHLIST_ENTRIES); + api.get(ENDPOINTS.FOLDERS, MOCK_FOLDERS); api.post(ENDPOINTS.TOURNAMENT_ITEMS_FROM_WISH(1), { tournamentItemIds: MOCK_TOURNAMENT_ITEMS.map(item => item.tournamentItemId), }); @@ -85,6 +87,57 @@ test('위시에서 상품 4개를 가져오면 장바구니에 담긴다', async await expect(page.getByAltText('토너먼트 아이템 4')).toBeVisible(); }); +test('폴더 칩을 오가며 고른 위시를 함께 후보로 담는다', async ({ page, api }) => { + const [FIRST_ENTRY, SECOND_ENTRY] = MOCK_WISHLIST_ENTRIES; + + api.get(ENDPOINTS.USER, MOCK_MEMBER_ME); + api.get(ENDPOINTS.TOURNAMENT(1), MOCK_TOURNAMENT_PENDING); + api.getPage(ENDPOINTS.WISHLISTS, MOCK_WISHLIST_ENTRIES); + api.get(ENDPOINTS.FOLDERS, MOCK_FOLDERS); + api.post(ENDPOINTS.TOURNAMENT_ITEMS_FROM_WISH(1), { tournamentItemIds: [11, 12] }); + + await page.goto('/tournament/1/create/by-wish'); + await page.getByRole('button', { name: FIRST_ENTRY?.item.name ?? '' }).click(); + + const folderRequest = page.waitForRequest( + request => + request.url().includes(ENDPOINTS.WISHLISTS) && + new URL(request.url()).searchParams.get('folderId') === String(MOCK_FOLDER.id) + ); + await page + .getByRole('group', { name: '폴더 필터' }) + .getByRole('button', { name: MOCK_FOLDER.name }) + .click(); + await folderRequest; + await page.getByRole('button', { name: SECOND_ENTRY?.item.name ?? '' }).click(); + await expect(page.getByText('2/32개 선택')).toBeVisible(); + + const addRequest = page.waitForRequest( + request => + request.method() === 'POST' && request.url().includes(ENDPOINTS.TOURNAMENT_ITEMS_FROM_WISH(1)) + ); + await page.getByRole('button', { name: '다음' }).click(); + + const { itemIds } = (await addRequest).postDataJSON() as { itemIds: number[] }; + expect([...itemIds].sort()).toEqual([FIRST_ENTRY?.item.id, SECOND_ENTRY?.item.id].sort()); +}); + +test('이미 후보에 담긴 상품은 위시 목록에서 빠진다', async ({ page, api }) => { + const [ALREADY_ADDED, ...REST] = MOCK_WISHLIST_ENTRIES; + + api.get(ENDPOINTS.USER, MOCK_MEMBER_ME); + api.getPage(ENDPOINTS.WISHLISTS, MOCK_WISHLIST_ENTRIES); + api.get(ENDPOINTS.FOLDERS, MOCK_FOLDERS); + + await page.goto('/tournament/11/create/by-wish'); + + await expect(page.getByText('0/31개 선택')).toBeVisible(); + await expect(page.getByText(ALREADY_ADDED?.item.name ?? '', { exact: true })).toHaveCount(0); + for (const { item } of REST) { + await expect(page.getByText(item.name ?? '', { exact: true })).toBeVisible(); + } +}); + /** * 아래 가드 테스트들은 담긴 개수별로 토너먼트 id 를 분리해 SSR 목을 태운다 (11/13/14) — * 클라이언트가 staleTime 동안 레이아웃의 SSR 시드를 그대로 쓰기 때문에