10/20/2018

스프링 쇼핑몰 만들기 #21. 카트 담기, 카트 리스트 기능 구현

스프링 쇼핑몰 만들기
- 깃허브 링크

상품이 마음에 들면 카트에 담아둔뒤 잠시 고민하는척(!) 하다가 구입하게됩니다.

굳이 담아두지 않고 바로 구입하는 방법도 있지만, 2개 이상의 상품을 구입하게될 때면 바로바로 구입하는것보다 카트에 담아놓고 한번에 구입하는게 사용자나 관리자나 여러모로 편하죠.

먼저 카트 테이블을 생성합니다.

create table tbl_cart (
    cartNum     number          not null,
    userId      varchar2(50)    not null,
    gdsNum      number          not null,
    cartStock   number          not null,
    addDate     date            default sysdate,
    primary key(cartNum, userId) 
);

카트 번호(cartNum)은 값을 구분하는 고유 번호, 카트 수량(cartStock)은 상품별 담은 갯수입니다. 상품 번호(gdsNum)와 아이디(userId)는 다른 테이블에서 참조하는 컬럼입니다.

create sequence tbl_cart_seq;

카트 번호를 생성할 시퀀스입니다.

카트 번호는 시퀀스만 사용하는것보다 날짜와 조합하여 사용하는것이 보다 좋습니다.

alter table tbl_cart
    add constraint tbl_cart_userId foreign key(userId)
    references tbl_member(userId);

alter table tbl_cart
    add constraint tbl_cart_gdsNum foreign key(gdsNum)
    references tbl_goods(gdsNum);

카트 테이블과 맴버 테이블, 상품 테이블을 참조하는 쿼리입니다.

테이블을 생성하였다면, 카트 담기 코드를 테스트해봅니다.

테스트를 끝낸 후 꼭 commit; 을 해주도록합니다.

새로운 테이블을 생성했으니, 테이블을 기반으로한 VO를 생성합니다.

테스트했던 쿼리를 매퍼에 추가합니다.

