관리 메뉴

거니의 velog

(47) 리액트 장바구니 구성 2 본문

SpringBoot_React 풀스택 프로젝트

(47) 리액트 장바구니 구성 2

Unlimited00 2024. 3. 8. 19:08

3. 장바구니 아이템 컴포넌트

* 브라우저에서는 장바구니에 있는 장바구니 아이템 목록의 개수만 출력해 주었으므로 실제 내용물을 보여주는 컴포넌트를 구성해 본다.

* components 폴더에 cart 폴더를 추가하고 CartItemComponent를 추가한다.

* CartItemComponent 는 장바구니 아이템을 출력하는 용도이므로 이에 대한 데이터를 속성으로 전달받도록 구성한다.

import React from "react";

const CartItemComponent = ({ cino, pname, price, pno, qty, imageFile }) => {
  return (
    <>
      <div>
        {cino} -- {pname}
      </div>
    </>
  );
};

export default CartItemComponent;

* menus 폴더의 CartComponent에서 각 장바구니 아이템을 CartItemComponent로 출력한다.

import React, { useEffect } from "react";
import useCustomLogin from "../../hooks/useCustomLogin";
import useCustomCart from "../../hooks/useCustomCart";
import CartItemComponent from "../cart/CartItemComponent";

const CartComponent = () => {
  const { isLogin, loginState } = useCustomLogin();
  const { refreshCart, cartItems } = useCustomCart();

  useEffect(() => {
    if (isLogin) {
      refreshCart();
    }
  }, [isLogin]); // refreshCart 를 의존성 배열에 넣으면 무한루프에 걸리므로 뺀다.

  return (
    <div className="w-full">
      {isLogin ? (
        <div className="flex flex-col">
          <div className="w-full flex">
            <div className="font-extrabold text-2xl w-4/5">
              {loginState.nickname}'s Cart
            </div>
            <div className="bg-orange-600 text-center text-white font-bold w-1/5 rounded-full m-1">
              {cartItems.length}
            </div>
          </div>
          <div>
            <ul>
              {cartItems.map((item) => (
                <CartItemComponent {...item} key={item.cino} />
              ))}
            </ul>
          </div>
        </div>
      ) : (
        <div></div>
      )}
    </div>
  );
};

export default CartComponent;

* 브라우저에서는 장바구니의 아이템에 대한 정보가 출력되는 것을 볼 수 있다.


(1) 장바구니 아이템 출력

* cart 폴더의 CartItemComponent는 <ul> 안에 들어가는 내용물이므로 <li>를 이용해서 구현한다.

import React from "react";
import { API_SERVER_HOST } from "../../api/todoApi";

const host = API_SERVER_HOST;

const CartItemComponent = ({ cino, pname, price, pno, qty, imageFile }) => {
  const handleClickQty = (amount) => {};

  return (
    <li key={cino} className="border-2">
      <div className="w-full border-2">
        <div className=" m-1 p-1 ">
          <img
            src={`${host}/api/products/view/s_${imageFile}`}
            alt="장바구니 썸네일 이미지"
          />
        </div>

        <div className="justify-center p-2 text-xl ">
          <div className="justify-end w-full"></div>
          <div>Cart Item No: {cino}</div>
          <div>Pno: {pno}</div>
          <div>Name: {pname}</div>
          <div>Price: {price}</div>
          <div className="flex ">
            <div className="w-2/3">Qty: {qty}</div>
            <div>
              <button
                className="m-1 p-1 text-2xl bg-orange-500 w-8 rounded-lg"
                onClick={() => handleClickQty(1)}
              >
                +
              </button>
              <button
                className="m-1 p-1 text-2xl bg-orange-500 w-8 rounded-lg"
                onClick={() => handleClickQty(-1)}
              >
                -
              </button>
            </div>
          </div>
          <div>
            <div className="flex text-white font-bold p-2 justify-center">
              <button
                className="m-1 p-1 text-xl text-white bg-red-500 w-8 rounded-lg"
                onClick={() => handleClickQty(-1 * qty)}
              >
                X
              </button>
            </div>
            <div className="font-extrabold border-t-2 text-right m-2 pr-4">
              {qty * price} 원
            </div>
          </div>
        </div>
      </div>
    </li>
  );
};

export default CartItemComponent;

* CartItemComponent에는 수량을 변경하는 버튼(+,-)과 삭제 버튼(x)를 만들어서 상품의 수량을 변경하고, 이를 handleClickQty()를 이용해서 처리한다. 특히, 삭제는 현재 수량만큼 음수의 값을 더해서 0으로 변경하는데 API 서버에서는 수량이 0으로 변경되면 장바구니 아이템이 삭제되는 것을 이용하기 위해서이다. 하나의 장바구니 아이템은 아래 오른쪽 그림과 같은 구성을 가지게 된다.


[수량 변경 이벤트 처리]

