이번 쇼핑몰 프로젝트에서 ,
header 부분에서 장바구니 목록을 보고,
상품내용페이지에서 상품을 장바구니에 넣고,
장바구니에 담긴 상품을 삭제하고,
담긴 상품의 총액이 변경되도록 하는 장바구니 부분을
비동기로 구현했다.
이번 프로젝트에서 가장 어려웠던 부분을 꼽자면 단연 이 비동기 장바구니 부분이라고 생각한다..
구현할 때는 왜 이렇게 어려울까, 내가 아직 실력이 많이 부족하구나 등등 고민을 많이 하며
시간을 타 로직보다 많이 들여 구현을 했는데 ,
나중에 검색해보니 쇼핑몰 프로젝트에서 어렵다고 꼽히는 로직이 장바구니라고 들었다...
그래도 혼자 힘으로, 내 머리속에 그려진 로직대로 설계해서,
성공적으로 구현을 완료하고나니,
뿌듯하기도 하고, 실력 항샹에 도움이 많이 됐다. ( 특히 js부분..)
로직은
비로그인 ( 세션x ) -
로그인이 되어있지 않을때에는 쿠키를 이용한다.
쿠키는 ADD TO CART 버튼을 누를 때 생성된다.
- if(로그인이 되어있지 않고 쿠키가 없을 때 )
비회원에 , 장바구니 버튼을 처음눌렀을 때는 쿠키를 생성해주고,
쿠키의 value값을 기준으로 DB에 장바구니테이블에 상품정보를 insert한다.
- if(로그인이 되어있지 않고 쿠키가 있을 때 )
비회원에, 장바구니 버튼을 이미 눌러서 쿠키가 있을 때에는
장바구니 테이블에 상품정보를 insert하고 쿠키의 제한시간을 다시 set해준다.
- if(회원일때)
로그인을 했을 때에는, 회원PR을 기준으로 DB의 장바구니 테이블에 저장하고
비로그인시에 담긴 장바구니의 상품을 로그인했을때 회원의 장바구니로 이동되도록 구현했다.
cartVO
public class CartVO
/* */ {
/* */ private int cart_no; //장바구니PR
/* */ private int cart_mem_no; //회원PR
/* */ private int cart_item_no; //상품PR
/* */ private Date cart_cklimit; //쿠키제한시간(삭제용)
/* */ private String cart_ckid; //쿠키value값
/* */ private String cart_option_content; //옵션내용
/* */ private int cart_option_no; //옵션PR
/* */ private ItemVO itemVO; // join용
private OptionVO optionVO; // join용
//getter & setter
}
테이블은 위와 동일하며, 생성시간을 기록할 cart_date가 default 값 sysdate로 들어가있다.
Mapper
<!-- 장바구니 등록 -->
<insert id="cartInsert">
INSERT INTO CART (CART_NO , CART_ITEM_NO
, CART_CKLIMIT , CART_OPTION_CONTENT , CART_OPTION_NO,
<if test="cart_mem_no == 0">
CART_CKID
</if>
<if test="cart_mem_no != 0">
CART_MEM_NO
</if>
)VALUES(
(SELECT
NVL(MAX(CART_NO), 0) + 1 FROM CART) ,
#{cart_item_no} ,SYSDATE , #{cart_option_content} , #{cart_option_no} ,
<if test="cart_mem_no == 0">
#{cart_ckid}
</if>
<if test="cart_mem_no != 0">
${cart_mem_no}
</if>
)
</insert>
<!-- 로그인시 비회원장바구니 -> 회원장바구니 -->
<update id="cartUpdate">
UPDATE CART SET cart_mem_no = #{mem_no} WHERE CART_CKID = #{cart_ckid}
</update>
장바구니 테이블은 ckID라는 쿠키의 정보가 기록될 컬럼과 mem_no라는 회원의 PR이 기록될 컬럼이 있는데,
세션이 null ( 비회원 ) 이라면 ck_id를 insert하고,
세션이 != null ( 회원 ) 이라면 mem_no를 insert하도록 동적쿼리를 작성했다.
또한, 로그인시 비로그인상태에서 담았던 장바구니의 물품을 쿠키의 value값으로 where문을 줘서
장바구니 테이블의 해당 값에 mem_no를 update시킨다.
한마디로 비회원 상태일때는 mem_no 부분이 null 이고, 로그인을 할때 mem_no가 채워지도록 설계해서
장바구니에 담긴 상품을 따로 delete나 insert하지않고 update문으로 한번에 할 수 있도록 구현했다.
Controller
//장바구니
@ResponseBody
@RequestMapping(value = {"/cart"}, method = {RequestMethod.POST})
public int cart(HttpSession session, HttpServletRequest request, HttpServletResponse response, CartVO cartVO) throws Exception {
logger.info("itemno=" + cartVO.getCart_item_no());
Cookie cookie = WebUtils.getCookie(request, "cartCookie");
//비회원장바구니 첫 클릭시 쿠키생성
if (cookie == null && session.getAttribute("member") == null) {
String ckid = RandomStringUtils.random(6, true, true);
Cookie cartCookie = new Cookie("cartCookie", ckid);
cartCookie.setPath("/");
cartCookie.setMaxAge(60 * 60 * 24 * 1);
response.addCookie(cartCookie);
cartVO.setCart_ckid(ckid);
this.mainService.cartInsert(cartVO);
//비회원 장바구니 쿠키생성 후 상품추가
} else if (cookie != null && session.getAttribute("member") == null) {
String ckValue = cookie.getValue();
cartVO.setCart_ckid(ckValue);
//장바구니 중복제한
if(mainService.cartCheck(cartVO) != 0) {
return 2;
}
//쿠키 시간 재설정해주기
cookie.setPath("/");
cookie.setMaxAge(60 * 60 * 24 * 1);
response.addCookie(cookie);
mainService.cartInsert(cartVO);
//회원 장바구니 상품추가
} else if(session.getAttribute("member") != null){
MemberVO memberVO = (MemberVO) session.getAttribute("member");
cartVO.setCart_mem_no(memberVO.getMEM_NO());
if(mainService.cartMemCheck(cartVO) != 0) {
return 2;
}
mainService.cartInsert(cartVO);
}
return 1;
}
비동기 처리를 보내기위해 @ResponseBody어노테이션을 붙여줬다.
리턴타입은 int로 줬다.
insert로직에 대한 리턴이기 때문에, ajax의 success에서 받는 리턴값을 String이나 타 값으로 줘도 되겠지만,
간단하게 장바구니 추가가 완료됐다면 1, 이미있는 상품이라면 2, 그 외에는 3
이런식으로 처리하기 위해 int로 리턴해줬다.
이런 방식이 현업 웹개발에서 통용되는 방법인지는 모르겠지만,
그저 내가 ajax를 사용하며 공부하다보니 이렇게 하는게 간단하고 편하다고 느껴져서 이렇게 구현했다.
로직은 위에 설명한 mapper의 설명과 같다 ( 당연히.. )
위에서부터 아래로 차례대로 설명해보자면
먼저 매개변수는 세션을 확인하기위한 Http Session,
쿠키를 확인하고, 반환하기 위한 HttpServletReqeust 와 HttpServeltResponse
그리고 장바구니에 입력될 상품과 선택한 옵션을 받기위한 cartVO다.
먼저 메서드의 시작은 WebUtils를 사용해 요청값에서 "cartCookie" 라는 key값의 쿠키를 가져온다.
그 다음 if문으로
- 비회원이 처음 장바구니 추가를 눌렀을때 ( 쿠키생성 )
- 비회원이 쿠키가 있는 상태로 장바구니 추가를 눌렀을때
- 회원이 장바구니 추가를 눌렀을 때
의 3가지 상황을 나눠주었다.
//비회원장바구니 첫 클릭시 쿠키생성
if (cookie == null && session.getAttribute("member") == null) {
String ckid = RandomStringUtils.random(6, true, true);
Cookie cartCookie = new Cookie("cartCookie", ckid);
cartCookie.setPath("/");
cartCookie.setMaxAge(60 * 60 * 24 * 1);
response.addCookie(cartCookie);
cartVO.setCart_ckid(ckid);
this.mainService.cartInsert(cartVO);
cookie == null , 아까 요청값에서 가져온 "cartCookie" 라는 key의 쿠키가 없을때
AND
session.getAttribute("member") == null ,
HttpSession으로 세션값을 가져오는데, "member"라는 key값의 세션이 없을떄
( 로그인 시 "member"라는 세션key로 세션이 부여되게 구현했음)
-------
JAVA의 RandomStringUtils 를 사용해서 6자리값의 난수값을 ckid 를 생성해준다.
new Cookie를 이용해 "cartCookie"를 생성해준다.
제한시간은 1일로 생성해줬다.
그 후 생성한 쿠키를 response에 담아서 브라우저로 보내준다.
그 다음 cartVO에 ckid를 담아서, 장바구니 insert 로직으로 보내준다.
이때!
cartVO에 mem_no는 null 상태이다.
mem_no는 세션에서 가져오는 회원의 PR인데 ,
해당 로직은 session이 null일때 들어오는 if문 내 이기 떄문이다.
그렇기 때문에, mapper에서 동적쿼리로 나눠준 mem_no = 0일때 의 로직을 타게되어
정상적으로 insert가 된다.
그 다음 부분은 쿠키가 생성되었을 때다.
쿠키를 생성해 줄 필요없이 , 쿠키의 value값을 ckid에 담아서 insert 해준다.
쿠키 제한시간도 1일로 다시 설정해준다.
다시 설정해주지 않으면 23시간 전에 추가한 장바구니는 상품을 추가해도 1시간 후에 사라져버리고,
이는 사용자의 깊은 분노와 이어져 쇼핑몰에서 물건을 사지않는 결과로 충분히 나올 수 있을 것이다..ㅜㅜ
그리고, 장바구니에 중복으로 물건이 담기지 않도록
<select id="cartCheck" resultType="int">
SELECT COUNT(*) FROM CART WHERE CART_ckid = #{cart_ckid}
AND CART_OPTION_NO = #{cart_option_no}
</select>
유효성 검사를 해서 막아주도록 하자.
이때 , 유효성 검사에 걸리면 숫자 2를 return하는데 이것은 아래 ajax 로직에서 설명하겠다.
service와 dao 부분은 컨트롤러의 부분과 같이 파라미터를 전달해줄 뿐이라서,
설명은 생략하도록 하겠다.
@Transactional
public void cartInsert(CartVO cartVO) throws Exception {
this.mainDAO.cartInsert(cartVO);
}
이제 로그인 상태로 장바구니를 추가할 때 이다.
해당 로직은 위에서 설명했듯, 동적쿼리를 이용해 세션이 있을때
즉,회원의 PR인 mem_no 가 있을때 ckid대신 mem_no를 insert 할 뿐이다.
하지만 회원상태에선 추가되야 할 로직이 있다.
바로 비회원 상태에서 장바구니에 담은 물품을 로그인 했을때, 회원의 장바구니에 담아주는 것이다.
// 로그인 POST
@RequestMapping(value = "/Login", method = RequestMethod.POST)
public String login(MemberVO vo, HttpSession session, HttpServletRequest request, HttpServletResponse response,
RedirectAttributes rttr, Model model) throws Exception {
MemberVO login = service.login(vo);
boolean pwdMatch = pwdEncoder.matches(vo.getMEM_PW(), login.getMEM_PW());
// 비회원 장바구니 회원장바구니로 이동
Cookie cookie = WebUtils.getCookie(request, "cartCookie");
if (cookie != null) {
String ckValue = cookie.getValue();
logger.info("비회원장바구니 삭제");
//쿠키에 담긴 정보에 회원NO 입력
mainService.cartUpdate(login.getMEM_NO(), ckValue);
//쿠키삭제
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
return "/main/index";
}
위의 로직은 로그인부분의 Controller 부분이다.
필요없는 부분은 전부 삭제하고, 장바구니 부분만 남겼다.
간단하다. 로그인 로직 사이에 아이디와 비밀번호 확인을 마치고,
세션을 부여해서 사용자에게 로그인을 시켜주는 부분에
cartCookie가 존재하는지 확인 후, 존재한다면
<!-- 로그인시 비회원장바구니 -> 회원장바구니 -->
<update id="cartUpdate">
UPDATE CART SET cart_mem_no = #{mem_no} WHERE CART_CKID = #{cart_ckid}
</update>
장바구니의 mem_no 컬럼에 세션값을 넣어주면 된다.
위에서 말했지만, 비회원 장바구니 추가시엔 mem_no 컬럼이 null로 들어가있다.
그 부분에 mem_no를 넣어줘서, 다른 로직없이 update 한번으로 완성시키는 것이다.
비회원 조회시에는 ckid로 조회를 하고,
회원 조회시에는 mem_no로 조회를 하도록 조회 부분을 구성하면
이런 방식을 사용해도 문제가 없었다.
View
<div class="row mb-20">
<div class="col-sm-12 mb-sm-20 mb-2">
<select class="form-control input-lg" name="item_optiont"
id="item_optiont">
<c:forEach items="${itemOption}" var="itemOption">
<c:if test="${itemOption.option_vol > 0}">
<option value="${itemOption.option_no}">${itemOption.option_content}
<c:if test="${itemOption.option_vol == 1}"> - 마지막 상품</c:if>
</option>
</c:if>
<c:if test="${itemOption.option_vol < 1}">
<option value="${itemOption.option_no}" disabled>${itemOption.option_content}
- 품절</option>
</c:if>
</c:forEach>
</select> <br />
</div>
<div class="col-sm-8">
<a class="btn btn-lg btn-block btn-round btn-b" id="cartInsert">
Add To Cart</a>
</div>
장바구니 추가는 상품내용을 보여주는 페이지에서 가능하다.
상품내용 페이지의 장바구니 추가 버튼과 옵션 부분이다.
장바구니 추가시엔 상품의 no와 옵션no, 옵션 내용이 필요하다.
(사실 no만 있어도 되지만 조회부분의 조인을 덜 쓰기위해 다 가져오기로 했다..)
var item_no = ${itemContent.item_no};
$(document)
.ready(
function() {
var target = document.getElementById("item_optiont");
$("#cartInsert").on("click",function() {
var item_optionValue = target.options[target.selectedIndex].value;
var item_optionContent = target.options[target.selectedIndex].text;
$
.ajax({
type : "POST",
url : "/main/cart",
dataType : "json",
data : {
'cart_option_no' : item_optionValue,
'cart_option_content' : item_optionContent,
'cart_item_no' : item_no
},
error : function(
request,
status, error) {
alert("code:"
+ request.status
+ "\n"
+ "message:"
+ request.responseText
+ "\n"
+ "error:"
+ error);
},
success : function(data) {
if (data == 1) {
cartHeaderView();
toastr.options.preventDuplicates = true;
toastr
.success("장바구니 추가완료");
} else if (data == 2) {
toastr.options.preventDuplicates = true;
toastr
.warning("이미 추가 된 상품입니다");
}
}
});
})
장바구니 버튼을 눌렀을때 , onclike 이벤트로 해당 script 함수에 들어온다.
target.option을 사용해서 select option의 text값과 value값 ( 옵션 Content와 옵션 no ) 을 가져온다.
그리고 상품 내용페이지를 구성하는 select문의 결과에서 상품no를 가져온다.
그 후 ajax로 3개의 데이터를 컨트롤러로 보내준다.
컨트롤러에서 반환되는 값이 1이라면 추가완료,
2라면 중복이다.
여기까지 , 비동기 장바구니의 장바구니 추가 ( insert ) 로직이다.
사실 이 부분이 힘들었던 이유는 부족한 프론트 ( js , jquery ) 실력 때문이였던 것 같다.
백쪽을 구상하는데는 그리 오래 걸리지 않았지만 select option에서 값을 가져오고,
깔끔하게 장바구니 부분을 만드는 것이 시간의 최소 반 이상을 차지했던 것 같다.
그래도 이 부분을 만들며 프론트 스킬이 많이 늘었다.
역시 빡세게 배워야 실력이 늘어나는 것 같다..ㅜ
다음 글에선 장바구니 조회와 삭제를 다뤄보려고 한다.
'SPRING > Homme Shop' 카테고리의 다른 글
[스프링] 쇼핑몰 - 상품 주문 [ 2 ] (0) | 2021.12.09 |
---|---|
[스프링] 쇼핑몰 - 상품 주문 [ 1 ] (0) | 2021.12.08 |
[스프링] 쇼핑몰 - 비동기 장바구니 [3] (0) | 2021.12.08 |
[스프링] 쇼핑몰 - 비동기 장바구니 [2] (1) | 2021.12.08 |
[스프링]쇼핑몰 - 상품 등록 (0) | 2021.12.07 |