react) 장바구니 수량 조절 + 전체 선택 + 개별 선택 + 선택 합계 금액 + 페이지 이동

김명성·2022년 10월 6일
0
post-thumbnail
post-custom-banner

글 작성 이전입니다


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)}>&nbsp;-&nbsp;</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)}>&nbsp;+&nbsp;</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;
post-custom-banner

0개의 댓글