드디어 평소에 궁금했었던 장바구니 기능을 진행해보았다.
(일단 최대한 기능 위주로 진도를 나가고자 디자인은 정말 하나도 안건드렸다;;)
먼저 홈 화면에서 상품 목록을 클릭하면 getProductDetail(item._id)를 실행시키도록 했다.
이 함수에 인자로 선택한 상품 아이디를 넣어서 실행하면 해당 아이디를 가지고 get요청을 해서 상품 상세페이지를 가져오게된다.
상세페이지에서는 사이즈를 선택해서 장바구니에 추가를 할 수 있다.
사이즈 정보를 선택하는 방법으로 라디오버튼 방식을 해보았다.
원래 드랍다운으로 하는게 일반적이나 당장은 이러한 컴포넌트 구현이 공부 주제는 아닌지라;;
최대한 간단하게 할 수 있는 라디오 방식으로 했다.
//사이즈 선택 라디오
Object . keys ( data . data . stock ). forEach (( size , index ) => {
const radioContainer = document . getElementById ( "radio-container" );
const radioGroup = `
<input type="radio" name="option" value=" ${ index } " id="option ${ index } ">
<label for="option ${ index } "> ${ size } </label>
` ;
radioContainer . insertAdjacentHTML ( "beforeend" , radioGroup );
});
//라디오버튼 동작
const radioButtons = document . querySelectorAll ( 'input[name="option"]' );
let selectedOption = "" ;
radioButtons . forEach ( radio => {
radio . addEventListener ( "change" , function () {
const errorMessage = document . getElementById ( 'error-message' );
if ( this . checked ) {
const label = document . querySelector ( `label[for=" ${this . id } "]` ). textContent ;
document . getElementById ( "selected-option" ). textContent = ` ${ label } ` ;
selectedOption = document . getElementById ( "selected-option" ). textContent ;
errorMessage . textContent = "" ;
}
});
});
아이디를 통해서 가져온 상품 정보 중 stock에 대한 오브젝트 키 값들만 forEach를 통해 html에 넣어주었다.
라디오의 경우 서로 일관되게 관리를 하기 위해서 input에는 name, 그 인풋에 대한 label에는 for라는 속성을 이용해주면 더 효과적으로 관리할 수 있었다.
라디오를 선택해서 checked를 확인 후 체크가된 엘리먼트의 라벨 텍스트값을 selectedOption이라는 변수로 저장.
그 다음은 장바구니 추가버튼을 눌렀을 때의 기능을 잡아보았다.
//장바구니에 추가 버튼 클릭 시
const addCartBTN = document . getElementById ( 'add-cart-btn' );
addCartBTN . addEventListener ( "click" , () => {
addToCart ( data . data . _id , selectedOption );
});
버튼을 누르면 상품아이디 값과 selectedOption 변수에 저장된 사이즈 값을 addToCart 함수 인자로 넣어서 실행시켜준다.
addToCart 함수는 두 인자로 받은 데이터를 이용해서 카트추가 POST를 요청하는 기능이다.
/사이즈가 비면 에러
if ( selectedOption === "" ) {
errorMessage . textContent = "사이즈를 선택해주세요." ;
return
}
errorMessage . textContent = "" ;
//로그인 안했으면 로그인 페이지로
const token = sessionStorage . getItem ( 'token' );
if ( ! token ) {
window . location . href = "/login" ;
return
}
포스트 요청 전에 두가지 확인을 하게되는데 사이즈를 선택했는지, 그리고 로그인을 하고 장바구니 추가 버튼을 누른건지 체크를 한다. 로그인을 안했으면 로그인 페이지로 보낸다.
아래는 카트 포스트 요청 코드이다.
fetch ( ` ${ URI } /api/cart` , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
"Authorization" : `Bearer ${ sessionStorage . getItem ( "token" ) } `
},
body : JSON . stringify ({
productId : productId ,
size : selectedOption ,
qty : 1
}),
})
. then (( response ) => response . json ())
. then (( data ) => {
if ( data . status === 'fail' ) {
// 실패 시 오류 메시지 표시
document . getElementById ( "error-message" ). innerHTML = `
<p> ${ data . message } </p>
` ;
} else {
console . log ( "카트 추가 완료!" );
cartCount ();
}
})
. catch (( error ) => console . error ( "Error:" , error ));
body에 productId, size, qty 값을 넣어서 요청을 보내게 된다.
백엔드에서는 cart.api를 추가하고 라우터 설정을 한다.
카트라는건 누구의 카트에 담는지가 중요하기 때문에 유저 정보가 꼭 필요하다. 따라서 authController에서 유저인증을 하는 미들웨어를 한번 거치도록 세팅되었다.
const express = require ( "express" );
const authController = require ( "../controllers/authController" );
const cartController = require ( "../controllers/cartController" );
const router = express . Router ();
router . post ( "/" , authController . athenticate , cartController . addItemToCart );
module . exports = router ;
카트 컨트롤러의 아이템 추가 기능이다.
const Cart = require ( "../models/Cart" );
const cartController = {};
//카트 추가
cartController . addItemToCart = async ( req , res ) => {
try {
const { userId } = req ;
const { productId , size , qty } = req . body ;
//유저를 통해 카트 찾기
let cart = await Cart . findOne ({ userId : userId });
//유저가 만든 카트가 없다? 새로 만들어주기
if ( ! cart ) {
cart = new Cart ({ userId });
await cart . save ();
}
//이미 카트에 있는 아이템인지 검사
const existItem = cart . items . find (( item ) =>
item . productId . equals ( productId ) //equals: mongoose ObjectId type을 비교할 때
&& item . size === size );
if ( existItem ) {
throw new Error ( "이미 아이템이 카트에 있습니다." )
}
//카트에 아이템 추가
cart . items = [ ... cart . items , { productId , size , qty }];
await cart . save ();
res . status ( 200 ). json ({ status : "ok" , data : cart , cartItemQty : cart . items . length });
} catch ( error ) {
res . status ( 400 ). json ({ status : "fail" , message : error . message });
}
};
authController 미들웨어 req.userId에서 userId를 가져온다.
그리고 아까 요청 정보의 body로부터 productId, size, qty를 가져온다.
이제 cart를 만들면 되는데 여기서도 두가지 체크를 하고 진행을 하게 되는데
먼저 기존 카트에서 userId를 통해서 기존 유저가 있는지 확인.
만약 기존에 유저 정보가 없으면 그 유저에 대한 cart를 하나 생성해준다.
다음으로는 같은 상품을 중복으로 카트에 담지 않기 위한 체크를 진행하는데
const existItem = cart . items . find (( item ) =>
item . productId . equals ( productId ) //equals: mongoose ObjectId type을 비교할 때
&& item . size === size );
이런식으로 검사를 하게된다. 여기서 특이한 부분은 equals라는 기능을 사용하는 부분인데
const cartSchema = Schema ({
userId : { type : mongoose . ObjectId , ref : User },
items : [{
productId : { type : mongoose . ObjectId , ref : Product },
size : { type : String , required : true },
qty : { type : Number , default : 1 , required : true }
}]
}, { timestamps : true });
카트 모델 스키마를 살펴보면 productId의 경우 타입이 mongoose.ObjectId라는 독특한 타입이다. (외래키를 가져오기 위한 방식)
이 타입의 경우 정확한 비교를 하려면 equals를 써야한다. size의 경우는 string이므로 === 을 사용해서 체크.
조건들에 부합하면 카트에 담는 코드를 진행하게 된다.
cart . items = [ ... cart . items , { productId , size , qty }];
await cart . save ();
res . status ( 200 ). json ({ status : "ok" , data : cart , cartItemQty : cart . items . length });
cart.items는 한 유저로 묶인 카트라는 컬렉션에 items라는 배열로 되어있고 그 안에 각 값을 객체 형태로 가진다. 카트의 경우 덮어쓰는 정보가 아니라 기존 카트 목록에 계속 추가를 할 수 있기 때문에 위 코드처럼 cart.items = [...cart.items, {productId, size, qty}] 방식으로 추가를 해준다. 이렇게하면 기존 배열 목록에 추가로 객체를 넣어줄 수 있다.
추가 성공시 응답 정보로는 cartItemQty: cart.item.length 라는 정보를 함께 넣어주어서 현재 카트에 담긴 아이템 갯수를 같이 응답해준다. 그러면 프론트엔드에서 장바구니에 담긴 상품 갯수를 정확히 받을 수 있어서 장바구니 버튼 옆에 숫자도 표시해 줄 수 있다.