* 수량을 변경하는 부분은 이미 useCustomCart() 에서 꺼낼 수 있는 changeCart를 CartItemComponent에 속성으로 전달해서 사용한다.

import React, { useEffect } from "react";
import useCustomLogin from "../../hooks/useCustomLogin";
import useCustomCart from "../../hooks/useCustomCart";
import CartItemComponent from "../cart/CartItemComponent";

const CartComponent = () => {
  const { isLogin, loginState } = useCustomLogin();
  const { refreshCart, cartItems, changeCart } = useCustomCart();

  useEffect(() => {
    if (isLogin) {
      refreshCart();
    }
  }, [isLogin]); // refreshCart 를 의존성 배열에 넣으면 무한루프에 걸리므로 뺀다.

  return (
    <div className="w-full">
      {isLogin ? (
        <div className="flex flex-col">
          <div className="w-full flex">
            <div className="font-extrabold text-2xl w-4/5">
              {loginState.nickname}'s Cart
            </div>
            <div className="bg-orange-600 text-center text-white font-bold w-1/5 rounded-full m-1">
              {cartItems.length}
            </div>
          </div>
          <div>
            <ul>
              {cartItems.map((item) => (
                <CartItemComponent
                  {...item}
                  key={item.cino}
                  changeCart={changeCart}
                />
              ))}
            </ul>
          </div>
        </div>
      ) : (
        <div></div>
      )}
    </div>
  );
};

export default CartComponent;

* CartItemComponent는 전달받은 changeCart 를 이용해서 호출한다.

import React from "react";
import { API_SERVER_HOST } from "../../api/todoApi";

const host = API_SERVER_HOST;

const CartItemComponent = ({
  cino,
  pname,
  price,
  pno,
  qty,
  imageFile,
  changeCart,
}) => {
  const handleClickQty = (amount) => {
    changeCart({ cino: cino, pno: pno, qty: qty + amount });
  };

  return (
    <li key={cino} className="border-2">
      (...)
    </li>
  );
};

export default CartItemComponent;

* changeCart()로 전달되는 파라미터에는 사용자의 이메일이 없기 때문에 403(Forbidden) 에러가 발생한다.


[이메일 전달]

* 장바구니 아이템의 수량을 변경하거나 추가하기 위해서 현재 사용자의 이메일이 필요하다. 이에 대한 처리를 CartComponent에서 추출해서 CartItemComponent의 속성으로 전달해서 처리한다.

            <ul>
              {cartItems.map((item) => (
                <CartItemComponent
                  {...item}
                  key={item.cino}
                  changeCart={changeCart}
                  email={loginState.email}
                />
              ))}
            </ul>

* CartItemComponent에서는 email 속성을 추가해서 장바구니 수량 변경 시에 사용한다.

import React from "react";
import { API_SERVER_HOST } from "../../api/todoApi";

const host = API_SERVER_HOST;

const CartItemComponent = ({
  cino,
  pname,
  price,
  pno,
  qty,
  imageFile,
  changeCart,
  email,
}) => {
  const handleClickQty = (amount) => {
    changeCart({ email, cino, pno, qty: qty + amount });
  };

* 브라우저에서 버튼들을 클릭해 보면 수량과 해당 아이템의 가격과 수량을 곱한 가격이 변하는 것을 확인할 수 있다.

* 화면에서 삭제 버튼을 클릭하면 해당 장바구니 아이템은 삭제되는 것을 확인할 수 있다.

* 위의 화면을 보면 CartComponent의 마지막에 현재 모든 장바구니 아이템의 수량과 가격의 합산을 볼 수 있는데 이는 이와 같이 구현할 수 있다.

import React, { useEffect, useMemo } from "react";
import useCustomLogin from "../../hooks/useCustomLogin";
import useCustomCart from "../../hooks/useCustomCart";
import CartItemComponent from "../cart/CartItemComponent";

const CartComponent = () => {
  const { isLogin, loginState } = useCustomLogin();
  const { refreshCart, cartItems, changeCart } = useCustomCart();

  const total = useMemo(() => {
    let total = 0;

    for (const item of cartItems) {
      total += item.price * item.qty;
    }

    return total;
  }, [cartItems]);

  useEffect(() => {
    if (isLogin) {
      refreshCart();
    }
  }, [isLogin]); // refreshCart 를 의존성 배열에 넣으면 무한루프에 걸리므로 뺀다.

  return (
    <div className="w-full">
      {isLogin ? (
        <div className="flex flex-col">
        
          (...)
          
          <div>
            <div className="text-2xl text-right font-extrabold">
              TOTAL: {total}
            </div>
          </div>
        </div>
      ) : (
        <div></div>
      )}
    </div>
  );
};

export default CartComponent;

* 브라우저에서는 상품의 수량을 변경하거나 삭제했을 때 전체 가격이 변경되는 것을 확인할 수 있다.