react-shop-web/front & back

카트에 들어있는 상품들 가격 계산 & 카트에 들어있는 상품 지우기

tjdvyzl 2023. 2. 14. 15:01
더보기
import React, {useEffect, useState} from 'react'
import {useDispatch} from 'react-redux';
import {getCartItems, removeCartItem} from '../../../_actions/user_actions';
import UserCardBlock from './Sections/UserCardBlock';

function CartPage(props) {
  const dispatch = useDispatch();

  const [Total, setTotal] = useState(0);

  useEffect(() => {

    let cartItems = []
    
    // 리덕스 user state에서 cart안에 상품이 들어있는지 확인 
    if(props.user.userData && props.user.userData.cart) {
      // 카트 안에 상품이 하나 이상 들어있다면 
      if(props.user.userData.cart.length > 0){
        props.user.userData.cart.forEach(item => {
          cartItems.push(item.id)
        })

        // 밑에 액션에 매개변수로 방금 props로부터 받은 user의 userData에서 cart들을 담은 변수와,
        // userData의 cart 정보도 주자. (이때 두번째 매개변수를 넣어주는 이유는 quantity는 user의 cart 부분에만 존재하기 때문)
        dispatch(getCartItems(cartItems, props.user.userData.cart))
          .then(response => {calculateTotal(response.payload)})
      }
    }
  
    return () => {
      
    }
  }, [props.user.userData])
  
  const calculateTotal = (cartDetail) => {
    let total = 0;

    cartDetail.map(item => {
      total += parseInt(item.price) * item.quantity
    })

    setTotal(total);
  }

  let removeFromCart = (productId) => {

    dispatch(removeCartItem(productId))
      .then(response => {
        calculateTotal(response.payload.productInfo)
      })
  }

  return (
    <div style={{ width: '85%', margin: '3rem auto'}}>
      <h1>My Cart</h1>
      {/* 밑에 props 주는 과정에서도 
      페이지가 렌더링 될 때는 return값을 먼저 렌더링되고 그 후에 props에 값이 들어올 때도 있다. 이때도 useEffect의
      빈칸에 props.user.userData를 넣어주지 않으면 useEffect 안에 있는 조건문이 통과되지 않기 때문에 cartItems에는 아무것도 담기지 않는다.
      UserCardBlock 컴포넌트에 props를 줄 때도 현재 컴포넌트 props에 값이 담겨있을 때를 조건으로 줘야 오류가 나지 않는다. */}
      <div>
        <UserCardBlock products={props.user.cartDetail && props.user.cartDetail} removeItem={removeFromCart}/>  
      </div>

      <div style={{marginTop: '3rem'}}>
        <h2>Total Amount: ${Total}</h2>
      </div>
    </div>
  )
}

export default CartPage

CartPage.js

 

 

더보기
import React from 'react'
import "./UserCardBlock.css"

function UserCardBlock(props) {
  
  const renderCartImage = (images) => {
    if(images.length > 0) {
      let image = images[0] 
      return `${process.env.REACT_APP_SERVER}/${image}`
    }
  }

  const renderItems = () => (
    props.products && props.products.map((product, index) => (
      <tr key={index}>
        <td>
          <img style={{width:'70px'}} alt="product"
          src={renderCartImage(product.images)} />
        </td>
        <td>
          {product.quantity} EA
        </td>
        <td>
          $ {product.price}
        </td>
        <td>
          <button onClick={() => props.removeItem(product._id)}>
            Remove
          </button>
        </td>
      </tr>
    ))
  )

  return (
    <div>
      <table>
        <thead>
          <tr>
            <th>Product Image</th>
            <th>Product Quantity</th>
            <th>Product Price</th>
            <th>Remove From Cart</th>
          </tr>
        </thead> 
        <tbody>
          {renderItems()}
        </tbody>
      </table>
    </div>
  )
}

export default UserCardBlock

UserCardBlock.js

 

 

export function removeCartItem(productId){

    // 상세보기 페이지 만들 때 상품을 products_by_id 엔드포인트에서 갖고왔었다. 
    // 이때는 상품을 하나만 갖고와서 single로 type을 줬었지만 
    // 매개변수로 받은 cartItems에 여러 개의 cart id가 존재할 수 있기 때문에
    // 이번엔 type을 array로 줘야한다. 

    const request = axios.get(`/api/users/removeFromCart?id=${productId}`)
        .then(response => {

            // productInfo, cart 정보를 조합해서 cartDetail를 만든다.
            response.data.cart.forEach(item => {
                response.data.productInfo.forEach((product,index) => {
                    if(item.id === product._id){
                        response.data.productInfo[index].quantity = item.quantity;
                    }
                })
            })
        
        return response.data;
        
    });

    return {
        type: REMOVE_CART_ITEM,
        payload: request
    }
}

user_actions.js

 

 

