데브코스

[15주차 - DAY2] 도서 정보 사이트 - 장바구니 목록

미안하다 강림이 좀 늦었다 2024. 6. 4. 14:58

 

 

장바구니

api/carts.api.ts

백엔드의 api로 요청을 보내는 함수를 작성한다. 각각 장바구니 추가, 장바구니 목록 가져오기, 장바구니에서 삭제 함수이다.

import { Cart } from "../models/cart.model";
import { httpClient } from "./http";

interface AddCartParams {
    book_id: number;
    quantity: number;
}

export const addCart = async (params: AddCartParams) => {
    const response = await httpClient.post('/carts', params);

    return response.data;
}

export const fetchCart = async () => {
    const response = await httpClient.get<Cart[]>('/carts');

    return response.data;
}

export const deleteCart = async (cartId: number) => {
    const response = await httpClient.delete(`/carts/${cartId}`);

    return response.data;

}

 

hooks/useCart.ts

장바구니 훅을 생성한다. 이 훅으로부터 장바구니 목록들과 장바구니가 비어있는지에 대한 여부, 장바구니 아이템 삭제 함수를 사용할 수 있다.

import { useEffect, useState } from "react"
import { Cart } from "../models/cart.model"
import { deleteCart, fetchCart } from "../api/carts.api";

export const useCart = () => {
    const [carts, setCarts] = useState<Cart[]>([]);
    const [isEmpty, setIsEmpty] = useState(true);

    const deleteCartItem = (id: number) => {
        deleteCart(id).then(() => {
            setCarts(carts.filter(cart => cart.id !== id));
        })
    }

    useEffect(() => {
        fetchCart().then((carts) => {
            setCarts(carts);
            setIsEmpty(carts.length === 0);
        })
    }, [])

    return { carts, isEmpty, deleteCartItem };
}

 

pages/Cart.tsx

checkedItems는 체크된 상품들의 id를 담아놓은 배열이다. 해당 배열을 CartItem 컴포넌트로 내려줘서 해당 컴포넌트에서 체크 목록에 자기가 있는지 확인한다. 그리고, handleCheckItem도 내려줘서 하위 컴포넌트에서 체크를 하면 checkedItems를 변경시킬 수 있도록 한다.

앞서 만들었던 cart 훅에서 deleteCartItem을 가져와서 handleItemDelete 함수를 만든다. 이 함수를 CartItem으로 내려준다.

...

function Cart() {
    ...

    const { carts, deleteCartItem, isEmpty } = useCart();

    const [checkedItems, setCheckedItems] = useState<number[]>([]);

    const handleCheckItem = (id: number) => {
        if (checkedItems.includes(id)) {
            setCheckedItems(checkedItems.filter((item) => item !== id))
        } else {
            setCheckedItems([...checkedItems, id]);
        }
    }

    const handleItemDelete = (id: number) => {
        setCheckedItems(checkedItems.filter(item => item !== id));
        deleteCartItem(id);
    }

    ...

    return (
        <>
            <Title size="large">장바구니</Title>
            <CartStyle>
                {
                    !isEmpty && (
                        <>
                            <div className="content">
                                {
                                    carts.map((item) => (
                                        <CartItem
                                            key={item.id}
                                            cart={item}
                                            checkedItems={checkedItems}
                                            onCheck={handleCheckItem}
                                            onDelete={handleItemDelete}
                                        />
                                    ))
                                }
                            </div>
                            <div className="summary">
                                ...
                            </div>
                        </>
                    )
                }
                ...
            </CartStyle >
        </>
    )
}

...

 

components/cart/CartItem.tsx

useMemo를 사용하여 이 아이템이 체크되어 있는지 확인한다.

handleCheck 함수를 만들어서 하위 컴포넌트인 CheckIconButton에서 클릭이 발생할 경우 상위 컴포넌트인 Cart의 체크된 아이템 배열에 해당 아이템의 아이디가 추가될 수 있도록 한다.

