본문 바로가기

react-shop-web/front & back

Add To Cart 구현하기

#1

현재 server/models에서 Product 모델이나 User 모델을 보면 Cart에 관한 필드나 모델이 존재하지 않는다. 이 상태면 DB에 카트에 관한 정보를 저장할 수 없다. 이러면 카트에 관한 모델을 만들거나 따로 처리를 해줘야한다.

 

강의에서는 User 모델에 Cart 필드를 만들어서 관리를 했다.

 

가이드 라인은 다음과 같다.

위 이미지에서 history 필드는 결제를 하고 나면 어떤 상품을 샀는지 등등이 남을 건데 이것을 관리한다.

 

 

User 모델에 Cart 필드랑 history 필드를 넣어주자.

더보기
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const saltRounds = 10;
const jwt = require('jsonwebtoken');
const moment = require("moment");

const userSchema = mongoose.Schema({
    name: {
        type:String,
        maxlength:50
    },
    email: {
        type:String,
        trim:true,
        unique: 1 
    },
    password: {
        type: String,
        minglength: 5
    },
    lastname: {
        type:String,
        maxlength: 50
    },
    role : {
        type:Number,
        default: 0 
    },
    cart: {
        type: Array,
        default: []
    },
    history: {
        type: Array,
        default: []
    },
    image: String,
    token : {
        type: String,
    },
    tokenExp :{
        type: Number
    }
})


userSchema.pre('save', function( next ) {
    var user = this;
    
    if(user.isModified('password')){    
        // console.log('password changed')
        bcrypt.genSalt(saltRounds, function(err, salt){
            if(err) return next(err);
    
            bcrypt.hash(user.password, salt, function(err, hash){
                if(err) return next(err);
                user.password = hash 
                next()
            })
        })
    } else {
        next()
    }
});

userSchema.methods.comparePassword = function(plainPassword,cb){
    bcrypt.compare(plainPassword, this.password, function(err, isMatch){
        if (err) return cb(err);
        cb(null, isMatch)
    })
}

userSchema.methods.generateToken = function(cb) {
    var user = this;
    console.log('user',user)
    console.log('userSchema', userSchema)
    var token =  jwt.sign(user._id.toHexString(),'secret')
    var oneHour = moment().add(1, 'hour').valueOf();

    user.tokenExp = oneHour;
    user.token = token;
    user.save(function (err, user){
        if(err) return cb(err)
        cb(null, user);
    })
}

userSchema.statics.findByToken = function (token, cb) {
    var user = this;

    jwt.verify(token,'secret',function(err, decode){
        user.findOne({"_id":decode, "token":token}, function(err, user){
            if(err) return cb(err);
            cb(null, user);
        })
    })
}

const User = mongoose.model('User', userSchema);

module.exports = { User }

server/models/User.js

 

조금 흐릿하긴 한데 위 이미지에서 볼 수 있듯이 cart의 구성은 그 상품의 id값, 카트에 넣은 상품의 개수, 카트에 넣은 날짜 이다.

 

더보기
router.post("/addToCart", auth, (req, res) => {
    
    // 먼저 User Collections에 해당 유저의 정보를 가져오기
    // auth 미들웨어를 통과할 때 server/middleware/auth.js를 보면 token을 이용하여 유저 정보를 갖고오고, 이 정보를 req.user = user를 해줌으로써
    // req.user에 유저 정보가 들어가게 된다. 그래서 밑의 findOne에서 user id를 사용할 수 있는 것임
    User.findOne({_id: req.user._id}, 
        (err, userInfo) => {
            // 가져온 정보에서 카트에다 넣으려하는 상품이 이미 들어 있는지 확인
            let duplicate = false;
            userInfo.cart.forEach((item) => {
                // userAction에서 함수를 호출 할 때 body 객체에 productId를 보내줬었다.
                // 반복문을 이용해서 DB에 productId에 해당하는 상품이 존재한다면 카트에 넣으려는 상품이 이미 들어있는 거임
                if(item.id === req.body.productId) {
                    duplicate = true;
                }
            })
            
            // 상품이 이미 있을 때
            if(duplicate) {
                // 상품 개수만 하나 더 올려준다. 
                User.findOneAndUpdate(
                    // 먼저 _id로 user를 찾고, 찾은 user의 cart에서 req.body.productId와 같은 productId를 찾는다.
                    {_id: req.user._id, "cart.id": req.body.productId},
                    // $inc는 increment의 약자. 1을 올려주고싶으면 1을 넣어주고 2를 올려주고싶으면 2를 넣어주면 된다.
                    {$inc: {"cart.$.quantity": 1}},
                    {new: true},
                    // 우리가 위 쿼리를 돌린 다음에 결과값 err나 userInfo를 받을텐데 
                    // userInfo를 받을 때 현재 여기선 하나를 찾아서 업데이트를 시켜준 것이다.
                    // 그리고 업데이트된 정보의 결과값을 받으려면 {new: true} 라는 옵션을 줘야한다. 
                    (err, userInfo) => {
                        if(err) return res.status(200).json({success:false, err})
                        return res.status(200).send(userInfo.cart)
                    }
                )
            } 
            
            // 없을 때 
            // 새로운 상품을 넣을 땐 상품 ID, 개수 1, 날짜 정보를 넣어줘야한다.
            else {
                User.findOneAndUpdate(
                    {_id: req.user._id},
                    {
                        $push: {
                            cart: {
                                id: req.body.productId,
                                quantity: 1,
                                date: Date.now()
                            }
                        }
                        // 위 코드를 해석하자면 push는 어떤것을 넣다라는 의미로 볼 수 있다.
                        // 즉, cart: {} 라는 것은 cart 부분에 push를 한다는 의미이고 
                        // 내용은 안에서 넣어줬던 cart객체와 같다. 
                    },
                    {new: true}, // 업데이트된 정보를 받기 위해선 넣어줘야함.
                    (err, userInfo) => {
                        if(err) return res.status(400).json({success: false, err})
                        res.status(200).send(userInfo.cart)
                    }
                )
            }
        })
});

