장바구니
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가 그대로면 다시 계산하지 않고 기억해 놨던 값을 반환해 준다.
'데브코스' 카테고리의 다른 글
[15주차 주간 발표] Flex, Grid (0) | 2024.06.05 |
---|---|
[15주차 - DAY3] 도서 정보 사이트 - 주문서 (0) | 2024.06.05 |
[15주차 - DAY1] 도서 정보 사이트 - 도서 상세 페이지 (0) | 2024.06.03 |
[14주차 - DAY5] 도서 정보 사이트 - 로그인과 도서 목록 페이지 (0) | 2024.05.31 |
[14주차 주간 발표] CORS (0) | 2024.05.30 |