## Milestone 1. ### BE * cart.controller.js ```javascript= cartController.deleteCartItem = async (req, res) => { try { const { id } = req.params; const { userId } = req; const cart = await Cart.findOne({ userId }); cart.items = cart.items.filter((item) => !item._id.equals(id)); await cart.save(); res.status(200).json({ status: 200, cartItemQty: cart.items.length }); } catch (error) { return res.status(400).json({ status: "fail", error: error.message }); } }; ``` * cart.api.js ```javascript= router.delete( "/:id", authController.authenticate, cartController.deleteCartItem ); ``` ### FE * cartSlice.js 전체코드 (milestone 1,2,3전체코드) ```javascript= import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import api from "../../utils/api"; import { showToastMessage } from "../common/uiSlice"; const initialState = { loading: false, error: "", cartList: [], selectedItem: {}, cartItemCount: 0, totalPrice: 0, }; // Async thunk actions export const addToCart = createAsyncThunk( "cart/addToCart", async ({ id, size }, { rejectWithValue, dispatch }) => { try { const response = await api.post("/cart", { size, productId: id, qty: 1 }); if (response.status !== 200) throw new Error(response.error); dispatch( showToastMessage({ message: "카트에 아이템이 추가됐습니다!", status: "success", }) ); return response.data.cartItemQty; } catch (error) { dispatch(showToastMessage({ message: error.error, status: "error" })); return rejectWithValue(error.error); } } ); export const getCartList = createAsyncThunk( "cart/getCartList", async (_, { rejectWithValue, dispatch }) => { try { const response = await api.get("/cart"); if (response.status !== 200) throw new Error(response.error); return response.data.data; } catch (error) { dispatch(showToastMessage({ message: error, status: "error" })); return rejectWithValue(error); } } ); export const deleteCartItem = createAsyncThunk( "cart/deleteCartItem", async (id, { rejectWithValue, dispatch }) => { try { const response = await api.delete(`/cart/${id}`); if (response.status !== 200) throw new Error(response.error); dispatch(getCartList()); return response.data.cartItemQty; } catch (error) { dispatch(showToastMessage({ message: error, status: "error" })); return rejectWithValue(error); } } ); export const updateQty = createAsyncThunk( "cart/updateQty", async ({ id, value }, { rejectWithValue }) => { try { const response = await api.put(`/cart/${id}`, { qty: value }); if (response.status !== 200) throw new Error(response.error); return response.data.data; } catch (error) { return rejectWithValue(error); } } ); export const getCartQty = createAsyncThunk( "cart/getCartQty", async (_, { rejectWithValue, dispatch }) => { try { const response = await api.get("/cart/qty"); if (response.status !== 200) throw new Error(response.error); return response.data.qty; } catch (error) { dispatch(showToastMessage({ message: error, status: "error" })); return rejectWithValue(error); } } ); const cartSlice = createSlice({ name: "cart", initialState, reducers: { initialCart: (state) => { state.cartItemCount = 0; }, // You can still add reducers here for non-async actions if necessary }, extraReducers: (builder) => { builder .addCase(addToCart.pending, (state) => { state.loading = true; }) .addCase(addToCart.fulfilled, (state, action) => { state.loading = false; state.cartItemCount = action.payload; }) .addCase(addToCart.rejected, (state, action) => { state.loading = false; state.error = action.payload; }) .addCase(getCartList.fulfilled, (state, action) => { state.loading = false; state.cartList = action.payload; state.totalPrice = action.payload.reduce( (total, item) => total + item.productId.price * item.qty, 0 ); }) .addCase(updateQty.fulfilled, (state, action) => { state.loading = false; state.cartList = action.payload; state.totalPrice = action.payload.reduce( (total, item) => total + item.productId.price * item.qty, 0 ); }) .addCase(deleteCartItem.fulfilled, (state, action) => { state.cartItemCount = action.payload; }) .addCase(getCartQty.fulfilled, (state, action) => { state.cartItemCount = action.payload; }); }, }); export default cartSlice.reducer; export const { initialCart } = cartSlice.actions; ``` * CartProductCard.js (Milestone 1,2,3전체 코드) ```javascript= import React from "react"; import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { Row, Col, Form } from "react-bootstrap"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useDispatch } from "react-redux"; import { currencyFormat } from "../../../utils/number"; import { updateQty, deleteCartItem } from "../../../features/cart/cartSlice"; const CartProductCard = ({ item }) => { const dispatch = useDispatch(); const handleQtyChange = (id, value) => { dispatch(updateQty({ id, value })); }; const deleteCart = (id) => { dispatch(deleteCartItem(id)); }; return ( <div className="product-card-cart"> <Row> <Col md={2} xs={12}> <img src={item.productId.image} width={112} alt="product" /> </Col> <Col md={10} xs={12}> <div className="display-flex space-between"> <h3>{item.productId.name}</h3> <button className="trash-button"> <FontAwesomeIcon icon={faTrash} width={24} onClick={() => deleteCart(item._id)} /> </button> </div> <div> <strong>₩ {currencyFormat(item.productId.price)}</strong> </div> <div>Size: {item.size}</div> <div>Total: ₩ {currencyFormat(item.productId.price * item.qty)}</div> <div> Quantity: <Form.Select onChange={(event) => handleQtyChange(item._id, event.target.value) } required defaultValue={item.qty} className="qty-dropdown" > <option value={1}>1</option> <option value={2}>2</option> <option value={3}>3</option> <option value={4}>4</option> <option value={5}>5</option> <option value={6}>6</option> <option value={7}>7</option> <option value={8}>8</option> <option value={9}>9</option> <option value={10}>10</option> </Form.Select> </div> </Col> </Row> </div> ); }; export default CartProductCard; ``` * OrderReciept.js (Milestone 1,2,3전체코드) ```javascript= import React from "react"; import { Button } from "react-bootstrap"; import { useNavigate } from "react-router"; import { useLocation } from "react-router-dom"; import { currencyFormat } from "../utils/number"; const OrderReceipt = ({ cartList, totalPrice }) => { const location = useLocation(); const navigate = useNavigate(); return ( <div className="receipt-container"> <h3 className="receipt-title">주문 내역</h3> <ul className="receipt-list"> {cartList.length > 0 && cartList.map((item) => ( <li key={item._id}> <div className="display-flex space-between"> <div>{item.productId.name}</div> <div>₩ {currencyFormat(item.productId.price * item.qty)}</div> </div> </li> ))} </ul> <div className="display-flex space-between receipt-title"> <div> <strong>Total:</strong> </div> <div> <strong>₩ {currencyFormat(totalPrice)}</strong> </div> </div> {location.pathname.includes("/cart") && cartList.length > 0 && ( <Button variant="dark" className="payment-button" onClick={() => navigate("/payment")} > 결제 계속하기 </Button> )} <div> 가능한 결제 수단 귀하가 결제 단계에 도달할 때까지 가격 및 배송료는 확인되지 않습니다. <div> 30일의 반품 가능 기간, 반품 수수료 및 미수취시 발생하는 추가 배송 요금 읽어보기 반품 및 환불 </div> </div> </div> ); }; export default OrderReceipt; ``` ## Milestone 2. ### BE * cart.controller.js ```javascript= cartController.editCartItem = async (req, res) => { try { const { userId } = req; const { id } = req.params; const { qty } = req.body; const cart = await Cart.findOne({ userId }).populate({ path: "items", populate: { path: "productId", model: "Product", }, }); if (!cart) throw new Error("There is no cart for this user"); const index = cart.items.findIndex((item) => item._id.equals(id)); if (index === -1) throw new Error("Can not find item"); cart.items[index].qty = qty; await cart.save(); res.status(200).json({ status: 200, data: cart.items }); } catch (error) { return res.status(400).json({ status: "fail", error: error.message }); } }; ``` * cart.api.js ```javascript= router.put("/:id", authController.authenticate, cartController.editCartItem); ``` * FE코드는 milestone1 에 cartSlice.js 참고 ## Milestone 3. ### BE * cart.controller.js ```javascript= cartController.getCartQty = async (req, res) => { try { const { userId } = req; const cart = await Cart.findOne({ userId: userId }); if (!cart) throw new Error("There is no cart!"); res.status(200).json({ status: 200, qty: cart.items.length }); } catch (error) { return res.status(400).json({ status: "fail", error: error.message }); } }; ``` * cart.api.js ```javascript= router.get("/qty", authController.authenticate, cartController.getCartQty); ``` ### FE * Applayout.js ```javascript= useEffect(() => { if (user) { dispatch(getCartQty()); } }, [user, dispatch]); ``` * FE코드는 milestone1 에 cartSlice.js 참고