장바구니 삭제 버튼이 눌리면 장바구니에서 삭제되도록 handleDelete 함수를 생성하고, 해당 함수 내부에 prop으로 받은 onDelete 함수를 사용한다.

...

function CartItem({ cart, checkedItems, onCheck, onDelete }: Props) {
    const { showConfirm } = useAlert();

    // checkedItems 목록에 내가 있는지 판단
    const isChecked = useMemo(() => {
        return checkedItems.includes(cart.id);
    }, [checkedItems, cart.id]);

    const handleCheck = () => {
        onCheck(cart.id);
    }

    const handleDelete = () => {
        showConfirm('정말 삭제하시겠습니까?', () => {
            onDelete(cart.id);
        });
    }

    return (
        <CartItemStyle>
            <div className="info">
                <div className="check">
                    <CheckIconButton
                        isChecked={isChecked}
                        onCheck={handleCheck}
                    />
                </div>
                <div>
                    <Title size="medium">{cart.title}</Title>
                    <p className="summary">{cart.summary}</p>
                    <p className="price">{formatNumber(cart.price)} 원</p>
                    <p className="quantity">{cart.quantity} 권</p>
                </div>
            </div>
            <Button size='medium' scheme='normal' onClick={handleDelete}>
                장바구니 삭제
            </Button>
        </CartItemStyle>
    )
}

...

 

 

주문하기

체크된 항목들의 총 가격과 총 수량이 장바구니 목록 옆에 표시되어야 한다.

총 수량과 총 가격은 useMemo를 사용하여 계산한다.

주문하기 버튼이 눌리면 현재 체크되어 있는 상품들의 id, 총 수량, 총 가격, 첫 책의 제목을 navigate를 사용하여 /order 페이지로 넘겨준다.

...
function Cart() {
    const { showAlert, showConfirm } = useAlert();
    const navigate = useNavigate();

    const { carts, deleteCartItem, isEmpty } = useCart();

    const [checkedItems, setCheckedItems] = useState<number[]>([]);

    ...

    const totalQuantity = useMemo(() => {
        return carts.reduce((acc, cart) => {
            if (checkedItems.includes(cart.id)) {
                return acc + cart.quantity;
            }
            return acc;
        }, 0)
    }, [carts, checkedItems]);

    const totalPrice = useMemo(() => {
        return carts.reduce((acc, cart) => {
            if (checkedItems.includes(cart.id)) {
                return acc + cart.price * cart.quantity;
            }
            return acc;
        }, 0)
    }, [carts, checkedItems]);

    const handleOrder = () => {
        if (checkedItems.length === 0) {
            showAlert('주문할 상품을 선택해 주세요.');
            return;
        }

        const orderData: Omit<OrderSheet, 'delivery'> = {
            items: checkedItems,
            totalPrice,
            totalQuantity,
            firstBookTitle: carts[0].title
        };

        showConfirm('주문하기겠습니까?', () => {
            navigate('/order', { state: orderData });
        });
    }

    return (
        <>
            <Title size="large">장바구니</Title>
            <CartStyle>
                {
                    !isEmpty && (
                        <>
                            <div className="content">
                                ...
                            </div>
                            <div className="summary">
                                <CartSummary
                                    totalQuantity={totalQuantity}
                                    totalPrice={totalPrice}
                                />
                                <Button
                                    size="large"
                                    scheme="primary"
                                    onClick={handleOrder}
                                >
                                    주문하기
                                </Button>
                            </div>
                        </>
                    )
                }
                ...
            </CartStyle >
        </>
    )
}

...

 

 

배운 점

  • useNavigate를 사용하여 다른 페이지로 데이터를 넘겨줄 수 있다.
  • useMemo는 deps가 변경되면 다시 계산할 때 사용한다. 값을 기억해 놨다가 deps가 그대로면 다시 계산하지 않고 기억해 놨던 값을 반환해 준다.