diff --git a/src/components/atoms/Toast/styled.ts b/src/components/atoms/Toast/styled.ts index 587c328a..c3538c3c 100644 --- a/src/components/atoms/Toast/styled.ts +++ b/src/components/atoms/Toast/styled.ts @@ -6,7 +6,7 @@ export const ToastManagerWrapper: ReturnType = styled.div` bottom: 80px; left: 50%; transform: translateX(-50%); - z-index: 1500; + z-index: 999999; `; export const ToastItemWrapper: ReturnType = styled.div` diff --git a/src/components/templates/DetailTemplate/index.tsx b/src/components/templates/DetailTemplate/index.tsx index 9ed92ac3..ebdfdd1e 100644 --- a/src/components/templates/DetailTemplate/index.tsx +++ b/src/components/templates/DetailTemplate/index.tsx @@ -13,8 +13,10 @@ import { DetailTemplateWrapper, ImageSliderAndTimer } from "./styled"; import { useBid, useDetailModal } from "hooks"; import { buttonNames, priceNames } from "constants/auctionControlBarNames"; import type { IComment, IProductDetail } from "types"; +import { LOGO_PATH } from "constants/imgPath"; -interface IBaseDetailTemplateProps extends Omit { +interface IBaseDetailTemplateProps + extends Omit { /** 판매자 정보 */ seller: { id: number; @@ -35,7 +37,7 @@ interface IBaseDetailTemplateProps extends Omit { uploadTime: string; /** 거래 희망 장소 */ productLocation: { - longtitude: number; + longitude: number; latitube: number; address: string; location: string; @@ -47,9 +49,9 @@ interface IBaseDetailTemplateProps extends Omit { /** 최소 입찰가 */ minimumPrice: number; /** 내 입찰가 */ - myPrice?: number; + myPrice: IProductDetail["myPrice"]; /** 최고 입찰가 */ - maximumPrice?: number; + maximumPrice: IProductDetail["winningPrice"]; /** 입찰 취소 */ onCancel: () => void; /** 조기마감 */ @@ -72,14 +74,15 @@ export const DetailTemplate = ({ // 판매자 정보 seller: { name, image }, // 거래 희망 장소 - productLocation: { longtitude, latitube, address, location }, + productLocation: { longitude, latitube, address, location }, onLocationClick, // 댓글 comments, // 가격 minimumPrice, - myPrice, maximumPrice, + myPrice, + myAuctionId, isEarly, // hasBuyer, onCancel, @@ -109,9 +112,10 @@ export const DetailTemplate = ({ createdAt={uploadTime} description={content} /> - + {/* TODO 프로필 사진 없는 경우 Logo 사진 */} + @@ -173,8 +177,8 @@ export const DetailTemplate = ({ price={price} setPrice={setPrice} minPrice={minimumPrice} - beforePrice={myPrice} - onBid={handleBid} + beforePrice={myPrice || undefined} + onBid={() => handleBid(minimumPrice, myAuctionId || undefined)} open={open} onClose={handleCloseBottomSheet} />, diff --git a/src/components/templates/DetailTemplate/stories.tsx b/src/components/templates/DetailTemplate/stories.tsx index f8d238e6..18128262 100644 --- a/src/components/templates/DetailTemplate/stories.tsx +++ b/src/components/templates/DetailTemplate/stories.tsx @@ -42,7 +42,7 @@ export const Default: Story = { }, // 거래 희망 장소 productLocation: { - longtitude: 126.9784147, + longitude: 126.9784147, latitube: 37.5666805, address: "관악구 신림동", location: "보라매공원 CU", diff --git a/src/components/templates/DetailTemplate/styled.ts b/src/components/templates/DetailTemplate/styled.ts index 24625738..fccb72d4 100644 --- a/src/components/templates/DetailTemplate/styled.ts +++ b/src/components/templates/DetailTemplate/styled.ts @@ -47,4 +47,7 @@ export const DetailTemplateWrapper: ReturnType = styled.div` width: 80px; } } + ${CommentWrapper} { + padding-bottom: 2rem; + } `; diff --git a/src/hooks/useBid.ts b/src/hooks/useBid.ts index 7ed6eb54..0bc713c3 100644 --- a/src/hooks/useBid.ts +++ b/src/hooks/useBid.ts @@ -1,40 +1,69 @@ -import { useEffect, useMemo, useState } from "react"; +import { useState } from "react"; import { bidding, cancelBidding, editBidding } from "services/apis"; -import { useFetchBidding } from "hooks"; +import { useFetchProduct } from "hooks"; +import { useModalStore } from "stores"; +import { Toast } from "components/atoms"; /** * 입찰 로직 */ export const useBid = (productId: number) => { - const { biddingList, isLoading } = useFetchBidding(); - const myPrice = useMemo(() => { - return ( - (!isLoading && biddingList.find((b) => b.productId === productId)) || null - ); - }, [isLoading]); + const { + actions: { closeModal }, + } = useModalStore(); + const { productRefetch } = useFetchProduct(productId.toString()); const [open, setOpen] = useState(false); const [price, setPrice] = useState(""); + const handleOpenBottomSheet = () => { + setOpen(true); + }; + + const handleCloseBottomSheet = () => { + setOpen(false); + }; + /** * 입찰 버튼 클릭 */ - const handleBid = () => { - if (!myPrice) { + const handleBid = (minimumPrice: number, myAuctionId?: number) => { + // 1. 가격 체크 + const priceValue = Number(price.replace(/,/g, "")); + if (priceValue < minimumPrice) { + Toast.show( + `${minimumPrice.toLocaleString()}원 이상으로 입력해주세요.`, + 2000, + ); + return; + } + // 2. 백엔드 요청 + if (!myAuctionId) { // 현재 입찰중이 아닌 경우 - bidding({ productId, price: Number(price.replace(/,/g, "")) }) + // TODO 동네 인증 여부 + // TODO 입찰 전 경고 모달 + bidding({ productId, price: priceValue }) .then((data) => { + // 입찰 완료 console.log(data); + // TODO Refetch 수정 + productRefetch().catch(console.error); + handleCloseBottomSheet(); + Toast.show("입찰을 성공했습니다.", 2000); }) .catch(console.error); } else { // 현재 입찰중인 경우 editBidding({ productId, - price: Number(price.replace(/,/g, "")), - auctionId: myPrice.auctionId, + price: priceValue, + auctionId: myAuctionId, }) .then((data) => { console.log(data); + // TODO Refetch 수정 + productRefetch().catch(console.error); + handleCloseBottomSheet(); + Toast.show("입찰 가격이 수정되었습니다.", 2000); }) .catch(console.error); } @@ -43,32 +72,23 @@ export const useBid = (productId: number) => { /** * 입찰 취소 */ - const handleCancel = () => { - if (myPrice) { - cancelBidding(myPrice.auctionId) + const handleCancel = (myAuctionId: number) => { + if (myAuctionId) { + cancelBidding(myAuctionId) .then((data) => { console.log(data); + // TODO Refetch 수정 + productRefetch().catch(console.error); + closeModal(); + Toast.show("입찰이 취소되었습니다.", 2000); }) .catch(console.error); } }; - const handleOpenBottomSheet = () => { - setOpen(true); - }; - - const handleCloseBottomSheet = () => { - setOpen(false); - }; - - useEffect(() => { - console.log(myPrice); - }, []); - return { open, price, - myPrice, setPrice, handleOpenBottomSheet, handleCloseBottomSheet, diff --git a/src/hooks/useFetchProduct.ts b/src/hooks/useFetchProduct.ts index d8772a89..0fad35b8 100644 --- a/src/hooks/useFetchProduct.ts +++ b/src/hooks/useFetchProduct.ts @@ -3,11 +3,15 @@ import { queries } from "constants/queryKeys"; import { getProduct } from "services/apis"; export const useFetchProduct = (productId: string) => { - const { data, isLoading } = useQuery({ + const { data, isLoading, refetch } = useQuery({ queryKey: queries.product.detail(productId), queryFn: () => getProduct(productId), select: (data) => data.result, }); - return { product: data, isProductLoading: isLoading }; + return { + product: data, + isProductLoading: isLoading, + productRefetch: refetch, + }; }; diff --git a/src/pages/DetailPage/index.tsx b/src/pages/DetailPage/index.tsx index a534b32a..f9595ad0 100644 --- a/src/pages/DetailPage/index.tsx +++ b/src/pages/DetailPage/index.tsx @@ -4,7 +4,11 @@ import { DetailTemplate } from "components/templates"; import { KebabMenu } from "components/molecules"; import { KebabIcon } from "components/atoms/Icon"; import { Loading } from "components/molecules/Loading"; -import { useSelectedLocationStore, useTopBarStore } from "stores"; +import { + useFormDataStore, + useSelectedLocationStore, + useTopBarStore, +} from "stores"; import { useFetchProduct, useFetchComment, @@ -14,6 +18,7 @@ import { } from "hooks"; import { KebabWrapper } from "./styled"; import { earlyClose } from "services/apis"; +import type { Category } from "../../types"; export const DetailPage = () => { const navigate = useNavigate(); @@ -24,8 +29,12 @@ export const DetailPage = () => { const { actions: { setCoord, setLocation, setAddress }, } = useSelectedLocationStore(); + // TODO + const { + actions: { setFormData }, + } = useFormDataStore(); const { open, handleOpen, handleClose, menuRef } = useKebabMenu(); - const { handleCancel, myPrice } = useBid(parseInt(productId!)); + const { handleCancel } = useBid(parseInt(productId!)); const { todo } = useDetailModal(); /** @@ -35,7 +44,7 @@ export const DetailPage = () => { if (product) { setCoord({ lat: product.productLocation.latitube, - lng: product.productLocation.longtitude, + lng: product.productLocation.longitude, }); setLocation(product.productLocation.location); setAddress(product.productLocation.address); @@ -65,7 +74,9 @@ export const DetailPage = () => { * (구매자) 입찰 취소 */ const handleCancelBid = () => { - handleCancel(); + if (product?.myAuctionId) { + handleCancel(product.myAuctionId); + } }; /** @@ -84,7 +95,20 @@ export const DetailPage = () => { const handleEdit = () => { if (product && !product.hasBuyer) { // 수정 페이지로 이동 - navigate(`/product?${productId!}`); + // TODO 확인 필요 + setFormData({ + title: product.title, + content: product.content, + minimumPrice: product.minimumPrice.toLocaleString(), + category: product.category as Category, + latitude: product.productLocation.latitube, + longitude: product.productLocation.longitude, + address: product.productLocation.address, + location: product.productLocation.location, + imgUrls: product.images.map((img) => ({ url: img, file: null })), + expiredTime: product.expiredTime, + }); + navigate(`/product?productId=${productId!}`); return; } }; @@ -138,7 +162,7 @@ export const DetailPage = () => { onLocationClick={handleLocationMapClick} comments={comments} minimumPrice={product.minimumPrice} - myPrice={myPrice?.bidPrice} + myPrice={product.myPrice} maximumPrice={product.winningPrice} isEarly={product.isEarly} productId={product.productId} @@ -146,6 +170,7 @@ export const DetailPage = () => { onCancel={handleCancelBid} onEarlyClosing={handleEarlyClosing} isSeller={product.isSeller} + myAuctionId={product.myAuctionId} /> {open && ( diff --git a/src/types/response/product.d.ts b/src/types/response/product.d.ts index a51399b1..6f3eadab 100644 --- a/src/types/response/product.d.ts +++ b/src/types/response/product.d.ts @@ -10,7 +10,7 @@ export interface IProductDetail { image: string; }; productLocation: { - longtitude: number; + longitude: number; latitube: number; address: string; location: string; @@ -25,8 +25,9 @@ export interface IProductDetail { expiredTime: string; isEarly: boolean; images: string[]; - myPrice?: number; - winningPrice?: number; + myAuctionId: number | null; + myPrice: number | null; + winningPrice: number | null; isSeller: boolean; }