Cart.tsx
import { useCallback, useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { useRecoilState } from 'recoil'
import { selectProduct, totalPriceState } from '../../store/selectProduct'
import { useRefreshToken } from '../auth/hooks/useRefreshToken'
import { useUser } from '../auth/hooks/useUser'
import CardTemplate from '../common/ui/CardTemplate'
import CheckBox from '../common/ui/CheckBox'
import PageLayout from '../common/ui/PageLayout'
import { getStoredToken } from '../local-storage/userStorage'
import CartItem from './CartItem'
import useCart, { CartItemsType } from './hooks/useCart'
const Cart = () => {
const refreshToken = useRefreshToken()
const [isTotalChecked, setIsTotalChecked] = useState(false)
const { user } = useUser()
const {cartItems} = useCart()
const [selectedProduct,setSelectedProduct] = useRecoilState(selectProduct)
const [totalPrice,setTotalPrice] = useRecoilState(totalPriceState);
const [shippingFee,setShippingFee] = useState(0)
const [products,setProducts] = useState<CartItemsType[]>([])
// 체크박스를 클릭한다.
// isTotalChecked 또는 isChecked가 true이면 해당 아이템이 담긴다.
// isTotalChecked 또는 isChecked가 false이면 해당 아이템이 빠진다.
const selectProductHandler = (cart:CartItemsType,checked:boolean) => {
if(checked){
setSelectedProduct((prev) => [...prev,cart])
setSelectedProduct(prev => Array.from(new Set(prev)));
}else{
setSelectedProduct((prev) => prev.filter((item) => item.cartId !== cart.cartId ))
}
}
const totalCheckedHandler = useCallback(() => {
setSelectedProduct([]);
setIsTotalChecked((prev) => !prev)
}, [])
const buyAllHandler = () => {
setIsTotalChecked(true);
setSelectedProduct(() => [...products])
}
useEffect(() => {
const token = getStoredToken()
refreshToken(token)
}, [])
useEffect(() => {
setProducts(cartItems);
}, [cartItems])
useEffect(() => {
if(!isTotalChecked){
setSelectedProduct([]);
}
products.map((item) => selectProductHandler(item,isTotalChecked))
}, [isTotalChecked])
useEffect(() => {
setTotalPrice(0);
let totalP = 0;
let totalDiscount = 0;
selectedProduct.map(item => totalP += (item.price * item.pcs))
selectedProduct.map(item => totalDiscount += item.discount)
setTotalPrice(totalP - (totalP * ((totalDiscount/selectedProduct.length || 0) / 100)))
setShippingFee(totalPrice >= 3000 ? 0 : 500)
}, [selectedProduct,totalPrice,products])
return (
<PageLayout layoutWidth="w-[90%]" innerTop="top-[30%]">
<CardTemplate title="장바구니" isTitleVisible={true}>
<div className="flex items-center justify-between py-1 border-b border-solid border-lenssisGray w-full">
<p className="pl-2 pb-1 text-base xs:text-xl text-lenssisDark font-bold">전체</p>
</div>
<div className="flex flex-col items-center xs:flex-row xs:items-start text-lenssisGray mt-4 xs:mt-10">
<div className="grow flex flex-col px-0 xs:px-2 w-full">
<div className="flex flex-col xs:flex-row items-start xs:items-center justify-between gap-2 xs:gap-0 w-full py-4 border-y border-solid border-lenssisStroke text-xs xs:text-base ">
<div className="flex items-center pl-4">
{isTotalChecked && (
<CheckBox
onClick={totalCheckedHandler}
bgColor="bg-lenssisDark"
isChecked={isTotalChecked}
/>
)}
{!isTotalChecked && <CheckBox onClick={totalCheckedHandler} bgColor="bg-lenssisStroke" />}
<label className="text-lenssisStroke text-base">전체선택(2/2)</label>
</div>
<p className="w-full xs:w-fit text-center xs:text-right">
<span className="font-semibold">TIP! 1200円</span> 더 구매하면,{' '}
<span className="font-semibold">500円 추가 할인</span> 받을 수 있어요.
</p>
</div>
<ul className="pl-4">
{products.map((item) => (
<CartItem setProducts={setProducts} key={item.productDetailsId} products={products} item={item} isTotalChecked={isTotalChecked} setIsTotalChecked={setIsTotalChecked} selectedProduct={selectedProduct} selectProductHandler={selectProductHandler} setSelectedProduct={setSelectedProduct} />
))}
</ul>
</div>
<div className="w-full xs:w-2/5 xs:max-w-[440px] text-base">
<div className="flex flex-col">
<div className="border border-solid border-gray-100 bg-[#f4f6f8] font-bold text-lenssisGray flex flex-col pt-2 p-6 rounded-[3px] px-8 gap-2">
<h3 className="text-xl py-4 text-[#5a5a5a]">지불 금액</h3>
<div className="flex items-center justify-between">
<p>총 상품 금액</p> <p>{totalPrice.toLocaleString()}円</p>
</div>
<div className="flex items-center justify-between">
<p>총 배송비</p> <p>{shippingFee}円</p>
</div>
<div className="flex items-center justify-between text-black">
<p>결제 예상 금액</p> <p>{(shippingFee + totalPrice).toLocaleString()}円</p>
</div>
</div>
<div className="flex gap-4 flex-col xs:flex-row items-center w-full justify-between mt-4">
<Link to="/payment" className="flex items-center justify-center border border-solid border-lenssisDark py-2 w-full xs:w-[220px] rounded-[5px] text-lenssisDark text-sm h-[50px] font-semibold">
선택상품구매
</Link>
{/* onClick시 모든 상품을 주문페이지에 request하는 로직 작성해야 함 */}
<Link
to="/payment"
className="flex items-center justify-center text-center border border-solid border-transparent bg-lenssisDark py-2 w-full xs:w-[220px] rounded-[5px] text-white text-sm h-[50px] font-semibold"
onClick={buyAllHandler}
>
전체상품구매
</Link>
</div>
</div>
<div className="flex flex-col items-center mt-[52px] text-lenssisGray font-semibold gap-4">
<p className="">3,000円 이상 구매 시 무료 배송</p>
<Link to="/">
<span className="underline">쇼핑 계속</span>
</Link>
</div>
</div>
</div>
</CardTemplate>
</PageLayout>
)
}
export default Cart
CartItem.tsx
import React, { useEffect, useState } from 'react'
import Counter from './Counter'
import CheckBox from '../common/ui/CheckBox'
import { CartItemsType } from './hooks/useCart'
interface CartItemProps {
isTotalChecked: boolean
setIsTotalChecked: React.Dispatch<React.SetStateAction<boolean>>
item: CartItemsType
selectedProduct:CartItemsType[]
selectProductHandler: (cart: CartItemsType, checked: boolean) => void
setSelectedProduct: React.Dispatch<React.SetStateAction<CartItemsType[]>>
products:CartItemsType[]
setProducts: React.Dispatch<React.SetStateAction<CartItemsType[]>>
}
const CartItem = ({setProducts,products, isTotalChecked, item ,selectedProduct,selectProductHandler,setIsTotalChecked,setSelectedProduct}: CartItemProps) => {
const [isChecked,setIsChecked] = useState(false)
const onClick = () => {
setIsTotalChecked(false);
setIsChecked(prev => !prev);
}
useEffect(() => {
if(!isTotalChecked){
setIsChecked(false)
}
selectProductHandler(item,isTotalChecked)
},[isTotalChecked])
useEffect(() => {
selectProductHandler(item,isChecked);
}, [isChecked]);
useEffect(() => {
const pcsChangeProduct = products.find((it) => it.cartId === item.cartId)
if(!pcsChangeProduct) return;
setSelectedProduct(prev => {
return prev.map(it => {
if(it.cartId === item.cartId){
return {...it,pcs:pcsChangeProduct.pcs}
}else{
return {...it}
}
})
})
}, [products])
return (
<li className="flex my-6 text-sm xs:text-base items-center h-[90px] xs:h-[110px] ">
{/* selectedProduct에 내 cartId가 있으면 true 없으면 false로 작동하게 만든다. */}
<CheckBox
isChecked={selectedProduct.some(product => product.cartId === item.cartId)}
onClick={onClick}
bgColor="bg-lenssisDark"
/>
<img className="w-[90px] xs:w-[120px] h-[100px] xs:h-[120px]" src={item.imageUrl} alt="" />
<div className="ml-[6px] xs:ml-4 grow flex flex-col">
<div className="mb-2">
{item.name} - {item.color}
</div>
<div className="mb-3 xs:mb-0">
<p className="line-through text-[10px] xs:text-sm">{item.price.toLocaleString()}円</p>
<p className="font-bold text-xs xs:text-lg text-black pb-1 xs:pb-4">
{(item.price - item.price * (item.discount / 100)).toLocaleString()}円
</p>
</div>
<div>
<Counter item={item} pcs={item.pcs} products={products} setProducts={setProducts} />
</div>
</div>
<div className=" min-w-[30px] xs:min-w-[40px]">
<button className="underline text-lenssisStroke">삭제</button>
</div>
</li>
)
}
export default CartItem
Counter.tsx
import React, { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import { selectProduct } from '../../store/selectProduct';
import useCart, { CartItemsType } from './hooks/useCart';
interface CounterProps {
pcs:number
item:CartItemsType
products:CartItemsType[]
setProducts: React.Dispatch<React.SetStateAction<CartItemsType[]>>
}
const Counter = ({setProducts,pcs,item,products}:CounterProps) => {
const [selectedProducts,setSelectedProducts] = useRecoilState(selectProduct);
const [count,setCount] = useState(pcs)
const countHandler = (cnt:number) => {
if(count + cnt < 1) return;
setCount(prev => prev += cnt);
let newSelectedProduct = products.map((product) => {
if( product.cartId === item.cartId) {
return {...product,pcs:pcs += cnt};
}else{
return product
}
})
setProducts(newSelectedProduct);
}
return (
<div className='flex h-[15px] xs:h-[26px] w-[50px] xs:w-[88px] items-center'>
<button className='w-[15px] xs:w-[20px] border border-solid border-lenssisStroke ' onClick={() => countHandler(-1)}> - </button>
<div className='w-[20px] xs:w-[30px] xs:h-[26px] text-center border-y border-solid border-lenssisStroke text-sm flex items-center justify-center text-[7px]'>{item.pcs}</div>
<button className='w-[15px] xs:w-[20px] border border-solid border-lenssisStroke 'onClick={() => countHandler(1)}> + </button>
</div>
);
};
export default Counter;
CheckBox.tsx
import { HiCheck } from 'react-icons/hi';
interface CheckBoxProps {
onClick: () => void
isChecked?:boolean;
isTotalChecked?:boolean
bgColor:string;
}
const CheckBox = ({onClick,isChecked,isTotalChecked,bgColor}:CheckBoxProps) => {
return (
<div
onClick={onClick}
className={`flex items-center justify-center h-4 w-4 border border-solid border-lenssisStroke rounded-[5px] ${isChecked && bgColor} transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer`}
>
<HiCheck size={14} color="#ffffff" />
</div>
);
};
export default CheckBox;
Payment.tsx
<CardTemplate title="주문/결제" isTitleVisible={true} marginTop="mt-40">
<div className="pb-12">
<PaymentTitle text="주문 상품" />
<OrderProductName />
<div className="flex flex-col w-full gap-4 items-center justify-center">
{selectedProduct.map((item, index) => (
<div className="flex w-full border-y border-solid border-gray-300" key={item.name + index}>
<div className="flex items-center justify-start w-full gap-4 py-4 flex-1">
<div className="w-16 xs:w-32 flex items-center h-full">
<img src={item.imageUrl} />
</div>
<div className="flex items-start flex-col ">
<div className="text-[#5a5a5a] font-semibold">
{item.name} - <span className="text-sm">{item.color}</span>
</div>
</div>
</div>
<p className="flex justify-center items-center w-[40px] xs:w-[80px] text-xs xs:text-base">
{item.pcs}
</p>
<p className="flex justify-center items-center w-[80px] xs:w-[160px] text-xs xs:text-base">
{((item.price * item.pcs) - (item.price * item.pcs) * (item.discount / 100)).toLocaleString()}円
</p>
</div>
))}
</div>
</div>
</CardTemplate>
store / selectProduct, totalPriceState
import { atom } from "recoil";
import { CartItemsType } from "../components/cart/hooks/useCart";
export const selectProduct = atom<CartItemsType[]>({
key: 'selectProduct',
default: []
})
export const totalPriceState = atom<number>({
key:'totalPriceState',
default:0
})
react-query / useCart
import { AxiosResponse } from 'axios';
import React from 'react';
import { useQuery } from 'react-query';
import { axiosInstance } from '../../axiosinstance';
import { queryKeys } from '../../react-query/queryKeys';
export interface CartItemsType {
cartId:number
color:string
colorCode:string
degree: number
discount: number
graphicDiameter: number
imageUrl:string
name: string
period: number
price: number
productDetailsId: number
stock: number
pcs:number
}
const getCartItems = async():Promise<CartItemsType[]> => {
const {data} = await axiosInstance.get<AxiosResponse<CartItemsType[]>>('/cart/list');
return data.data
}
const useCart = () => {
const fallback:CartItemsType[] = []
const {data:cartItems = fallback} = useQuery(queryKeys.cart, () => getCartItems())
return {cartItems}
};
export default useCart;