Skip to content

Commit f9de8ab

Browse files
authored
Merge pull request #66 from Leets-Official/63-fix/판매페이지-기능-오류-수정
[Fix] 판매페이지 기능 오류 수정
2 parents 19d7c6e + 7f98b2b commit f9de8ab

31 files changed

Lines changed: 764 additions & 592 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ToastContext } from '@shared/contexts';
2+
import { useToastProvider } from '@shared/hooks';
3+
import { Portal } from '@shared/ui/Portal';
4+
import { Toast } from '@shared/ui/Toast';
5+
import { tv } from 'tailwind-variants';
6+
import type { ReactNode } from 'react';
7+
8+
const toastContainerVariants = tv({
9+
base: 'fixed top-[92px] left-1/2 z-50 -translate-x-1/2 transition-all duration-300 ease-out',
10+
variants: {
11+
isAnimating: {
12+
true: 'opacity-100 translate-y-0',
13+
false: 'opacity-0 -translate-y-2',
14+
},
15+
},
16+
defaultVariants: {
17+
isAnimating: false,
18+
},
19+
});
20+
21+
interface ToastProviderProps {
22+
children: ReactNode;
23+
}
24+
25+
export function ToastProvider({ children }: ToastProviderProps) {
26+
const { contextValue, isVisible, isAnimating, toastProps } = useToastProvider();
27+
28+
return (
29+
<ToastContext.Provider value={contextValue}>
30+
{children}
31+
{isVisible && (
32+
<Portal containerId="toast-root">
33+
<div className={toastContainerVariants({ isAnimating })}>
34+
<Toast {...toastProps} />
35+
</div>
36+
</Portal>
37+
)}
38+
</ToastContext.Provider>
39+
);
40+
}

src/app/providers/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ToastProvider } from '@shared/contexts';
1+
import { ToastProvider } from './ToastProvider';
22
import type { ReactNode } from 'react';
33

44
type AppProvidersProps = {

src/app/styles/index.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,15 @@
9292
--radius-l: 24px;
9393
--radius-m: 16px;
9494
--radius-s: 8px;
95+
96+
/* Layout */
97+
--spacing-header-content: 54px;
9598
}
9699

97100
@layer base {
98101
html {
99102
font-family: var(--font-sans);
103+
zoom: 0.9;
100104
}
101105

102106
/* View Transitions */

src/pages/main/ui/MainPage.tsx

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,17 @@
1-
import { ChatCircleCloseIcon } from '@shared/assets/icons';
21
import { ROUTES } from '@shared/constants';
3-
import { useToast } from '@shared/hooks';
42
import { BannerCard } from '@shared/ui/BannerCard';
53
import { Carousel3D } from '@shared/ui/Carousel3D';
64
import { ChatbotFloatingButton } from '@shared/ui/ChatbotFloatingButton';
75
import { ClientOnly } from '@shared/ui/ClientOnly';
8-
import { useEffect, useRef } from 'react';
9-
import { useLocation, useNavigate } from 'react-router';
6+
import { useNavigate } from 'react-router';
107
import { BANNER_CARDS } from './BannerCards';
118
import { CAROUSEL_SLIDES } from './CarouselSlides';
129

13-
type MainLocationState = {
14-
deleted?: boolean;
15-
};
16-
1710
export default function MainPage() {
18-
const location = useLocation();
1911
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]);
3612

3713
return (
38-
<main className="flex w-full flex-col items-center gap-14 px-4 pt-8 pb-24">
14+
<main className="flex w-full flex-col items-center gap-14 px-4 pb-24">
3915
<section className="h-79.25 w-full max-w-300" aria-label="메인 슬로건 영역">
4016
<ClientOnly>
4117
<Carousel3D slides={CAROUSEL_SLIDES} />

src/pages/sell-confirm/ui/SellConfirmPage.tsx

Lines changed: 70 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,126 @@
11
import checkerImg from '@shared/assets/icons/common/checker.png';
2+
import { useModal, useToast } from '@shared/hooks';
3+
import { MOCK_SELL_CONFIRM_DATA } from '@shared/mocks/data';
24
import { Button } from '@shared/ui/Button/Button';
35
import { Modal } from '@shared/ui/Modal/Modal';
46
import { useEffect, useState } from 'react';
57
import { useLocation, useNavigate } from 'react-router';
68
import type { SellState } from '@shared/types/sell';
79

10+
const CONDITION_LABELS = {
11+
productCondition: { used: '개봉-중고', unopened: '미개봉-새상품' },
12+
scratchCondition: { clean: '스크래치 없음', scratch: '스크래치 있음' },
13+
screenCondition: { clean: '화면 깨짐 없음', broken: '화면 깨짐' },
14+
batteryCondition: {
15+
'80plus': '배터리 성능 80% 이상',
16+
'80minus': '배터리 성능 80% 미만',
17+
'50minus': '배터리 성능 50% 미만',
18+
},
19+
} as const;
20+
821
type SellConfirmState = SellState;
922

1023
export default function SellConfirmPage() {
1124
const location = useLocation();
1225
const navigate = useNavigate();
26+
const { showToast } = useToast();
1327
const [imageError, setImageError] = useState(false);
14-
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
15-
const state = (location.state ?? {}) as SellConfirmState;
28+
const deleteModal = useModal();
29+
const state = (location.state ?? MOCK_SELL_CONFIRM_DATA) as SellConfirmState;
30+
31+
const displayPrice = `${new Intl.NumberFormat('ko-KR').format(Number(state.price))}원`;
32+
const showImage = Boolean(state.imageUrl) && !imageError;
33+
34+
const productInfo = [
35+
{ label: '제조사', value: state.manufacturer },
36+
{ label: '모델명', value: state.modelName },
37+
{ label: '색상', value: state.colorName },
38+
];
39+
40+
const conditionTags = [
41+
CONDITION_LABELS.productCondition[state.productCondition as keyof typeof CONDITION_LABELS.productCondition],
42+
CONDITION_LABELS.scratchCondition[state.scratchCondition as keyof typeof CONDITION_LABELS.scratchCondition],
43+
CONDITION_LABELS.screenCondition[state.screenCondition as keyof typeof CONDITION_LABELS.screenCondition],
44+
CONDITION_LABELS.batteryCondition[state.batteryCondition as keyof typeof CONDITION_LABELS.batteryCondition],
45+
];
1646

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% 이상';
47+
const actionButtons = [
48+
{ label: '수정', variant: 'outline' as const, onClick: () => navigate('/sell', { state }) },
49+
{ label: '삭제', variant: 'fill' as const, onClick: deleteModal.open },
50+
];
4551

4652
useEffect(() => {
4753
setImageError(false);
48-
}, [displayImageUrl]);
54+
}, [state.imageUrl]);
4955

