Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 사전 경매 수정 및 에러 바운더리 적용 등 #134

Merged
merged 23 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion src/@types/AuctionDetails.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ declare module 'AuctionDetails' {
}

export interface IPreAuctionDetails extends IAuctionDetailsBase {
createdAt: string;
updatedAt: string;
likeCount: number;
isLiked: boolean;
images: {
Expand Down
2 changes: 1 addition & 1 deletion src/@types/Register.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ declare module 'Register' {
minPrice: number;
auctionRegisterType?: string;
category: string;
imageSequence?: Map<number, number>;
imageSequence?: { [k: string]: number };
}
}
9 changes: 9 additions & 0 deletions src/assets/icons/error.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/assets/icons/partial_error.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 101 additions & 0 deletions src/components/bid/BidMain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { getBidSchema } from "@/constants/schema";
import { useEditableNumberInput } from "@/hooks/useEditableNumberInput";
import { convertCurrencyToNumber } from "@/utils/convertCurrencyToNumber";
import { formatCurrencyWithWon } from "@/utils/formatCurrencyWithWon";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { z } from "zod";
import FormField from "../common/form/FormField";
import AuctionItem from "../common/item/AuctionItem";
import { useGetAuctionDetails } from "../details/queries";
import Layout from "../layout/Layout";
import { Input } from "../ui/input";
import BidCaution from "./BidCaution";
import BidFooter from "./BidFooter";
import { usePatchBid, usePostBid } from "./queries";

const BidMain = ({ auctionId }: { auctionId: number }) => {
const { auctionDetails } = useGetAuctionDetails(auctionId);
const { mutate: postBid } = usePostBid(auctionId);
const { mutate: patchBid } = usePatchBid(auctionId);
const [check, setCheck] = useState<boolean>(false);
const toggleCheckBox = () => setCheck((state) => !state);

const { imageUrls, productName, minPrice, participantCount, remainingBidCount, bidAmount, timeRemaining, isParticipated, bidId } = auctionDetails;
const BidSchema = getBidSchema(minPrice);
type FormFields = z.infer<typeof BidSchema>;

const {
control,
formState: { errors, isSubmitting },
setValue,
getValues,
handleSubmit,
} = useForm<FormFields>({
defaultValues: { bidAmount: '' },
resolver: zodResolver(BidSchema),
});

const { isEditing, handleBlur, handleFocus } = useEditableNumberInput({
name: 'bidAmount',
setValue,
getValues,
});

const onPostSubmit: SubmitHandler<FormFields> = async (data) => {
const bidData = {
auctionId: Number(auctionId),
bidAmount: convertCurrencyToNumber(data.bidAmount),
};

postBid(bidData);
};
const onPatchSubmit = () => {
if (bidId) patchBid(bidId);
};

return (
<>
<Layout.Main>
<div className='flex flex-col gap-8'>
<AuctionItem axis='row' label='입찰 상품'>
<AuctionItem.Image src={imageUrls[0]} time={timeRemaining} />
<AuctionItem.Main kind='register' name={productName} count={participantCount} price={minPrice} />
</AuctionItem>
{isParticipated && (
<div className='flex flex-col gap-2'>
<div className='text-heading3'>나의 참여 금액</div>
<div aria-label='나의 참여 금액' className='text-body1Bold text-cheeseYellow'>
{formatCurrencyWithWon(bidAmount)}
</div>
</div>
)}
<FormField
label='가격 제안하기'
name='bidAmount'
control={control}
error={errors.bidAmount?.message}
render={(field) => (
<Input
id='가격 제안하기'
type={isEditing ? 'number' : 'text'}
placeholder={`최소 입찰가는 ${formatCurrencyWithWon(minPrice)} 입니다.`}
className=' focus-visible:ring-cheeseYellow'
{...field}
onBlur={handleBlur}
onFocus={handleFocus}
/>
)}
/>
<BidCaution check={check} handleCheck={toggleCheckBox} />
</div>
</Layout.Main>
<Layout.Footer type={isParticipated ? 'double' : 'single'}>
<BidFooter remain={remainingBidCount} check={check} isSubmitting={isSubmitting} handlePatch={onPatchSubmit} handlePost={handleSubmit(onPostSubmit)} />
</Layout.Footer>
</>
);
}

export default BidMain;
61 changes: 61 additions & 0 deletions src/components/bidderList/BidderListMain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { BIDDER_LIST_PRICE_FILTER } from "@/constants/filter";
import { formatCurrencyWithWon } from "@/utils/formatCurrencyWithWon";
import type { IBidder } from "Bid";
import { useState } from "react";
import Button from "../common/Button";
import AuctionItem from "../common/item/AuctionItem";
import { useGetAuctionDetails } from "../details/queries";
import Layout from "../layout/Layout";
import { useGetBidderList } from "./queries";

const BidderListMain = ({ auctionId }: { auctionId: number }) => {
const [filterState, setFilterState] = useState(BIDDER_LIST_PRICE_FILTER.HIGH);

const handleFilterState = () =>
setFilterState((prev) => (prev.name === BIDDER_LIST_PRICE_FILTER.HIGH.name ? BIDDER_LIST_PRICE_FILTER.LOW : BIDDER_LIST_PRICE_FILTER.HIGH));


const { auctionDetails } = useGetAuctionDetails(auctionId);
const { bidderList } = useGetBidderList(auctionId);

const filteredBidderList = filterState.sort === 'desc' ? bidderList : bidderList.sort((a, b) => a.bidAmount - b.bidAmount)
const { imageUrls, productName, minPrice, participantCount } = auctionDetails;

return (
<>
<Layout.Main>
<div className='flex flex-col gap-8 pt-4'>
<AuctionItem axis='row' label='입찰자 목록 상품'>
<AuctionItem.Image src={imageUrls[0]} />
<AuctionItem.Main kind='register' name={productName} count={participantCount} price={minPrice} />
</AuctionItem>
<div className='flex items-center justify-between'>
<h2 className='text-heading2'>참여 가격</h2>
<div onClick={handleFilterState} className='flex items-center gap-1 cursor-pointer text-body2 text-gray1'>
<span>{filterState.name}</span>
<img className='pb-1' src={filterState.icon} alt={filterState.name} />
</div>
</div>
<hr className='border my-[-16px] border-gray3' />
<ul className='flex flex-col gap-2'>
{filteredBidderList.map((el: IBidder, idx: number) => (
<li
key={el.bidderNickname}
className={`flex p-3 items-center justify-between text-gray1 ${idx === 0 && 'border border-cheeseYellow rounded-lg'}`}
>
<span className='text-body1'>{el.bidderNickname}</span>
<span className='text-body1Bold'>{formatCurrencyWithWon(el.bidAmount)}</span>
</li>
))}
</ul>
</div>
</Layout.Main>
<Layout.Footer type='single'>
<Button type='button' color='cheeseYellow' className='w-full h-full' aria-label='최종 판매 버튼'>
확인 완료
</Button>
</Layout.Footer></>
);
}

export default BidderListMain;
4 changes: 2 additions & 2 deletions src/components/common/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import classNames from 'classnames';
import ButtonSpinner from './loadingAndError/ButtonSpinner';
import { ReactNode } from 'react';
import ButtonSpinner from './loading/ButtonSpinner';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/CustomCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const CustomCarousel = ({ contentStyle, length, children }: { contentStyle?: str
<CarouselContent className={`flex items-center h-full ${contentStyle}`}>{children}</CarouselContent>
{length > 1 && (
<>
<CarouselPrevious className='z-50 ml-14' />
<CarouselNext className='z-50 mr-14' />
<CarouselPrevious type='button' className='ml-14' />
<CarouselNext type='button' className='mr-14' />
</>
)}
</Carousel>
Expand Down
16 changes: 0 additions & 16 deletions src/components/common/EmptyBoundary.tsx

This file was deleted.

7 changes: 0 additions & 7 deletions src/components/common/EmptyFallback.tsx

This file was deleted.

6 changes: 3 additions & 3 deletions src/components/common/atomic/LikeCount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ const LikeCount = ({ count }: { count: number }) => {
return (
<div
aria-label="좋아요"
className="flex items-center text-body2 text-gray2"
className="flex items-center text-xs sm:text-body2 text-gray2"
>
<img src={PriceIcon} alt="좋아요" />
<span>
<span className='whitespace-nowrap'>
{`좋아요 `}
<span className="text-black text-body2Bold">{count}명</span>
<span className="text-xs text-black sm:text-body2Bold">{count}명</span>
</span>
</div>
);
Expand Down
6 changes: 3 additions & 3 deletions src/components/common/atomic/MinPrice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ const MinPrice = ({ price }: { price: number }) => {
return (
<div
aria-label="시작가"
className="flex items-center text-body2 text-gray2"
className="flex items-center text-xs sm:text-body2 text-gray2"
>
<img src={PriceIcon} alt="시작가" />
<span>
시작가 <span className="text-black text-body2Bold">{formatted}</span>
<span className='overflow-hidden whitespace-nowrap'>
시작가 <span className="text-xs text-black sm:text-body2Bold">{formatted}</span>
</span>
</div>
);
Expand Down
6 changes: 3 additions & 3 deletions src/components/common/atomic/ParticipantCount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ const ParticipantCount = ({ count }: { count: number }) => {
return (
<div
aria-label="참여자"
className="flex items-center text-body2 text-gray2"
className="flex items-center text-xs sm:text-body2 text-gray2"
>
<img src={UserIcon} alt="참여자" />
<span>
참여자 <span className="text-black text-body2Bold">{count}명</span>
<span className='whitespace-nowrap'>
참여자 <span className="text-xs text-black sm:text-body2Bold">{count}명</span>
</span>
</div>
);
Expand Down
43 changes: 43 additions & 0 deletions src/components/common/boundary/APIAsyncBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ReactNode, Suspense } from 'react';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';

import ErrorIcon from '@/assets/icons/error.svg';
import { useQueryErrorResetBoundary } from '@tanstack/react-query';
import { isAxiosError } from 'axios';
import { useLocation } from 'react-router-dom';
import Button from '../Button';
import { getErrorByCode } from '../error/ErrorCode';
import GlobalSpinner from '../loading/GlobalSpinner';

const FallbackComponent = ({ error, resetErrorBoundary }: FallbackProps) => {
const { title, description } = getErrorByCode(error)
if (!isAxiosError(error)) throw error

return (
<div className='flex flex-col items-center min-w-[10rem] justify-center h-full gap-5'>
<img src={ErrorIcon} alt='에러 아이콘' />
<div className='space-y-2 text-center'>
<h2 className='sm:text-heading2 text-heading3 text-gray1'>{title}</h2>
<p className='text-gray2 text-body2'>{description}</p>
</div>
<Button type='button' color='cheeseYellow' className='' onClick={resetErrorBoundary}>
다시 불러오기
</Button>
</div>
);
};

const APIAsyncBoundary = ({ children }: { children: ReactNode }) => {
const { pathname, key } = useLocation();
const { reset } = useQueryErrorResetBoundary()

return (
<ErrorBoundary onReset={reset} FallbackComponent={FallbackComponent} resetKeys={[pathname, key]}>
<Suspense fallback={<GlobalSpinner />}>
{children}
</Suspense>
</ErrorBoundary>
);
};

export default APIAsyncBoundary;
20 changes: 20 additions & 0 deletions src/components/common/boundary/EmptyBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { EMPTY_MESSAGE } from '@/constants/emptyMessage';
import { ReactNode } from 'react';

const EmptyFallback = ({ name }: { name: string }) => {
return <div className='flex items-center justify-center w-full h-full md:text-heading2 text-heading3 text-gray2'>{EMPTY_MESSAGE[name]}</div>;
};

interface EmptyBoundaryProps {
length: number;
name: string;
children: ReactNode;
}

const EmptyBoundary = ({ length, name, children }: EmptyBoundaryProps) => {
if (length === 0) return <EmptyFallback name={name} />;

return children;
};

export default EmptyBoundary;
Loading