router.get("/removeFromCart", auth, (req,res) => {

    // 먼저 cart 안에 내가 지우려고 한 상품을 지워주기 
    User.findOneAndUpdate(
        {_id: req.user._id},
        {
            "$pull":
                {"cart": {"id": req.query.id}}
        },
        { new: true},
        (err, userInfo) => {
             // product collection에서 현재 남아있는 상품들의 정보를 가져오기 
            let cart = userInfo.cart;
            let array = cart.map(item => {
                return item.id
            })
            
            Product.find({_id: {$in: array}})
                .populate('writer')
                .exec((err, productInfo) => {
                    return res.status(200).json({
                        productInfo,
                        cart
                    })
                })
        }
    )

})

server/routes/users.js

 

 

 

export const REMOVE_CART_ITEM = 'remove_cart_item';

types.js

 

 

import {
    LOGIN_USER,
    REGISTER_USER,
    AUTH_USER,
    LOGOUT_USER,
    ADD_TO_CART,
    GET_CART_ITEMS,
    REMOVE_CART_ITEM,
} from '../_actions/types';
 

export default function(state={},action){
    switch(action.type){
        case REGISTER_USER:
            return {...state, register: action.payload }
        case LOGIN_USER:
            return { ...state, loginSucces: action.payload }
        case AUTH_USER:
            return {...state, userData: action.payload }
        case LOGOUT_USER:
            return {...state }
        case ADD_TO_CART:
            return {...state,
                userData: {
                    ...state.userData,
                    cart: action.payload
                }
            }
        case GET_CART_ITEMS:
            return {...state, cartDetail: action.payload }
        case REMOVE_CART_ITEM:
            return {...state, cartDetail: action.payload.productInfo,
                ...state.userData, cart: action.payload.cart}
        default:
            return state;
    }
}

user_reducer.js

 

 

router.get("/removeFromCart", auth, (req,res) => {

    // 먼저 cart 안에 내가 지우려고 한 상품을 지워주기 
    User.findOneAndUpdate(
        {_id: req.user._id},
        {
            "$pull":
                {"cart": {"id": req.query.id}}
        },
        { new: true},
        (err, userInfo) => {
             // product collection에서 현재 남아있는 상품들의 정보를 가져오기 
            let cart = userInfo.cart;
            let array = cart.map(item => {
                return item.id
            })
            
            Product.find({_id: {$in: array}})
                .populate('writer')
                .exec((err, productInfo) => {
                    return res.status(200).json({
                        productInfo,
                        cart
                    })
                })
        }
    )

})

users.js

 

 

상품들 가격 계산하는 과정은 쉬워서 굳이 설명을 안해놔도 될 것 같다.

 

위 과정은 다음과 같다

1. UserCardBlock 컴포넌트에서 Remove 버튼을 눌렀을 때 부모 컴포넌트인 CartPage에서 props로 준 removeItem에 매개변수로 해당 상품의 id값이 들어가면서 호출된다. 

 

2. CartPage에선 매개변수로 받은 상품 id를 removeCartItem 액션에 매개변수로 넣어주면서 바로 호출한다.

 

3. user_actions.js에서 removeCartItem 액션이 query로 매개변수로 받은 상품 id값을 주면서 users.js의 removeFromCart 라우터에 해당 상품을 제거하도록 요청한다. 

 

4. users.js의 removeFromCart 라우터에선 $pull 문법을 이용하여 cart로부터 쿼리로 받은 상품 id값과 일치하는 상품을 제거한다.

그리고 전에서 나왔듯이 find()에 여러개의 상품을 찾게되는 경우가 있기 때문에 $in 문법을 사용한다.

쿼리로 받은 상품 id의 상품을 제거한 후의 cart를 map으로 하나 하나 뽑아서 array에 배열 형태로 만들어서 넣어준다.

이것을 $in 에 넣어서 모든 상품을 찾은 productInfo와 user의 cart를 client에 보낸다. 

 

5. 다시 user_actions.js에선 4번에서 받은 productInfo와 cart를 이용하여 이중 반복문으로 각각 탐색한다.

탐색하는 과정에서 cart의 id값과 productInfo의 id값이 같다면 response.data.product[index].quantity = item.quantity를 통해 

quantity 필드를 만들어 줌과 동시에 cart 모델만 갖고 있던 quantity값을 넣어준다. 이 response.data를 payload로 return한다. 

 

6. user_action까지 다 마친 후 user_reducer에선 REMOVE_CART_ITEM에 해당하는 케이스를 처리한다.

이때 기존에 있던 state와 userData는 그대로 넣어주고 위에서 quantity 필드를 만들어주면서 값까지 넣어줬던 response.data.product를 cartDetail에 넣어주고, cart는 유저의 cart를 넣어준다.

이렇게 되면 redux-dev tools에선 state, userData, cartDetail, cart 목록이 있을 것이다.