5056
return (
5157
<div className="w-full bg-white">
5258
<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)]">
59+
<main className="gap-l mt-[90px] flex items-start">
5460
{showImage ? (
5561
<img
56-
src={displayImageUrl ?? ''}
62+
src={state.imageUrl ?? ''}
5763
alt="업로드된 이미지"
58-
className="h-[568px] w-[590px] rounded-[var(--radius-s)] object-cover"
64+
className="h-[568px] w-[590px] rounded-s object-cover"
5965
onError={() => setImageError(true)}
6066
/>
6167
) : (
6268
<div
63-
className="h-[568px] w-[590px] rounded-[var(--radius-s)] bg-[var(--color-gray-100)] bg-cover bg-center"
69+
className="h-[568px] w-[590px] rounded-s bg-gray-100 bg-cover bg-center"
6470
style={{ backgroundImage: `url(${checkerImg})` }}
6571
aria-label="확인 이미지"
6672
/>
6773
)}
6874
<div className="flex h-[568px] w-[590px] flex-col items-start justify-between">
6975
<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>
76+
<h1 className="typo-title-2 line-clamp-2 w-full text-gray-900">{state.title}</h1>
77+
<p className="typo-title-1 text-gray-900">{displayPrice}</p>
7278

73-
<div className="h-px w-full bg-[var(--color-gray-200)]" />
79+
<div className="h-px w-full bg-gray-200" />
7480

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>
81+
<div className="gap-xxs flex w-full flex-col items-start">
82+
{productInfo.map(({ label, value }) => (
83+
<div key={label} className="flex items-center gap-[11px]">
84+
<span className="typo-caption-1 text-gray-500">{label}</span>
85+
<span className="typo-body-1 text-gray-900">{value}</span>
86+
</div>
87+
))}
8888
</div>
8989

9090
<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>
91+
{conditionTags.map((tag, index) => (
92+
<span key={tag} className="typo-caption-1 flex items-center gap-[3px] text-gray-300">
93+
{index > 0 && '•'}
94+
<span className="whitespace-nowrap">{tag}</span>
95+
</span>
96+
))}
10697
</div>
10798

108-
<div className="h-px w-[584px] bg-[var(--color-gray-200)]" />
99+
<div className="h-px w-[584px] bg-gray-200" />
109100

110-
<p className="typo-body-2 line-clamp-6 w-full text-ellipsis whitespace-pre-line text-[var(--color-gray-900)]">
111-
{displayDescription}
101+
<p className="typo-body-2 line-clamp-6 w-full text-ellipsis whitespace-pre-line text-gray-900">
102+
{state.description}
112103
</p>
113104
</div>
114105

115106
<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>
107+
{actionButtons.map(({ label, variant, onClick }) => (
108+
<Button key={label} variant={variant} size="full" className="px-xl py-m h-[44px]" onClick={onClick}>
109+
{label}
110+
</Button>
111+
))}
132112
</div>
133113
</div>
134114
</main>
135-
{isDeleteModalOpen && (
115+
{deleteModal.isOpen && (
136116
<Modal
137117
title="삭제하시겠어요?"
138118
subtitle="삭제하면 복구할 수 없어요."
139-
onCancel={() => setIsDeleteModalOpen(false)}
119+
onCancel={deleteModal.close}
140120
onConfirm={() => {
141-
setIsDeleteModalOpen(false);
142-
navigate('/', { state: { deleted: true } });
121+
deleteModal.close();
122+
showToast('삭제되었습니다', 'success');
123+
navigate('/');
143124
}}
144125
cancelText="취소"
145126
confirmText="삭제"

src/pages/sell/model/options.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1+
import type { SellFormData } from '@shared/utils/schemas';
2+
13
export const MANUFACTURER_OPTIONS = ['삼성', '애플'] as const;
24

5+
export const TEXT_FIELDS = [
6+
{ name: 'title', label: '제목', placeholder: '제목을 입력해 주세요' },
7+
{ name: 'modelName', label: '모델명', placeholder: '모델명을 입력해 주세요' },
8+
{ name: 'colorName', label: '색상', placeholder: '색상을 입력해 주세요' },
9+
{ name: 'storageSize', label: '저장 용량', placeholder: '128GB' },
10+
] as const satisfies ReadonlyArray<{ name: keyof SellFormData; label: string; placeholder: string }>;
11+
12+
export const PRODUCT_CONDITION_OPTIONS = [
13+
{ label: '미개봉-새상품', value: 'new' },
14+
{ label: '개봉-중고', value: 'used' },
15+
] as const;
16+
317
export const SCRATCH_OPTIONS = [
418
{ label: '스크래치 있음', value: 'scratch' },
519
{ label: '스크래치 없음', value: 'clean' },

0 commit comments

Comments
 (0)