Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/web/e2e/helpers/apiResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const createApiSuccess = <T>(data: T, status = 200): ApiResponseT<T> => (
data,
detail: '요청이 정상적으로 처리되었습니다.',
code: 'COMMON_SUCCESS',
pageResponse: {
nextCursor: null,
hasNext: false,
},
});

type CreateApiErrorOptionsT = {
Expand Down
15 changes: 5 additions & 10 deletions apps/web/e2e/mocks/wish.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
import type { ItemT } from '@/types/item';
import type { WishT } from '@/types/wish';
import type { GetWishlistResponseT } from '@/types/wish';

import { MOCK_IMAGE_URLS } from './images';
import { MOCK_TOURNAMENT_ITEMS } from './tournament';

/** getWishlist 원 응답의 data 항목 형태 (apis/getWishlist.ts 의 WishlistEntryT 와 동일) */
type WishlistEntryT = {
wish: WishT;
item: ItemT;
};

/**
* by-wish 담기 spec 용 위시 4개.
* item.id 를 토너먼트 목의 itemId 와 맞춰, 담기 후 pending.items 와 자연스럽게 이어지게 한다.
*/
export const MOCK_WISHLIST_ENTRIES: WishlistEntryT[] = MOCK_TOURNAMENT_ITEMS.map(
export const MOCK_WISHLIST_ENTRIES: GetWishlistResponseT[] = MOCK_TOURNAMENT_ITEMS.map(
(tournamentItem, index) => ({
wish: { id: index + 1, createdAt: '2026-01-01T00:00:00Z' },
item: {
id: tournamentItem.itemId,
status: 'READY',
name: tournamentItem.name,
currentPrice: tournamentItem.price,
price: tournamentItem.price,
currency: 'KRW',
imageUrl: MOCK_IMAGE_URLS.product,
sourceUrl: null,
sourcePlatform: null,
},
reused: null,
refreshNeeded: null,
})
);
57 changes: 13 additions & 44 deletions apps/web/src/apis/getWishlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,23 @@ import { clientApi } from '@/apis/client';
import { serverApi } from '@/apis/server';
import { ENDPOINTS } from '@/consts/api';
import type { ApiResponseT } from '@/types/api';
import type { ItemT } from '@/types/item';
import type { WishItemT, WishT } from '@/types/wish';
import type { GetWishlistResponseT } from '@/types/wish';

type WishlistEntryT = {
wish: WishT;
item: ItemT;
};

type WishlistApiResponseT = ApiResponseT<WishlistEntryT[]> & {
pageResponse: {
nextCursor: string | null;
hasNext: boolean;
};
};

export type WishlistPageT = {
items: WishItemT[];
nextCursor: string | null;
hasNext: boolean;
};

const mapWishlist = (entries: WishlistEntryT[]): WishItemT[] =>
entries.map(({ wish, item }) => ({
id: wish.id,
itemId: item.id,
status: item.status,
name: item.name ?? '',
price: item.currentPrice ?? 0,
imageUrl: item.imageUrl ?? null,
sourcePlatform: item.sourcePlatform ?? null,
}));

export const getWishlist = async (cursor: string | null = null): Promise<WishlistPageT> => {
export const getWishlist = async (cursor: string | null = null) => {
const params = { size: 20, ...(cursor ? { cursor } : {}) };

if (environmentManager.isServer()) {
const { data } = await serverApi.get<WishlistApiResponseT>(ENDPOINTS.WISHLISTS, { params });
return {
items: mapWishlist(data.data),
nextCursor: data.pageResponse.nextCursor,
hasNext: data.pageResponse.hasNext,
};
const { data } = await serverApi.get<ApiResponseT<GetWishlistResponseT[]>>(
ENDPOINTS.WISHLISTS,
{
params,
}
);
return data;
}

const { data } = await clientApi.get<WishlistApiResponseT>(ENDPOINTS.WISHLISTS, { params });
return {
items: mapWishlist(data.data),
nextCursor: data.pageResponse.nextCursor,
hasNext: data.pageResponse.hasNext,
};
const { data } = await clientApi.get<ApiResponseT<GetWishlistResponseT[]>>(ENDPOINTS.WISHLISTS, {
params,
});
return data;
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { Header, HeaderIcon } from '@/components/header';
import ItemLinkBanner from '@/components/common/item-link-banner';
import { Header, HeaderIcon } from '@/components/header';

import { useGetWish } from '../_hooks/useGetWish';
import ItemEditForm from './ItemEditForm';
Expand All @@ -24,9 +24,9 @@ function EditContent({ wishId }: EditContentProps) {
<ItemEditForm
wishId={wishId}
itemStatus={wishData.item.status}
initialImageUrl={wishData.item.status === 'READY' ? wishData.item.imageUrl : null}
initialName={wishData.item.status === 'READY' ? wishData.item.name : ''}
initialPrice={wishData.item.status === 'READY' ? wishData.item.currentPrice : 0}
initialImageUrl={wishData.item.imageUrl}
initialName={wishData.item.name ?? ''}
initialPrice={wishData.item.price ?? 0}
/>

{wishData.item.sourceUrl && <ItemLinkBanner href={wishData.item.sourceUrl} />}
Expand Down
9 changes: 4 additions & 5 deletions apps/web/src/app/archive/wish/[id]/_hooks/usePatchWish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ export const usePatchWish = (wishId: number) => {
const queryClient = useQueryClient();

const { mutate: patchWishMutation, isPending: isPatchWishPending } = useMutation({
mutationFn: (body: Omit<PatchItemRequestT, 'currency'>) => {
mutationFn: (body: PatchItemRequestT) => {
const formData = new FormData();
formData.append('name', body.name);
formData.append('currentPrice', String(body.currentPrice));
formData.append('currency', 'KRW');
formData.append('image', body.image);
if (body.name) formData.append('name', body.name);
if (body.price) formData.append('price', String(body.price));
if (body.image) formData.append('image', body.image);
return patchWish(wishId, formData);
},
onSuccess: () => {
Expand Down
16 changes: 14 additions & 2 deletions apps/web/src/app/archive/wish/[id]/_types/wish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,31 @@ export type GetWishResponseT = {
status: (typeof ITEM_STATUS)['PROCESSING'] | (typeof ITEM_STATUS)['FAILED'];
name: null;
imageUrl: null;
currentPrice: null;
price: null;
currency: null;
sourceUrl: string | null; // 확인필
}
| {
status: (typeof ITEM_STATUS)['READY'] | (typeof ITEM_STATUS)['PENDING'];
name: string;
imageUrl: string;
currentPrice: number;
price: number;
currency: string;
sourceUrl: string | null;
}
);
/**
* 갱신 필요 여부
* - 이미지로 등록한 경우: null
* - 링크로 등록한 경우: boolean
*/
refreshNeeded: boolean | null;
/**
* 재사용 여부
* - 이미지로 등록한 경우: null
* - 링크로 등록한 경우: boolean
*/
reused: boolean | null;
};

export type PatchWishResponseT = GetWishResponseT;
13 changes: 8 additions & 5 deletions apps/web/src/app/archive/wish/_components/WishCardSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import Skeleton from '@/components/skeleton';

function WishCardSkeleton() {
return (
<div className="flex flex-col overflow-hidden bg-white">
<Skeleton className="h-[143px] w-full rounded-none" />
<div className="flex flex-col items-center gap-[9px] px-3 py-3">
<Skeleton className="h-4 w-3/4 rounded-none" />
<Skeleton className="h-5 w-1/2 rounded-none" />
<div className="flex flex-col overflow-hidden bg-bg-layer-basement">
<Skeleton className="aspect-[201/166] w-full rounded-none" />
<div className="flex h-[124px] flex-col items-start gap-2.5 self-stretch p-4">
<div className="flex flex-col gap-1 self-stretch">
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-1/2" />
</div>
<Skeleton className="h-5 w-12 rounded-[4px]" />
</div>
</div>
);
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/app/archive/wish/_components/WishContent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';

import type { WishlistPageT } from '@/apis/getWishlist';
import { getWishlist } from '@/apis/getWishlist';
import type { ApiResponseT } from '@/types/api';
import type { GetWishlistResponseT } from '@/types/wish';
import { getQueryClient } from '@/utils/queryClient';

import WishContentClient from './WishContentClient';
Expand All @@ -11,9 +12,10 @@ async function WishContent() {

await queryClient.prefetchInfiniteQuery({
queryKey: ['wishlists'],
queryFn: ({ pageParam }) => getWishlist(pageParam as string | null),
queryFn: ({ pageParam }) => getWishlist(pageParam),
initialPageParam: null as string | null,
getNextPageParam: (page: WishlistPageT) => (page.hasNext ? page.nextCursor : null),
getNextPageParam: (page: ApiResponseT<GetWishlistResponseT[]>) =>
page.pageResponse.hasNext ? page.pageResponse.nextCursor : null,
});

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ function WishContentClient() {
const { wishlistData } = useGetWishlist();
const selectableIds = wishlistData
.filter(
item =>
({ item }) =>
item.status !== 'FAILED' && item.status !== 'PENDING' && item.status !== 'PROCESSING'
)
.map(item => item.id);
.map(({ wish }) => wish.id);
const isAllSelected =
selectableIds.length > 0 && selectableIds.every(id => selectedIds.has(id));

Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/archive/wish/_components/WishGridContent.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HeartIconFill } from '@/assets/icons';
import type { WishItemT } from '@/types/wish';
import type { GetWishlistResponseT } from '@/types/wish';

import WishGrid from './wish-grid';

type WishGridContentProps = {
items: WishItemT[];
items: GetWishlistResponseT[];
isDeleteMode?: boolean;
selectedIds?: Set<number>;
onToggleSelect?: (id: number) => void;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/archive/wish/_components/WishlistList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function WishlistList({ isDeleteMode, selectedIds, onToggleSelect }: WishlistLis
const sentinelRef = useRef<HTMLDivElement>(null);

const { wishlistData, fetchNextPage, hasNextPage, isFetchingNextPage } = useGetWishlist();
const hasPendingItem = hasParsingItems(wishlistData);
const hasPendingItem = hasParsingItems(wishlistData.map(({ item }) => item));

useSSEFallback(['wishlists'], hasPendingItem);
useScrollRestoration();
Expand Down
32 changes: 16 additions & 16 deletions apps/web/src/app/archive/wish/_components/wish-grid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { CheckboxEmptyIconFill, CheckboxSelectedIconFill } from '@/assets/icons'
import WishCard from '@/components/common/wish-card';
import { ROUTES } from '@/consts/route';
import { Z_INDEX } from '@/consts/zIndex';
import type { WishItemT } from '@/types/wish';
import type { GetWishlistResponseT } from '@/types/wish';

import { saveWishScroll } from '../../_utils/wishScroll';
import WishFailedCard from './WishFailedCard';
import WishProcessingCard from './WishProcessingCard';

type WishGridProps = {
items: WishItemT[];
items: GetWishlistResponseT[];
isDeleteMode?: boolean;
selectedIds?: Set<number>;
onToggleSelect?: (id: number) => void;
Expand All @@ -28,28 +28,28 @@ function WishGrid({ items, isDeleteMode = false, selectedIds, onToggleSelect }:

return (
<div className="grid grid-cols-2">
{items.map((item, index) => {
{items.map(({ wish, item }, index) => {
if (item.status === 'FAILED')
return (
<Link
href={ROUTES.WISH_EDIT(item.id)}
key={item.id}
data-wish-id={item.id}
onClick={event => handleCardClick(event, item.id)}
href={ROUTES.WISH_EDIT(wish.id)}
key={wish.id}
data-wish-id={wish.id}
onClick={event => handleCardClick(event, wish.id)}
>
<WishFailedCard key={item.id} />
<WishFailedCard />
</Link>
);
else if (item.status === 'PENDING' || item.status === 'PROCESSING')
return <WishProcessingCard key={item.id} />;
return <WishProcessingCard key={wish.id} />;

if (isDeleteMode) {
const isSelected = selectedIds?.has(item.id) ?? false;
const isSelected = selectedIds?.has(wish.id) ?? false;
return (
<button
key={item.id}
key={wish.id}
type="button"
onClick={() => onToggleSelect?.(item.id)}
onClick={() => onToggleSelect?.(wish.id)}
aria-pressed={isSelected}
className="relative cursor-pointer text-left transition-opacity active:opacity-80"
>
Expand Down Expand Up @@ -79,10 +79,10 @@ function WishGrid({ items, isDeleteMode = false, selectedIds, onToggleSelect }:

return (
<Link
href={ROUTES.WISH_EDIT(item.id)}
key={item.id}
data-wish-id={item.id}
onClick={event => handleCardClick(event, item.id)}
href={ROUTES.WISH_EDIT(wish.id)}
key={wish.id}
data-wish-id={wish.id}
onClick={event => handleCardClick(event, wish.id)}
>
<WishCard
name={item.name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,8 @@ function ByWishContent({ tournamentId }: ByWishContentProps) {
const pending = 'pending' in tournamentData ? tournamentData.pending : null;
const existingItemIds = new Set(pending?.items.map(i => i.itemId) ?? []);
const items = wishlistData.filter(
item =>
item.status !== 'FAILED' &&
item.status !== 'PROCESSING' &&
item.itemId != null &&
!existingItemIds.has(item.itemId)
({ item }) =>
item.status !== 'FAILED' && item.status !== 'PROCESSING' && !existingItemIds.has(item.id)
);

useEffect(() => {
Expand All @@ -50,8 +47,8 @@ function ByWishContent({ tournamentId }: ByWishContentProps) {

const handleNext = () => {
const itemIds = items
.filter(item => selectedIds.includes(item.id))
.map(item => item.itemId as number);
.filter(({ wish }) => selectedIds.includes(wish.id))
.map(({ item }) => item.id);
postTournamentItemsByWishMutation(itemIds);
};

Expand All @@ -60,7 +57,9 @@ function ByWishContent({ tournamentId }: ByWishContentProps) {
<div className="px-5">
<Header
left={<HeaderIcon name="BACK" />}
center={<h1 className="heading-1-bold text-text-neutral-primary">내 위시에서 가져오기</h1>}
center={
<h1 className="heading-1-bold text-text-neutral-primary">내 위시에서 가져오기</h1>
}
/>
<WishSelectHeader
selectedCount={selectedIds.length}
Expand All @@ -72,15 +71,15 @@ function ByWishContent({ tournamentId }: ByWishContentProps) {

<main className="mt-6 hide-scrollbar flex flex-1 flex-col overflow-y-auto pb-32">
<div className="grid grid-cols-2">
{items.map(item => (
{items.map(({ wish, item }) => (
<WishSelectCard
key={item.id}
key={wish.id}
name={item.name}
price={item.price}
imageUrl={item.imageUrl}
sourcePlatform={item.sourcePlatform}
isSelected={selectedIds.includes(item.id)}
onSelect={() => handleSelect(item.id)}
isSelected={selectedIds.includes(wish.id)}
onSelect={() => handleSelect(wish.id)}
/>
))}
</div>
Expand Down
Loading
Loading