Skip to content

Commit d435c61

Browse files
authored
Merge pull request #59 from Leets-Official/55-feat/판매페이지-및-판매확인페이지-UI-구현
[Feat] 판매 페이지 및 판매 확인 페이지 UI 구현
2 parents 6ed19de + fc4cfdf commit d435c61

34 files changed

Lines changed: 956 additions & 15 deletions

src/app/providers/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import { ToastProvider } from '@shared/contexts';
12
import type { ReactNode } from 'react';
23

34
type AppProvidersProps = {
45
children: ReactNode;
56
};
67

78
export function AppProviders({ children }: AppProvidersProps) {
8-
return <>{children}</>;
9+
return <ToastProvider>{children}</ToastProvider>;
910
}

src/app/routes.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { index, layout, route, type RouteConfig } from '@react-router/dev/routes
33
export default [
44
route('login', 'routes/login.tsx'),
55
route('signup', 'routes/signup.tsx'),
6-
layout('layout/MainLayout.tsx', [index('routes/(main)/_index.tsx')]),
6+
layout('layout/MainLayout.tsx', [
7+
index('routes/(main)/_index.tsx'),
8+
route('sell', 'routes/(main)/sell.tsx'),
9+
route('sell/confirm', 'routes/(main)/sell.confirm.tsx'),
10+
]),
711
route('playground', 'routes/playground.tsx'),
812
] satisfies RouteConfig;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { SellConfirmPage } from '@pages/sell-confirm';
2+
3+
export default SellConfirmPage;

src/app/routes/(main)/sell.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { SellPage } from '@pages/sell';
2+
3+
export default SellPage;

src/app/styles/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
--spacing-xs: 12px;
7373
--spacing-xxs: 10px;
7474
--spacing-xxxs: 8px;
75+
--spacing-xxxxs: 4px;
7576

7677
/* Margin (Primitive/Scale) */
7778
--margin-l: 24px;

src/pages/main/ui/MainPage.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,38 @@
1+
import { ChatCircleCloseIcon } from '@shared/assets/icons';
12
import { ROUTES } from '@shared/constants';
3+
import { useToast } from '@shared/hooks';
24
import { BannerCard } from '@shared/ui/BannerCard';
35
import { Carousel3D } from '@shared/ui/Carousel3D';
46
import { ChatbotFloatingButton } from '@shared/ui/ChatbotFloatingButton';
57
import { ClientOnly } from '@shared/ui/ClientOnly';
6-
import { useNavigate } from 'react-router';
8+
import { useEffect, useRef } from 'react';
9+
import { useLocation, useNavigate } from 'react-router';
710
import { BANNER_CARDS } from './BannerCards';
811
import { CAROUSEL_SLIDES } from './CarouselSlides';
912