server/routes/users.js

 

 

export const ADD_TO_CART = 'add_to_cart';

client/src/_actions/types.js

 

 

export function addToCart(id){
    let body = {
        productId : id
    }

    const request = axios.post(`${USER_SERVER}/addToCart`, body)
    .then(response => response.data);

    return {
        type: ADD_TO_CART,
        payload: request
    }
}

client/src/_actions/user_actions.js

 

 

  const clickHandler = () => {
    // 원래라면 여기서 Axios로 처리를 해줬겠지만 유저 관련된 내용은 리덕스를 이용했기 때문에 리덕스로 구현하자.

    // 필요한 정보를 Cart 필드에다가 넣어준다.
    dispatch(addToCart(props.detail._id))

  }

ProductInfo.js

 

import {
    LOGIN_USER,
    REGISTER_USER,
    AUTH_USER,
    LOGOUT_USER,
    ADD_TO_CART,
} 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
                }
            }
        default:
            return state;
    }
}

client/src/_reducers/user_reducer.js

 

위 코드들을 각 파일에 추가해주자.

 


#2

이 쯤 왔을 때 리덕스 한번 쯤 다시 상기해 볼 필요가 있을거같아서 한 번 더 정리해야겠다.

 

출처 : https://chanhuiseok.github.io/posts/redux-1/

 

위에서 말한 action creator는 user_acitons.js에서 만들어준 함수를 의미한다.

 

과정을 설명하면 다음과 같다.

1. ProductInfo 컴포넌트에서 Add To Cart 버튼을 누르면 clickHandler가 dispatch를 이용하여 addToCart 액션값을 넣어서 리듀서를 호출한다. (이때 매개변수는 ProductInfo의 부모 컴포넌트인 DetailProduct 컴포넌트에서 props로 줬었던 productId 이다.)

 

2. 액션인 addToCart가 매개변수로 받은 productId를 body로 넣어서 endpoint가 '/api/users/addToCart'인 route에 데이터를 요청한다. 이때 return 값은 response.data이다. 후에 이 return 값은 user_reducer의 cart값으로 들어갈 것이다. 

 

3. addToCart 라우터에선 일단 auth 미들웨어를 통해 req.user에 넣었던 유저 정보에서 유저 id를 이용하여 DB에서 해당 유저를 찾는다. 유저가 없다면 에러가 날거고, 있다면 userInfo 변수에 담긴다. 현재 userInfo는 DB에 있는 유저의 모든 정보가 담겨있다. 이때 userInfo.cart의 모든 아이템들을 탐색하여 매개변수로 받은 productId가 존재하는지 검사한다.

 

3-1. productId인 product가 userInfo.cart에 존재한다면

{$inc: {"cart.$.quantity": 1}},

 

이 문법을 이용하여 개수만 하나 올려준다. 
 
 
3-2. productd인 product가 userInfo.cart에 존재하지 않다면 
{
                        $push: {
                            cart: {
                                id: req.body.productId,
                                quantity: 1,
                                date: Date.now()
                            }
                        }
                    },
 
이 문법을 이용하여 새로운 product를 cart에 넣어준다.
 
 
3-1과 3-2에서 주의할 점은 User 모델에서 하나를 찾아서 업데이트를 해주는 것이므로 {new: true}를 넣어줘야 업데이트 된 정보값을 얻을 수 있다. 
 
그리고 클라이언트에 userInfo.cart 값을 보내준다. 이때 보내주는 값이 2번에서 말했던 response.data 값이다. 
 
 
 
4. 1번에서 dispatch를 이용하여 reducer를 호출했었다. user_reducer.js에선 ADD_TO_CART 케이스에 해당하는 부분을 처리할 것이다.
일단 기존에 있던 state들을 모두 갖고 온다. 
그리고 새로운 userData를 넣어주는데, 기존에 있던 userData를 갖고오고, cart에는 위에서 말했던 action의 payload값(addToCart 라우터로부터 받은 데이터값 UserInfo.cart)인 action.payload를 넣어준다. 
 
여기까지 했다면
 

이전 리덕스
이후 리덕스

위에서 아래 이미지로 바뀐 것을 볼 수 있다. 

 

 

 

위에서 말한 inc나 push 문법 정리 사이트는 밑에있다.

https://thalals.tistory.com/169

 

[MongoDB] 특정 값 증가 - Update 메소드와 $inc 제한자 ( 조회 수, 좋아요)

몽고디비에서 Update 메소드를 사용할 때 값을 수정하기위해 $set을 이용하는데, 이 set이 제한자이다. 제한자의 종류는 다양하고 기능 또한 다양합니다. 제한자의 구체적인 용어는 '갱신 제한자'로

thalals.tistory.com