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
Binary file added apps/web/public/mock/wish/wish-1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/mock/wish/wish-2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/mock/wish/wish-3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/mock/wish/wish-4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/mock/wish/wish-5.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/mock/wish/wish-6.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CheckboxEmptyIconFill, CheckboxSelectedIconFill } from '@/assets/icons';
import WishCard from '@/components/common/wish-card';
import { ITEM_STATUS } from '@/consts/item';
import { Z_INDEX } from '@/consts/zIndex';

type WishSelectCardProps = {
name: string | null;
Expand Down Expand Up @@ -33,7 +34,10 @@ function WishSelectCard({
imageUrl={imageUrl}
sourcePlatform={sourcePlatform}
/>
<span className="pointer-events-none absolute top-3 left-3 z-10 block size-5">
<span
className="pointer-events-none absolute top-3 left-3 block size-5"
style={{ zIndex: Z_INDEX.BASE_IMAGE + 1 }}
>
{isSelected ? (
<CheckboxSelectedIconFill className="size-5 text-bg-accent" />
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Skeleton from '@/components/skeleton';
import { Z_INDEX } from '@/consts/zIndex';

type MatchSkeletonProps = {
isFinal?: boolean;
Expand All @@ -25,8 +26,8 @@ function MatchSkeleton({ isFinal = false }: MatchSkeletonProps) {
))}

<div
className="absolute left-1/2 z-10 flex size-8 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-bg-neutral-secondary text-[12.026px] leading-[17.18px] font-semibold tracking-[-0.515px] text-white"
style={{ top: isFinal ? 61.5 : 156 }}
className="absolute left-1/2 flex size-8 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-bg-neutral-secondary text-[12.026px] leading-[17.18px] font-semibold tracking-[-0.515px] text-white"
style={{ zIndex: Z_INDEX.BASE_IMAGE + 1, top: isFinal ? 61.5 : 156 }}
>
VS
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { useCallback, useEffect, useState } from 'react';
import type { CSSProperties } from 'react';

import { Z_INDEX } from '@/consts/zIndex';

import type { TransitionStageT } from '../_utils/rounds';
import './RoundTransitionSheet.css';

Expand Down Expand Up @@ -316,7 +318,8 @@ function RoundTransitionSheet({ stage, onComplete }: RoundTransitionSheetProps)
<>
{/* Backdrop */}
<div
className={`fixed inset-0 z-40 cursor-pointer bg-bg-layer-overlay ${isExiting ? 'rs-dim-exit' : 'rs-dim'}`}
className={`fixed inset-0 cursor-pointer bg-bg-layer-overlay ${isExiting ? 'rs-dim-exit' : 'rs-dim'}`}
style={{ zIndex: Z_INDEX.DRAWER }}
aria-hidden
onClick={handleSkip}
/>
Expand All @@ -326,8 +329,9 @@ function RoundTransitionSheet({ stage, onComplete }: RoundTransitionSheetProps)
role="dialog"
aria-modal="true"
aria-label={title}
className={`fixed bottom-0 left-1/2 z-50 w-full max-w-120 -translate-x-1/2 cursor-pointer overflow-hidden ${isExiting ? 'rs-sheet-exit' : 'rs-sheet'}`}
className={`fixed bottom-0 left-1/2 w-full max-w-120 -translate-x-1/2 cursor-pointer overflow-hidden ${isExiting ? 'rs-sheet-exit' : 'rs-sheet'}`}
style={{
zIndex: Z_INDEX.DRAWER,
height: 540,
borderRadius: '24px 24px 0 0',
background: 'linear-gradient(180deg, #ECF8FE 0%, #FFF 50%)',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';

import { Z_INDEX } from '@/consts/zIndex';

import type { ProductT } from '../../_common/_types/tournament';
import useCardSelectionAnimation from '../_hooks/useCardSelectionAnimation';
import ProductCard from './ProductCard';
Expand Down Expand Up @@ -92,8 +94,9 @@ function VsSection({ left, right, isFinal = false, onSelect }: VsSectionProps) {

{/* VS 뱃지 */}
<div
className={`z-10 flex size-8 shrink-0 items-center justify-center rounded-full bg-bg-neutral-secondary text-[12.026px] leading-[17.18px] font-semibold tracking-[-0.515px] text-white ${transition}`}
className={`flex size-8 shrink-0 items-center justify-center rounded-full bg-bg-neutral-secondary text-[12.026px] leading-[17.18px] font-semibold tracking-[-0.515px] text-white ${transition}`}
style={{
zIndex: Z_INDEX.BASE_IMAGE + 1,
marginTop: isFinal ? IMAGE_HEIGHT / 2 - 16 : HOOK_HEIGHT + IMAGE_HEIGHT - 16,
filter: selectedSide ? 'blur(2px)' : 'none',
...duration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useScrollRestoration } from '@/hooks/useScrollRestoration';
import type { TournamentPlayTypeT, TournamentStatusT } from '@/types/tournament';
import { SCROLL_NAMESPACE } from '@/utils/scrollRestoration';

import type { TournamentStatusTabT } from '../_consts/tournamentTab';
import type { TournamentStatusTabT } from '../_types/tournamentTab';

type Props = {
statuses: TournamentStatusT[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { useSearchParams } from 'next/navigation';
import { Suspense } from 'react';

import Spacing from '@/components/spacing';
import { Z_INDEX } from '@/consts/zIndex';
import { replaceUrlKeepingState } from '@/utils/replaceUrlKeepingState';

import type { TournamentPlayTypeFilterT } from '../_consts/tournamentPlayType';
import { PLAY_TYPE_BY_FILTER, PLAY_TYPE_FILTER } from '../_consts/tournamentPlayType';
import { STATUS_BY_TAB, type TournamentStatusTabT } from '../_consts/tournamentTab';
import { STATUS_BY_TAB } from '../_consts/tournamentTab';
import type { TournamentPlayTypeFilterT } from '../_types/tournamentPlayType';
import type { TournamentStatusTabT } from '../_types/tournamentTab';
import { parsePlayParam, parseTabParam } from '../_utils/tournamentSearchParams';
import TournamentHistoryList from './TournamentHistoryList';
import TournamentHistorySkeleton from './TournamentHistorySkeleton';
Expand All @@ -19,15 +22,8 @@ function TournamentHistorySection() {
const activeTab = parseTabParam(searchParams.get('tab'));
const activePlayTypeFilter = parsePlayParam(searchParams.get('play'));

/**
* 서버 왕복 없이 주소만 동기화.
* 스크롤 복원 키가 history.state 에 저장되므로 기존 state 를 반드시 보존한다.
*/
const syncSearchParams = (tab: TournamentStatusTabT, play: TournamentPlayTypeFilterT) => {
const { __NA, _N, __PRIVATE_NEXTJS_INTERNALS_TREE, ...customState } =
(window.history.state as Record<string, unknown> | null) ?? {};

window.history.replaceState(customState, '', `?tab=${tab}&play=${play}`);
replaceUrlKeepingState(`?tab=${tab}&play=${play}`);
};

/** 탭을 옮기면 플레이 유형 필터는 항상 '전체'로 초기화 */
Expand All @@ -41,7 +37,10 @@ function TournamentHistorySection() {

return (
<>
<div className="sticky top-0 z-20 flex w-full flex-col bg-bg-layer-basement pt-padding-top pb-2">
<div
className="sticky top-0 flex w-full flex-col bg-bg-layer-basement pt-padding-top pb-2"
style={{ zIndex: Z_INDEX.STICKY_HEADER }}
>
<h1 className="heading-1-bold text-text-neutral-primary">내 토너먼트</h1>
<Spacing size={16} />
<TournamentStatusTab activeTab={activeTab} onTabChange={handleTabChange} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import type { ComponentType, SVGProps } from 'react';
import { GroupIconFill, PersonIconFill } from '@/assets/icons';
import { cn } from '@/utils/cn';

import { PLAY_TYPE_FILTER, type TournamentPlayTypeFilterT } from '../_consts/tournamentPlayType';
import { PLAY_TYPE_FILTER } from '../_consts/tournamentPlayType';
import type { TournamentPlayTypeFilterT } from '../_types/tournamentPlayType';

type Props = {
activeFilter: TournamentPlayTypeFilterT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ComponentType, SVGProps } from 'react';
import { BasketIconOutline, ReceiptIconOutline } from '@/assets/icons';
import { cn } from '@/utils/cn';

import type { TournamentStatusTabT } from '../_consts/tournamentTab';
import type { TournamentStatusTabT } from '../_types/tournamentTab';

type Props = {
activeTab: TournamentStatusTabT;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { TOURNAMENT_PLAY_TYPE } from '@/consts/tournament';
import type { TournamentPlayTypeT } from '@/types/tournament';

import type { TournamentPlayTypeFilterT } from '../_types/tournamentPlayType';

export const PLAY_TYPE_FILTER = {
ALL: 'all',
SOLO: 'solo',
SOCIAL: 'social',
} as const;

export type TournamentPlayTypeFilterT = (typeof PLAY_TYPE_FILTER)[keyof typeof PLAY_TYPE_FILTER];

/** NOTE: '전체'는 의도적으로 비워둔다 — 조회 시 playType 파라미터 자체가 빠진다. */
export const PLAY_TYPE_BY_FILTER: Partial<Record<TournamentPlayTypeFilterT, TournamentPlayTypeT>> =
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TOURNAMENT_STATUS } from '@/consts/tournament';
import type { TournamentStatusT } from '@/types/tournament';

export type TournamentStatusTabT = 'ongoing' | 'completed';
import type { TournamentStatusTabT } from '../_types/tournamentTab';

/**
* 탭 → 조회할 토너먼트 상태 매핑
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { PLAY_TYPE_FILTER } from '../_consts/tournamentPlayType';

export type TournamentPlayTypeFilterT = (typeof PLAY_TYPE_FILTER)[keyof typeof PLAY_TYPE_FILTER];
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type TournamentStatusTabT = 'ongoing' | 'completed';
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PLAY_TYPE_FILTER, type TournamentPlayTypeFilterT } from '../_consts/tournamentPlayType';
import type { TournamentStatusTabT } from '../_consts/tournamentTab';
import { PLAY_TYPE_FILTER } from '../_consts/tournamentPlayType';
import type { TournamentPlayTypeFilterT } from '../_types/tournamentPlayType';
import type { TournamentStatusTabT } from '../_types/tournamentTab';

export const parseTabParam = (tab?: string | null): TournamentStatusTabT =>
tab === 'completed' ? 'completed' : 'ongoing';
Expand Down
68 changes: 68 additions & 0 deletions apps/web/src/app/wish/_common/_components/WishCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import ImageIconOutline from '@/assets/icons/outline/image.svg';
import BaseImage from '@/components/base-image';
import Skeleton from '@/components/skeleton';
import formatPrice from '@/utils/formatPrice';

type WishCardProps = {
name: string | null;
price: number | null;
imageUrl: string | null;
sourcePlatform: string | null;
discountRate: number | null;
preload?: boolean;
};

const imageFallback = (
<div className="absolute inset-0 flex items-center justify-center text-gray-200">
<ImageIconOutline width={32} height={32} />
</div>
);

/** 3열 그리드용 위시 카드 — 이미지·상품명·할인율·가격·커머스 칩 */
function WishCard({
name,
price,
imageUrl,
sourcePlatform,
discountRate,
preload = false,
}: WishCardProps) {
return (
<div className="flex flex-col">
<div className="relative aspect-[133/140] w-full bg-gray-75">
{imageUrl ? (
<BaseImage
src={imageUrl}
alt={name ?? ''}
sizes="(max-width: 480px) 34vw, 160px"
preload={preload}
loadingFallback={<Skeleton className="absolute inset-0 rounded-none" />}
errorFallback={imageFallback}
className="object-cover"
/>
) : (
imageFallback
)}
</div>

<div className="flex h-[120px] flex-col items-start gap-2 p-3">
<div className="flex w-full flex-col gap-1">
<p className="line-clamp-2 caption-1-semibold whitespace-pre-line text-text-neutral-primary">
{name}
</p>
<p className="flex gap-1 body-2-semibold">
{discountRate !== null && <span className="text-bg-accent">{discountRate}%</span>}
<span className="truncate text-text-neutral-primary">{formatPrice(String(price))}</span>
</p>
</div>
{sourcePlatform && (
<span className="rounded-[3px] bg-gray-75 px-1 caption-1-regular text-text-neutral-secondary">
{sourcePlatform}
</span>
)}
</div>
</div>
);
}

export default WishCard;
31 changes: 31 additions & 0 deletions apps/web/src/app/wish/_common/_components/WishGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { WishListItemT } from '../_types/wish';
import WishCard from './WishCard';

type WishGridProps = {
wishes: WishListItemT[];
};

/** 이미지 preload 개수 */
const PRELOAD_COUNT = 6;

/** 위시 3열 그리드 **/
function WishGrid({ wishes }: WishGridProps) {
return (
<ul className="grid grid-cols-3 gap-x-[1.5px] gap-y-6">
{wishes.map(({ wish, item }, index) => (
<li key={wish.id}>
<WishCard
name={item.name}
price={item.price}
imageUrl={item.imageUrl}
sourcePlatform={item.sourcePlatform}
discountRate={item.discountRate}
preload={index < PRELOAD_COUNT}
/>
</li>
))}
</ul>
);
}

export default WishGrid;
15 changes: 15 additions & 0 deletions apps/web/src/app/wish/_common/_components/WishSearchLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Link from 'next/link';

import { SearchIconOutline } from '@/assets/icons';
import { ROUTES } from '@/consts/route';

function WishSearchLink() {
return (
// TODO: 경로 변경 필요
<Link href={ROUTES.HOME} aria-label="위시 검색">
<SearchIconOutline className="size-6 text-text-neutral-primary" />
</Link>
);
}

export default WishSearchLink;
28 changes: 28 additions & 0 deletions apps/web/src/app/wish/_common/_consts/wishMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ITEM_STATUS } from '@/consts/item';

import type { WishFolderT, WishListItemT } from '../_types/wish';

// TEMP: API 연동 시 public/mock/wish 이미지와 함께 제거
const MOCK_IMAGE_URLS = [1, 2, 3, 4, 5, 6].map(index => `/mock/wish/wish-${index}.jpg`);

export const MOCK_WISHES: WishListItemT[] = Array.from({ length: 12 }, (_, index) => ({
wish: { id: index + 1, createdAt: '2026-10-14T00:00:00' },
item: {
id: index + 1,
status: ITEM_STATUS.READY,
name:
index % 3 === 1 ? '상품명입니다.' : '상품명입니다.\n2줄 이상 길어지면 이렇게 말줄임 처리돼요',
price: 100000,
currency: 'KRW',
imageUrl: MOCK_IMAGE_URLS[index % MOCK_IMAGE_URLS.length] ?? null,
sourceUrl: null,
sourcePlatform: 'BRAND',
discountRate: index % 4 === 3 ? null : 99,
},
refreshNeeded: null,
reused: null,
}));

export const MOCK_FOLDERS: WishFolderT[] = [
{ id: 1, name: '가방 위시템', wishIds: [4, 1, 2, 3, 5, 6] },
];
17 changes: 17 additions & 0 deletions apps/web/src/app/wish/_common/_types/wish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { ItemT } from '@/types/item';
import type { GetWishlistResponseT } from '@/types/wish';

// TODO: API 연동 시 GetWishlistResponseT 로 교체 — discountRate 는 아직 API 에 없음
export type WishListItemT = Omit<GetWishlistResponseT, 'item'> & {
item: ItemT & {
/** 할인율(%) — 할인이 없으면 null */
discountRate: number | null;
};
};

// TEMP: API 연동 시 응답 타입으로 교체
export type WishFolderT = {
id: number;
name: string;
wishIds: number[];
};
9 changes: 9 additions & 0 deletions apps/web/src/app/wish/_components/AllWishTabContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import WishGrid from '../_common/_components/WishGrid';
import { MOCK_WISHES } from '../_common/_consts/wishMock';

function AllWishTabContent() {
/** TODO: 위시 상세 링크 연결 시 saveScrollAnchor·useScrollRestoration(scope: all) 추가 */
return <WishGrid wishes={MOCK_WISHES} />;
}

export default AllWishTabContent;
20 changes: 20 additions & 0 deletions apps/web/src/app/wish/_components/FolderAddCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AddIconOutline } from '@/assets/icons';

// TODO: 폴더 추가 동작 연결
function FolderAddCard() {
return (
<button type="button" className="flex w-full cursor-pointer flex-col text-left">
<span className="flex aspect-[201/248] w-full items-center justify-center bg-gray-75">
<AddIconOutline className="size-8 text-icon-neutral-primary" />
</span>
<span className="flex flex-col gap-1 p-3">
<span className="body-2-medium text-text-neutral-primary">폴더 추가하기</span>
<span className="caption-1-regular text-text-neutral-secondary">
위시를 모아 관리하세요.
</span>
</span>
</button>
);
}

export default FolderAddCard;
Loading
Loading