<!-- 카트 담기 -->
<insert id="addCart">
 insert into tbl_cart (cartNum, userId, gdsNum, cartStock)
     values (tbl_cart_seq.nextval, #{userId}, #{gdsNum}, #{cartStock})
</insert>

DAO와 Service를 추가합니다.

컨트롤러에도 카트 담기용 메서드를 추가합니다.

// 카트 담기
@ResponseBody
@RequestMapping(value = "/view/addCart", method = RequestMethod.POST)
public void addCart(CartListVO cart, HttpSession session) throws Exception {
 
 MemberVO member = (MemberVO)session.getAttribute("member");
 cart.setUserId(member.getUserId());

 service.addCart(cart);
 
}

첫부분의 어노테이션 @ResponseBody 를 보면 알 수 있듯, 에이젝스(Ajax)를 사용하겠습니다.

view.jsp의 카트에 담기 버튼에 클래스 addCart_btn을 추가하고, 바로 아래에 스크립트를 추가합니다.

<p class="addToCart">
 <button type="button" class="addCart_btn">카트에 담기</button>
 
 <script>
  $(".addCart_btn").click(function(){
   var gdsNum = $("#gdsNum").val();
   var cartStock = $(".numBox").val();
      
   var data = {
     gdsNum : gdsNum,
     cartStock : cartStock
     };
   
   $.ajax({
    url : "/shop/view/addCart",
    type : "post",
    data : data,
    success : function(result){
     alert("카트 담기 성공");
     $(".numBox").val("1");
    },
    error : function(){
     alert("카트 담기 실패");
    }
   });
  });
 </script>
</p>

CartVO형태의 제이슨(Json) 데이터를 만들어서 에이젝스를 이용해 지정한 URL로 보낸 뒤, 전송에 성공하면 success 함수를 실행, 실패하면 error 함수를 실행합니다.

실패할 경우는 세션이 다 되어서 로그인이 자동으로 풀렸거나, 로그인을 안한 사용자입니다.

수량을 적절히 조정하여 카트에 담기 버튼을 클릭하면, 이렇게 메시지가 출력됩니다.

로그인하지 않은 사용자에게 로그인하라는 메시지를 보낼 수 있도록, 컨트롤러를 약간 수정했습니다.

// 카트 담기
@ResponseBody
@RequestMapping(value = "/view/addCart", method = RequestMethod.POST)
public int addCart(CartListVO cart, HttpSession session) throws Exception {
 
 int result = 0;
 
 MemberVO member = (MemberVO)session.getAttribute("member");
 
 if(member != null) {
  cart.setUserId(member.getUserId());
  service.addCart(cart);
  result = 1;
 }
 
 return result;
}

result값을 이용해 사용자가 로그인되었는지 안되었는지 확인할 수 있습니다.

에이젝스의 success 함수도 살짝 수정합니다.

<script>
 $(".addCart_btn").click(function(){
  var gdsNum = $("#gdsNum").val();
  var cartStock = $(".numBox").val();
           
  var data = {
    gdsNum : gdsNum,
    cartStock : cartStock
    };
  
  $.ajax({
   url : "/shop/view/addCart",
   type : "post",
   data : data,
   success : function(result){
    
    if(result == 1) {
     alert("카트 담기 성공");
     $(".numBox").val("1");
    } else {
     alert("회원만 사용할 수 있습니다.")
     $(".numBox").val("1");
    }
   },
   error : function(){
    alert("카트 담기 실패");
   }
  });
 });
</script>

컨트롤러에서 반환받는 result값에 의해 로그인이 안된 사용자에겐 다른 메시지를 출력합니다.

로그인하지 않은 상태에서 카트에 담기 버튼을 클릭하면 의도한대로 다른 메시지가 출력됩니다.

다음은 카트 리스트입니다. 카트 리스트는 사용자의 아이디(userId)로 구분할 수 있습니다. 이때 카트 테이블만 사용하게되면 상품 정보를 가져올 수 없으므로, 상품 테이블과 조인하여 상품 정보를 가져왔습니다.

또 처음보는 row_number() over(order by c.cartNum desc) as num 가 있는데, 이는 출력된 결과에 순서를 표시하는 기능이라고 생각하시면 됩니다.

:

CartVO를 복사/붙여넣기 한뒤 CartListVO로 이름을 변경하고, 새로운 요소를 추가합니다.

매퍼에 쿼리를 추가합니다.

<!-- 카트 리스트 -->
<select id="cartList" resultType="com.kubg.domain.CartListVO">
 select
     row_number() over(order by c.cartNum desc) as num,
     c.cartNum, c.userId, c.gdsNum, c.cartStock, c.addDate,
     g.gdsName, g.gdsPrice, g.gdsThumbImg
 from tbl_cart c
     inner join tbl_goods g
         on c.gdsNum = g.gdsNum   
     where c.userId = #{userId}
</select>

DAO와 Service를 작성하고

컨트롤러에 카트 목록용 메서드를 추가합니다.

// 카트 목록
@RequestMapping(value = "/cartList", method = RequestMethod.GET)
public void getCartList(HttpSession session, Model model) throws Exception {
 logger.info("get cart list");
 
 MemberVO member = (MemberVO)session.getAttribute("member");
 String userId = member.getUserId();
 
 List<CartListVO> cartList = service.cartList(userId);
 
 model.addAttribute("cartList", cartList);
 
}

list.jsp를 복사/붙여넣기 한뒤 이름을 cartList.jsp로 변경한 뒤 <section id="content"> ~ </section> 의 코드를 수정합니다.

<section id="content">
 
 <ul>
  <c:forEach items="${cartList}" var="cartList">
  <li>
   <div class="thumb">
    <img src="${cartList.gdsThumbImg}" />
   </div>
   <div class="gdsInfo">
    <p>
     <span>상품명 : </span>${cartList.gdsName}<br />
     <span>개당 가격 : </span><fmt:formatNumber pattern="###,###,###" value="${cartList.gdsPrice}" /><br />
     <span>구입 수량 : </span>${cartList.cartStock}<br />
     <span>최종 가격 : </span><fmt:formatNumber pattern="###,###,###" value="${cartList.gdsPrice * cartList.cartStock}" />
    </p>    
   </div>
   
   
  </li>
  </c:forEach>
 </ul>
</section>

nav.jsp에 카트 리스트 페이지로 이동할 수 있는 링크를 추가합니다.

실행해보면, 상품 목록(list.jsp)과 비슷한 형태로 출력됩니다. 스타일을 수정해야할 필요가 있고, 카트 목록중 특정 상품을 지울 버튼도 필요하겠네요.

가격에 '원'을 붙이거나 수량에 '개'를 붙이거나하는 자잘한 수정사항과, 버튼을 하나 추가했습니다.

<section id="content">
 <ul>
  <c:forEach items="${cartList}" var="cartList">
  <li>
   <div class="thumb">
    <img src="${cartList.gdsThumbImg}" />
   </div>
   <div class="gdsInfo">
    <p>
     <span>상품명</span>${cartList.gdsName}<br />
     <span>개당 가격</span><fmt:formatNumber pattern="###,###,###" value="${cartList.gdsPrice}" /> 원<br />
     <span>구입 수량</span>${cartList.cartStock} 개<br />
     <span>최종 가격</span><fmt:formatNumber pattern="###,###,###" value="${cartList.gdsPrice * cartList.cartStock}" /> 원
    </p>
    
    <div class="delete">
     <button type="button" class="delete_btn">삭제</button>
    </div>
   </div>   
  </li>
  </c:forEach>
 </ul>
</section>

기존에 사용하던 스타일에 주석을 걸고, 새로운 스타일을 추가합니다.

<style>
 /*
 section#content ul li { display:inline-block; margin:10px; }
 section#content div.goodsThumb img { width:200px; height:200px; }
 section#content div.goodsName { padding:10px 0; text-align:center; }
 section#content div.goodsName a { color:#000; }
 */
 section#content ul li { margin:10px 0; }
 section#content ul li img { width:250px; height:250px; }
 section#content ul li::after { content:""; display:block; clear:both; }
 section#content div.thumb { float:left; width:250px; }
 section#content div.gdsInfo { float:right; width:calc(100% - 270px); }
 section#content div.gdsInfo { font-size:20px; line-height:2; }
 section#content div.gdsInfo span { display:inline-block; width:100px; font-weight:bold; margin-right:10px; }
 section#content div.gdsInfo .delete { text-align:right; }
 section#content div.gdsInfo .delete button { font-size:22px;
            padding:5px 10px; border:1px solid #eee; background:#eee;}
 
</style>

상품 목록과 다르게 한줄에 한개의 상품만 있으며, 상품에 필요한 간단한 정보와 삭제 버튼이 보입니다.

게시물 수정
  1. shop컨트롤러에서 카트목록부분에 value = /view/cartList 로 잡으니 404 오류가 나고 /view/cartList에서 view를 빼니까 제대로 작동이 하는데 경로가 잘못된거겠죠?

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      4040에러는 URL이나 팔일 경로가 잘못되었을 때 생기는 에러입니다.
      view를 빼면 제대로 동작한다면, 컨트롤러의 매핑이 view로 되어있는걸로 추측이 됩니다.

      삭제
  2. script 안에 .numBox라는건 css 클래스를 뜻하는건가요?

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      var cartStock = $(".numBox").val(); 이 부분을 말씀하신거라면..
      정확히 말하자면 '선택자'이고, html태그의 클래스에 기반한게 맞습니다.
      (그러므로 말씀하신 css 클래스와 같은 의미이긴합니다)

      html 태그의 클래스를 이용하여 css에서 선택자로 사용하는것이고
      제이쿼리에서 이 선택자를 같은 방식으로 이용합니다.

      삭제
  3. Error creating bean with name 'ShopDAO': Unsatisfied dependency expressed through field 'sql'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.apache.ibatis.session.SqlSession' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@javax.inject.Inject()}

    이건 어떻게 해결하나요?

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      해당 문제에서 가장 의심되는건 ShopDAO@Repository 어노테이션이 빠진걸로 추측됩니다.

      삭제
  4. 그외래키를 해제하고 카트담기를 누르면 이상하게 상품번호 값 gdsnum 값이겠네요 이게 0으로만 가져오게되는데 에이잭스 내부에서 뭔가 이 번호를 못가져오는거같은데 이런경우는 어떤게 문제인가요 ㅠㅠ?

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      에이젝스에 문제가 있다기보다는, 필요한 데이터(gdsnum)을 전송하는 데이터에 넣지 않은걸로 추측됩니다.

      삭제
  5. 익명1/08/2021

    카트 리스트에서 404 에러 나시는 분들은 컨트롤러에서 링크 수정하시면 돼요.
    이미지엔 /view/cartList로 되어있는데, 실제 코드에선 /cartList로 되어있어요.

    // 카트 목록
    @RequestMapping(value = "/cartList", method = RequestMethod.GET)

    답글삭제
  6. 안녕하세요~ 궁금한게 있는데, 보통 쇼핑몰에서 사용자가 최근 본 목록을 구현하려하면, DB에 저장하지않고 사용하는 쿠키를 쓰나요? 아니면 DB에 최근목록 테이블을 만들어서 넣었다가 로그이시 뿌려주는 형식으로 하나요?

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      db로도 할 수 있고, 쿠키나 로컬스토리지등으로도 가능합니다. 이건 구현하는 사람의 자유입니다.

      삭제
  7. 안녕하세요 선생님
    상품에 옵션이 있을때 조인에 대해서 궁금한게 있어요

    상품1과 상품2에 옵션이 3개씩 있다고 했을때 inner join하면

    상품1 옵션1
    상품1 옵션2
    상품1 옵션3

    상품2 옵션1
    상품2 옵션2
    상품2 옵션3
    이렇게 되는데

    이걸 리스트로 받아서 jsp에서 보여줄땐 어떤식으로 해야되나요??


    답글삭제
    답글
    1. 추가.....

      상품목록을 보면

      상품 1 옵션1
      ........옵션2
      ........옵션3

      상품 2 옵션1
      ........옵션2
      ........옵션3

      이런식으로 보여주고싶은데 잘안되네요 ...

      삭제
    2. 안녕하세요? 방문해주셔서 감사합니다.

      말씀하시는 방법은 카테고리에 대한 데이터를 모두 가져와서, 상위 카테고리를 출력하는 반복문 내부에 하위 카테고리를 출력하는 반복문을 넣어서 처리할 수 있습니다.

      삭제
    3. 감사합니다 ㅎㅎ

      삭제
  8. 카트담기할 때 한개의 아이디로 로그인해서 카트담기는 잘 되는데 다른 아이디로 로그인해서 카트담기하면

    Cannot add or update a child row: a foreign key constraint fails (`webook`.`tbl_cart`, CONSTRAINT `tbl_cart_gdsNum` FOREIGN KEY (`gdsNum`) REFERENCES `tbl_goods` (`gdsNum`))

    이 에러가뜹니다. 어떻게 해야할까요 ㅜㅜ

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      이 경우 참조하는 테이블의 값이 없는 경우 생기는 에러로 보입니다.
      카트담기를 할 때 로직에 따라 값이 제대로 들어가는지 확인해야할 것 같습니다.

      삭제
  9. 안녕하세요 포스팅 너무 잘 보고있습니다.. 혹시 Cart 테이블과 Cartlist 테이블을 따로 설정하신 이유가 있으신가요? 잘 이해가 안돼서요

    답글삭제
    답글
    1. 제가 잘못 말씀드렸습니다 cartvo와 cartlistvo를 따로 설정하신 이유가 궁금합니다.

      삭제
    2. 안녕하세요? 방문해주셔서 감사합니다.

      CartListVO는 CartVO의 데이터를 그대로 사용하면서, CartVO에 없는 데이터까지 다루기 위해 만든 VO입니다.
      상속을 받아서 사용해도 되고, 아예 CartVO 하나만 사용하셔도 문제가 없습니다.


      1. 그렇다면 CartVO는 상속받으면 되지 않은가
      상속받고 추가할 데이터들만 작성하시면 되는데, 전 상속을 싫어합니다(?)


      2. 애초에 CartVO를 CartListVO처럼 만들면 되지 않나
      이것도 가능합니다. 다만 햇갈리지 않기 위해 분리해서 다뤘다고 보시면 됩니다.
      (쭉 보시면 아시겠지만, 대체적으로 VO 1개당 단일 역할을 하고 있습니다)

      삭제
  10. 익명7/14/2022

    감사합니다 참고 잘 하고 있습니다! 다름이 아니라 스크립트에서 .numBox 는 작성하신 코드의 jsp에서 어떤 선택자를 가져오는걸까요?

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      선택자 .numBox는 구입할 수량을 가르키고 있습니다 (#16. 상품 조회 구현)

      삭제
  11. 익명6/15/2023

    안녕하세요 지금 댓글 달아도 되는지 모르겠지만 궁금한게 있어서 질문드려요! 서버에서 카트 목록을 못 가져 오면 어디를 살펴봐야할까요? 따로 아이디를 입력하고 카트목록을 junit테스트를 하면 올바르게 잘 뜹니다ㅜ 상품페이지에서 담고 장바구니 뷰페이지에 목록을 띄워야하는데 데이터가 안들어가서 질문드립ㄴ다ㅜㅜ

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      카트 목록을 못 가져올때는.. 가장 먼저 에러가 발생한건지부터 확인해봐야합니다.
      db에 데이터가 제대로 있고, 프론트(jsp) -> 컨트롤러 -> 서비스 -> dao -> 디비까지, 그리고 그 역순으로 데이터가 오가는지 보셔야하고(db랑 컨트롤러, 프론트 정도만 보시면 됩니다) 확인은 디버그 모드나 로거, 또는 출력문을 이용해서 확인하시면 되겠습니다.

      삭제