13+
type MainLocationState = {
14+
deleted?: boolean;
15+
};
16+
1017
export default function MainPage() {
18+
const location = useLocation();
1119
const navigate = useNavigate();
20+
const { showToast } = useToast();
21+
const didShowDeleteToast = useRef(false);
22+
23+
useEffect(() => {
24+
const state = location.state as MainLocationState | null;
25+
if (state?.deleted && !didShowDeleteToast.current) {
26+
didShowDeleteToast.current = true;
27+
showToast('삭제되었습니다!', {
28+
tone: 'info',
29+
icon: (
30+
<ChatCircleCloseIcon className="h-6 w-6 text-[var(--color-gray-900)] [&_*]:stroke-[var(--color-gray-900)] [&_*]:stroke-[2]" />
31+
),
32+
});
33+
navigate(location.pathname, { replace: true, state: null });
34+
}
35+
}, [location.pathname, location.state, navigate, showToast]);
1236

1337
return (
1438
<main className="flex w-full flex-col items-center gap-14 px-4 pt-8 pb-24">

src/pages/sell-confirm/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as SellConfirmPage } from './ui/SellConfirmPage';
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import checkerImg from '@shared/assets/icons/common/checker.png';
2+
import { Button } from '@shared/ui/Button/Button';
3+
import { Modal } from '@shared/ui/Modal/Modal';
4+
import { useEffect, useState } from 'react';
5+
import { useLocation, useNavigate } from 'react-router';
6+
import type { SellState } from '@shared/types/sell';
7+
8+
type SellConfirmState = SellState;
9+
10+
export default function SellConfirmPage() {
11+
const location = useLocation();
12+
const navigate = useNavigate();
13+
const [imageError, setImageError] = useState(false);
14+
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
15+
const state = (location.state ?? {}) as SellConfirmState;
16+
17+
const displayTitle = state.title ?? '아이폰 14 맥스';
18+
const displayPrice = state.price
19+
? `${new Intl.NumberFormat('ko-KR').format(Number(state.price || 0))}원`
20+
: '580,000원';
21+
const displayManufacturer = state.manufacturer ?? '애플';
22+
const displayModelName = state.modelName ?? '아이폰 14 MAX';
23+
const displayColor = state.colorName ?? '실버';
24+
const displayDescription =
25+
state.description ??
26+
`쓴지는 1년정도 되었습니다.
27+
잔기스는 살짝 있습니다.
28+
택배거래 희망합니다.
29+
케이스랑 충전기 줄 드립니다!`;
30+
const displayImageUrl = state.imageUrl ?? null;
31+
const showImage = Boolean(displayImageUrl) && !imageError;
32+
const productCondition = state.productCondition ?? 'used';
33+
const scratchCondition = state.scratchCondition ?? 'scratch';
34+
const screenCondition = state.screenCondition ?? 'clean';
35+
const batteryCondition = state.batteryCondition ?? '80plus';
36+
const displayProductCondition = productCondition === 'used' ? '개봉-중고' : '미개봉-새상품';
37+
const displayScratchCondition = scratchCondition === 'clean' ? '스크래치 없음' : '스크래치 있음';
38+
const displayScreenCondition = screenCondition === 'clean' ? '화면 깨짐 없음' : '화면 깨짐';
39+
const displayBatteryCondition =
40+
batteryCondition === '80minus'
41+
? '배터리 성능 80% 미만'
42+
: batteryCondition === '50minus'
43+
? '배터리 성능 50% 미만'
44+
: '배터리 성능 80% 이상';
45+
46+
useEffect(() => {
47+
setImageError(false);
48+
}, [displayImageUrl]);
49+
50+
return (
51+
<div className="w-full bg-white">
52+
<div className="mx-auto flex min-h-screen w-full max-w-[1440px] flex-col items-center bg-white px-4">
53+
<main className="mt-[90px] flex items-start gap-[var(--spacing-l)]">
54+
{showImage ? (
55+
<img
56+
src={displayImageUrl ?? ''}
57+
alt="업로드된 이미지"
58+
className="h-[568px] w-[590px] rounded-[var(--radius-s)] object-cover"
59+
onError={() => setImageError(true)}
60+
/>
61+
) : (
62+
<div
63+
className="h-[568px] w-[590px] rounded-[var(--radius-s)] bg-[var(--color-gray-100)] bg-cover bg-center"
64+
style={{ backgroundImage: `url(${checkerImg})` }}
65+
aria-label="확인 이미지"
66+
/>
67+
)}
68+
<div className="flex h-[568px] w-[590px] flex-col items-start justify-between">
69+
<div className="flex w-full flex-col items-start gap-[21px]">
70+
<h1 className="typo-title-2 line-clamp-2 w-full text-[var(--color-gray-900)]">{displayTitle}</h1>
71+
<p className="typo-title-1 text-[var(--color-gray-900)]">{displayPrice}</p>
72+
73+
<div className="h-px w-full bg-[var(--color-gray-200)]" />
74+
75+
<div className="flex w-full flex-col items-start gap-[var(--spacing-xxs)]">
76+
<div className="flex items-center gap-[11px]">
77+
<span className="typo-caption-1 text-[var(--color-gray-500)]">제조사</span>
78+
<span className="typo-body-1 text-[var(--color-gray-900)]">{displayManufacturer}</span>
79+
</div>
80+
<div className="flex items-center gap-[11px]">
81+
<span className="typo-caption-1 text-[var(--color-gray-500)]">모델명</span>
82+
<span className="typo-body-1 text-[var(--color-gray-900)]">{displayModelName}</span>
83+
</div>
84+
<div className="flex items-center gap-[11px]">
85+
<span className="typo-caption-1 text-[var(--color-gray-500)]">색상</span>
86+
<span className="typo-body-1 text-[var(--color-gray-900)]">{displayColor}</span>
87+
</div>
88+
</div>
89+
90+
<div className="flex w-full items-center gap-[3px] text-center">
91+
<span className="typo-caption-1 whitespace-nowrap text-[var(--color-gray-300)]">
92+
{displayProductCondition}
93+
</span>
94+
<span className="typo-caption-1 text-[var(--color-gray-300)]"></span>
95+
<span className="typo-caption-1 whitespace-nowrap text-[var(--color-gray-300)]">
96+
{displayScratchCondition}
97+
</span>
98+
<span className="typo-caption-1 text-[var(--color-gray-300)]"></span>
99+
<span className="typo-caption-1 whitespace-nowrap text-[var(--color-gray-300)]">
100+
{displayScreenCondition}
101+
</span>
102+
<span className="typo-caption-1 text-[var(--color-gray-300)]"></span>
103+
<span className="typo-caption-1 whitespace-nowrap text-[var(--color-gray-300)]">
104+
{displayBatteryCondition}
105+
</span>
106+
</div>
107+
108+
<div className="h-px w-[584px] bg-[var(--color-gray-200)]" />
109+
110+
<p className="typo-body-2 line-clamp-6 w-full text-ellipsis whitespace-pre-line text-[var(--color-gray-900)]">
111+
{displayDescription}
112+
</p>
113+
</div>
114+
115+
<div className="flex h-[44px] w-full items-center gap-[34px]">
116+
<Button
117+
variant="outline"
118+
size="full"
119+
className="h-[44px] px-[var(--padding-xl)] py-[var(--padding-m)] text-[var(--color-gray-900)]"
120+
onClick={() => navigate('/sell', { state })}
121+
>
122+
수정
123+
</Button>
124+
<Button
125+
variant="fill"
126+
size="full"
127+
className="h-[44px] px-[var(--padding-xl)] py-[var(--padding-m)]"
128+
onClick={() => setIsDeleteModalOpen(true)}
129+
>
130+
삭제
131+
</Button>
132+
</div>
133+
</div>
134+
</main>
135+
{isDeleteModalOpen && (
136+
<Modal
137+
title="삭제하시겠어요?"
138+
subtitle="삭제하면 복구할 수 없어요."
139+
onCancel={() => setIsDeleteModalOpen(false)}
140+
onConfirm={() => {
141+
setIsDeleteModalOpen(false);
142+
navigate('/', { state: { deleted: true } });
143+
}}
144+
cancelText="취소"
145+
confirmText="삭제"
146+
/>
147+
)}
148+
</div>
149+
</div>
150+
);
151+
}

src/pages/sell/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as SellPage } from './ui/SellPage';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { SellState } from '@shared/types/sell';
2+
import type { SellFormData } from '@shared/utils/schemas';
3+
import type { DefaultValues } from 'react-hook-form';
4+
5+
export const getSellFormDefaults = (): DefaultValues<SellFormData> => ({
6+
title: '',
7+
price: '',
8+
manufacturer: '',
9+
modelName: '',
10+
colorName: '',
11+
storageSize: '',
12+
description: '',
13+
productCondition: 'new',
14+
scratchCondition: 'scratch',
15+
screenCondition: 'broken',
16+
batteryCondition: '80plus',
17+
});
18+
19+
export const mapSellDraftToForm = (state: SellState): DefaultValues<SellFormData> => ({
20+
title: state.title ?? '',
21+
price: state.price ?? '',
22+
manufacturer: state.manufacturer ?? '',
23+
modelName: state.modelName ?? '',
24+
colorName: state.colorName ?? '',
25+
storageSize: state.storageSize ?? '',
26+
description: state.description ?? '',
27+
productCondition: state.productCondition ?? 'new',
28+
scratchCondition: state.scratchCondition ?? 'scratch',
29+
screenCondition: state.screenCondition ?? 'broken',
30+
batteryCondition: state.batteryCondition ?? '80plus',
31+
});

0 commit comments

Comments
 (0)