[8] Cart

Veloger·2023년 1월 10일
0

1. 장바구니 담기

상품 상세 페이지에서 장바구니에 담을 수량 선택 후
장바구니 담기 버튼 클릭 시, 상품이 장바구니에 담기는 기능 구현해보자

Index

  1. 장바구니에 담을 상품 정보를 전달할 DTO 생성
  2. 장바구니 상품을 생성하는 메소드 추가
  3. 장바구니 찾는 쿼리문
  4. 장바구니 상품을 찾는 쿼리문
  5. 서비스에서 상품을 장바구니에 넣는 로직 작성
  6. 장바구니 컨트롤러 매핑
  7. 장바구니 담는 테스트 코드 작성
  8. JS에 장바구니 담기 버튼 기능 작성

✨ 상품 상세 페이지에서 장바구니에 담을 상품 아이디수량을 전달 받을 DTO 생성

-> CartItemDto

@Getter @Setter
public class CartItemDto {

    @NotNull(message = "상품 아이디는 필수 입력 값 입니다.")
    private Long itemId;

    @Min(value = 1, message = "최소 1개 이상 담아주세요")
    private int count;
}

✨ 처음 장바구니에 상품 추가 시, 해당 회원 장바구니 생성 로직 추가

-> Cart

    public static Cart createCart(Member member){
        Cart cart = new Cart();
        cart.setMember(member);
        return cart;
    }

✨ 장바구니 상품 관련 설정

  • 장바구니에 담을 상품 생성 메소드 추가
  • 장바구니에 담을 수량 증가시키는 메소드 추가

-> CartItem

	// 장바구니에 담을 상품 생성
    public static CartItem createCartITem(Cart cart, Item item, int count){
        CartItem cartItem = new CartItem();
        cartItem.setCart(cart);
        cartItem.setItem(item);
        cartItem.setCount(count);
        return cartItem;
    }
    // 장바구니에 담을 상품 수량 증가
    public void addCount(int count){
        this.count += count;
    }

✨ 현재 로그인한 회원의 Cart 찾기

-> CartRepository

public interface CartRepository extends JpaRepository<Cart, Long> {

    // 로그인한 회원의 장바구니 조회
    Cart findByMemberId(Long memberId);
}

✨ 장바구니 상품 Repository 생성

  • 장바구니 아이디와 상품 아이디로 장바구니 상품 조회

-> CartItemRepository

public interface CartItemRepository extends JpaRepository<CartItem, Long> {
    // 장바구니 들어갈 상품 저장하거나 조회
    CartItem findByCartIdAndItemId(Long cartId, Long itemId);
}

✨ Service에서 장바구니에 상품을 담는 로직을 작성

  • 회원의 장바구니가 없으면 장바구니 생성
  • 현재 장바구니에 상품이 있는지 확인
  • 이미 장바구니에 상품이 있으면 수량을 증가
  • 없으면 장바구니 상품을 생성해서 save()

-> CartService

@Service
@RequiredArgsConstructor
@Transactional
public class CartService {

    private final ItemRepository itemRepository;
    private final MemberRepository memberRepository;
    private final CartRepository cartRepository;
    private final CartItemRepository cartItemRepository;
    private final OrderService orderService;

    // 장바구니에 상품을 담는 로직을 작성
    public Long addCart(CartItemDto cartItemDto, String email){
        Item item = itemRepository.findById(cartItemDto.getItemId())
                .orElseThrow(EntityNotFoundException::new);
        Member member = memberRepository.findByEmail(email);

		// 회원의 장바구니가 없으면 생성
        Cart cart = cartRepository.findByMemberId(member.getId());
        if(cart==null){
            cart = Cart.createCart(member);
            cartRepository.save(cart);
        }
        
        // 현제 싱픔이 장바구니에 이미 있는지 확인
        CartItem savedCartItem = cartItemRepository.findByCartIdAndItemId(cart.getId(),item.getId());

        // 이미 장바구니에 있으면, 수량 증가
        if(savedCartItem != null){
            savedCartItem.addCount(cartItemDto.getCount());
            return savedCartItem.getId();
        }else{ // 없으면 장바구니 상품 생성
            CartItem cartItem = CartItem.createCartITem(cart, item, cartItemDto.getCount());
            cartItemRepository.save(cartItem);
            return cartItem.getId();
        }
}

✨ 장바구니 관련된 요청 처리하는 컨트롤러 생성

  • 장바구니 상품 폼을 파라미터로 받는다
  • bindingResult의 오류가 있으면 FieldError을 ResponseEntity로 반환
  • try-catch 문을 이용해 장바구니에 장바구니 상품을 넣는 로직을 호출
  • ResponseEntity로 장바구니 상품의 아이디를 반환

-> CartController

@Controller
@RequiredArgsConstructor
public class CartController {

    private final CartService cartService;

    @PostMapping(value = "/cart")
    public @ResponseBody ResponseEntity order(@RequestBody @Valid CartItemDto cartItemDto, BindingResult bindingResult, Principal principal){

        if(bindingResult.hasErrors()){
            StringBuilder sb = new StringBuilder();
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            for(FieldError fieldError:fieldErrors){
                sb.append(fieldError.getDefaultMessage());
            }
            return new ResponseEntity<String>(sb.toString(), HttpStatus.BAD_REQUEST);
        }

        String email = principal.getName();
        Long cartItemId;

        // 장바구니에 상품 추가 로직 호출
        try{
            cartItemId = cartService.addCart(cartItemDto, email);
        }catch (Exception e){
            return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST);
        }

        return new ResponseEntity<Long>(cartItemId, HttpStatus.OK);
    }

✨ 상품을 장바구니 담는 테스트 코드 작성

  • 상품과 회원을 임의로 만듬
  • 장바구니 상품에 상품 아이디와 주문 수량을 셋팅
  • 장바구니에 장바구니 상품 담는 로직 호출
  • 장바구니 상품 Repository에 저장된 회원 정보와 장바구니 상품에 저장된 회원 정보 비교
  • 장바구니 상품 Repository에 저장된 수량와 장바구니 상품에 저장된 수량 비교

-> CartServiceTest

생략

✨ JS에 장바구니 담기 로직을 호출하는 코드 작성

  • 상품 상세 페이지에서 장바구니 담기 버튼에 해당 로직 등록

-> ItemDtl.html

생략


2. 장바구니 조회

장바구니에 담긴 상품들을 조회하는 기능을 구현해보자

Index

  1. 장바구니 조회 페이지에 전달할 DTO 생성 (상세 정보 DTO)
  2. 장바구니 ID로 장바구니 상세 정보 DTO를 조회 하는 쿼리문 작성
  3. 장바구니에 들어있는 장바구니 상세 정보 리스트를 조회하는 로직
  4. 장바구니 페이지로 이동할 컨트롤러 매핑
  5. 상품 수량을 회원 장바구니 상품의 수량과 동기화
  6. 장바구니 상품의 수량을 업데이트
  7. 장바구니 상품 수량 업데이트 요청을 처리

✨ 장바구니 조회 페이지에 전달할 DTO 생성

  • 장바구니 상품 아이디
  • 상품 이름
  • 가격
  • 수량
  • 이미지 주소

-> CartDetailDto

@Getter @Setter
public class CartDetailDto {

    private Long cartItemId;

    private String itemNm;

    private int price;

    private int count;

    private String imgUrl;

    public CartDetailDto(Long cartItemId, String itemNm, int price, int count, String imgUrl){
        this.cartItemId = cartItemId;
        this.itemNm = itemNm;
        this.price = price;
        this.count = count;
        this.imgUrl = imgUrl;
    }

}

✨ 장바구니 상세 정보 조회 쿼리

  • 연관 매핑이 지연 로딩이면 추가적인 쿼리문이 실행 됨.
  • 이를 위해 DTO의 생성자를 이용해서 반환 값으로 DTO 객체를 생성

-> CartItemRepository

JPQL 추가 공부..

    // new 키워드와 해당 DTO패키지, 클래명을 적어줌 (파라미터는 DTO 클래스에 명시한 순으로 넣음)
    @Query("select new com.example.demo.dto.CartDetailDto(ci.id, i.itemNm, i.price, ci.count, im.imgUrl) "+
    "from CartItem ci, ItemImg im " +
    "join ci.item i "+
    "where ci.cart.id = :cartId " +
    "and im.item.id = ci.item.id " +
            "and im.repImgYn = 'Y' " +
    "order by ci.regTime desc")
    List<CartDetailDto> findCartDetailDtoList(@Param("cartId") Long cartId);

✨ 장바구니에 들어있는 상품들 조회

  • 장바구니 조회 페이지에 보낼 DTO 리스트 생성
  • 이메일로 회원 정보 받아옴
  • 회원 정보로 장바구니 조회
  • 장바구니가 없으면 DTO 리스트 반환
  • 장바구니가 있으면, 장바구니에 들어있는 DTO 리스트 조회 후, 해당 리스트 반환

-> CartService

    // 로그인한 회원으로 장바구니에 들어있는 상품 조회
    @Transactional(readOnly = true)
    public List<CartDetailDto> getCartList(String email){

        List<CartDetailDto> cartDetailDtoList = new ArrayList<>();

        Member member = memberRepository.findByEmail(email);
        Cart cart = cartRepository.findByMemberId(member.getId());
        if(cart == null){
            return cartDetailDtoList;
        }

        cartDetailDtoList = cartItemRepository.findCartDetailDtoList(cart.getId());

        return cartDetailDtoList;
    }

✨ 장바구니 페이지로 이동할 컨트롤러 매핑

장바구니 리스트를 보여주는 페이지로 이동
CartDetailDto : 장바구니 조회 페이지에 보낼 DTO

-> CartController

    @GetMapping(value = "/cart")
    public String orderHist(Principal principal, Model model){
        List<CartDetailDto> cartDetailList = cartService.getCartList(principal.getName());
        model.addAttribute("cartItems", cartDetailList);
        return "cart/cartList";
    }

✨ 장바구니 조회 페이지 생성

-> CartList.html

		$(document).ready(function(){
            // 1. 주문할 상품을 체크하거나 해제할 경우 총 주문 금액을 구하는 함수 호출
            // .change로 변경 감지 시, function 실행
            $("input[name=cartChkBox]").change( function(){
                getOrderTotalPrice();
            });
        });

        // 2. 총 주문 금액 구하는 함수
        function getOrderTotalPrice(){
            var orderTotalPrice = 0;
            // 3. 현재 체크된 장바구니 상품들의 가격과 수량을 곱해서 총 주문 금액 계산
            $("input[name=cartChkBox]:checked").each(function() {
                var cartItemId = $(this).val();
                var price = $("#price_" + cartItemId).attr("data-price");
                var count = $("#count_" + cartItemId).val();
                orderTotalPrice += price*count;
            });

            $("#orderTotalPrice").html(orderTotalPrice+'원');
        }

        // 4. 상품 수량 변경 시, 상품 가격과 상품 수량을 곱해서 상품 금액을 변경
        function changeCount(obj){
            var count = obj.value;
            var cartItemId = obj.id.split('_')[1];
            var price = $("#price_" + cartItemId).data("price");
            var totalPrice = count*price;
            $("#totalPrice_" + cartItemId).html(totalPrice+"원");
            getOrderTotalPrice(); // 주문된 총 주문 금액 구함
            updateCartItemCount(cartItemId, count);
        }

        // 5. 장바구니의 전체 상품을 체크하거나 체크 해제
        function checkAll(){
            if($("#checkall").prop("checked")){
                $("input[name=cartChkBox]").prop("checked",true);
            }else{
                $("input[name=cartChkBox]").prop("checked",false);
            }
            // 변경된 주문 총 금액 계산
            getOrderTotalPrice();
        }

✨ 상품 수량을 회원 장바구니 상품의 수량과 동기화

장바구니 상품의 수량을 업데이트하는 메서드 추가

-> CartItem

    public void updateCount(int count){
        this.count = count;
    }

✨ 장바구니 상품의 수량을 업데이트하는 로직을 추가

  • 서비스 클래스에서 해당 로직을 추가
  • 현재 로그인 회원과 장바구니 상품을 저장한 회원을 검사하는 로직도 구현

-> CartService

    // 장바구니 상품을 저장한 회원과 현재 로그인한 회원이 같은지 확인하는 메서드
    @Transactional(readOnly = true)
    public boolean validateCartItem(Long cartItemId, String email){
        Member curMemebr = memberRepository.findByEmail(email);
        CartItem cartItem = cartItemRepository.findById(cartItemId)
                .orElseThrow(EntityNotFoundException::new);

        Member savedMember = cartItem.getCart().getMember();

        if(!StringUtils.equals(curMemebr.getEmail(), savedMember.getEmail())){
            return false;
        }

        return true;
    }

    // 장바구니 상품의 수량을 업데이트
    public void updateCartItemCount(Long cartItemId, int count){
        CartItem cartITem = cartItemRepository.findById(cartItemId)
                .orElseThrow(EntityNotFoundException::new);

        cartITem.updateCount(count);
    }

✨ 장바구니 상품 수량 업데이트 요청 처리

  • 요청된 자원 일부만 업데이트하기 위해 PATCH 사용
  • 수량이 0개보다 작거나 같으면, 에러
  • 회원이 다르면, 에러
  • 서비스의 수량 업데이트 메서드 호출
  • ResponseEntity로 장바구니 상품 아이디 반환

-> CartController

    // 요청된 자원의 일부를 업데이트할 때 Patch 사용 (장바구니 상품의 수량만 업데이트 함)
    @PatchMapping(value = "/cartItem/{cartItemId}")
    public @ResponseBody ResponseEntity updateCartItem(@PathVariable("cartItemId") Long cartItemId, int count, Principal principal){

        if(count <= 0){
            return new ResponseEntity<String>("최소 1개 이상 담아주세요", HttpStatus.BAD_REQUEST);
        } else if(!cartService.validateCartItem(cartItemId, principal.getName())){
            return new ResponseEntity<String>("수정 권한이 없습니다.", HttpStatus.FORBIDDEN);
        }

        cartService.updateCartItemCount(cartItemId, count);
        return new ResponseEntity<Long>(cartItemId, HttpStatus.OK);
    }

✨ 장바구니 상품 수량을 수정할 경우 업데이트 요청

장바구니 페이지에서 수량 변경 시, 업데이트하도록 요청하는 JS 추가

  • 부분 업데이트기 때문에 형식을 PATCH로 변경!

-> cartList.html

        function updateCartItemCount(cartItemId, count){
            var token = $("meta[name='_csrf']").attr("content");
            var header = $("meta[name='_csrf_header']").attr("content");

            var url = "/cartItem/" + cartItemId+"?count=" + count;

            $.ajax({
                url      : url,
                type     : "PATCH", // 부분 업데이트기 때문에 PATCH 타입으로 변경!!
                beforeSend : function(xhr){
                    /* 데이터를 전송하기 전에 헤더에 csrf값을 설정 */
                    xhr.setRequestHeader(header, token);
                },
                dataType : "json",
                cache   : false,
                success  : function(result, status){
                    console.log("cartItem count update success");
                },
                error : function(jqXHR, status, error){

                    if(jqXHR.status == '401'){
                        alert('로그인 후 이용해주세요');
                        location.href='/members/login';
                    } else{
                        alert(jqXHR.responseJSON.message);
                    }

                }
            });
        }

✨ 장바구니 상품을 삭제하는 로직

-> CartService

    // 장바구니 상품 아이디로 장바구니 상품 제거
    public void deleteCartItem(Long cartItemId) {
        CartItem cartItem = cartItemRepository.findById(cartItemId)
                .orElseThrow(EntityNotFoundException::new);
        cartItemRepository.delete(cartItem);
    }

✨ 장바구니 상품 삭제 요청 처리

  • 요청된 자원을 삭제할 땐, Delete 사용
  • 회원이 맞는지 확인
  • 서비스의 삭제 메서드 호출
  • 삭제된 장바구니 상품 아이디를 ResponseEntity 객체로 반환

-> CartController

    @DeleteMapping(value = "/cartItem/{cartItemId}")
    public @ResponseBody ResponseEntity deleteCartItem(@PathVariable("cartItemId") Long cartItemId, Principal principal){

        if(!cartService.validateCartItem(cartItemId, principal.getName())){
            return new ResponseEntity<String>("수정 권한이 없습니다.", HttpStatus.FORBIDDEN);
        }

        cartService.deleteCartItem(cartItemId);

        return new ResponseEntity<Long>(cartItemId, HttpStatus.OK);
    }

✨ X버튼으로 상품을 삭제하는 JS 구현

  • 요청된 자원을 삭제하기 위해 타입을 DELETE로 변경
  • 삭제 완료 시, 장바구니 페이지로 새로고침

-> cartList.html

        function deleteCartItem(obj){
            var cartItemId = obj.dataset.id;
            var token = $("meta[name='_csrf']").attr("content");
            var header = $("meta[name='_csrf_header']").attr("content");

            var url = "/cartItem/" + cartItemId;

            $.ajax({
                url      : url,
                type     : "DELETE", // 형식을 DELETE로 변경!
                beforeSend : function(xhr){
                    /* 데이터를 전송하기 전에 헤더에 csrf값을 설정 */
                    xhr.setRequestHeader(header, token);
                },
                dataType : "json",
                cache   : false,
                success  : function(result, status){
                    location.href='/cart';
                },
                error : function(jqXHR, status, error){

                    if(jqXHR.status == '401'){
                        alert('로그인 후 이용해주세요');
                        location.href='/members/login';
                    } else{
                        alert(jqXHR.responseJSON.message);
                    }

                }
            });
        }


3. 장바구니 상품 주문하기

체크박스가 선택된 상품을 주문하는 로직을 추가해보자
장바구니는 기존 주문하기와 달리 여러 개의 상품을 하나의 주문에 담을 수 있다
또한, 주문한 상품을 장바구니에서 삭제 하는 로직도 추가해야한다.

✨ 장바구니 페이지에서 주문할 상품 데이터를 전달할 DTO 생성

-> CartOrderDto

// 장바구니 페이지에서 주문할 상품 데이터를 전달
@Getter
@Setter
public class CartOrderDto {

    private Long cartItemId;

    // 장바구니에서 여러 개의 상품을 주문하기 때문에 자기를 List로 가짐
    private List<CartOrderDto> cartOrderDtoList;
}

✨ 장바구니에서 주문할 상품 데이터를 전달받아서 주문을 생성하는 로직 추가

  • 주문 상품 리스트를 만듬
  • 주문 폼을 돌면서 각 주문의 주문 상품을 리스트에 저장
  • 주문 상품 리스트로 주문 만듬
  • 주문 Repository에서 해당 주문 저장
  • 주문 아이디 반환

-> OrderService

    /*
    장바구니에서 주문할 상품 데이터를 전달받아 주문을 생성하는 로직
     */
    public Long orders(List<OrderDto> orderDtoList, String email){

        Member member = memberRepository.findByEmail(email);
        List<OrderItem> orderItemList = new ArrayList<>(); // 1. 주문상품 리스트

        for(OrderDto orderDto : orderDtoList){ // 주문 폼 돌면서 각 상품으로 주문상품 만들어서 넣음
            Item item = itemRepository.findById(orderDto.getItemId())
                    .orElseThrow(EntityNotFoundException::new);
            OrderItem orderItem = OrderItem.createOrderItem(item, orderDto.getCount());
            orderItemList.add(orderItem);
        }

        Order order = Order.createOrder(member, orderItemList); // 주문 만들어서 주문 상품 리스트 넣음
        orderRepository.save(order);

        return order.getId();
    }

✨ 서비스 클래스에 주문 로직 및 장바구니 업데이트 로직 추가

  • 주문 로직으로 전달할 orderDto 리스트 생성
  • 장바구니에서 보낸 주문 객체를 돌면서 orderDto를 만들어 리스트에 저장
  • orderDto 리스트로 주문을 생성하고, 주문 아이디를 반환
  • orderDto 리스트 돌면서 해당 장바구니 상품을 삭제

-> CartService

    // 장바구니에서 주문하기 위해 보낸 리스트들을 주문 폼 리스트로 만들어서 주문로직에 전달
    public Long orderCartItem(List<CartOrderDto> cartOrderDtoList, String email){

        List<OrderDto> orderDtoList = new ArrayList<>();

        // 장바구니페이지에서 보낸 상품들을 하나씩 돌면서 주문 객체로 만들고,
        // 이 주문 객체들을 리스트로 만듦
        for(CartOrderDto cartOrderDto : cartOrderDtoList){
            CartItem cartItem = cartItemRepository.findById(cartOrderDto.getCartItemId())
                    .orElseThrow(EntityNotFoundException::new);
            OrderDto orderDto = new OrderDto();
            orderDto.setItemId(cartItem.getItem().getId());
            orderDto.setCount(cartItem.getCount());
            orderDtoList.add(orderDto);
        }

        // 주문 폼 리스트로 주문을 생성하고, 주문 번호를 반환
        Long orderId = orderService.orders(orderDtoList, email);

        // 주문한 상품들을 장바구니에서 삭제하는 작업
        for(CartOrderDto cartOrderDto : cartOrderDtoList){
            CartItem cartItem = cartItemRepository.findById(cartOrderDto.getCartItemId())
                    .orElseThrow(EntityNotFoundException::new);
            cartItemRepository.delete(cartItem);
        }

        return orderId;
    }

✨ 장바구니 상품 수량을 업데이트하는 요청을 처리

  • 장바구니 페이지의 주문 폼을 받음
  • 주문 폼이 비었거나 주문 회원과 로그인된 회원 같은지 확인
  • 주문 로작 실행

-> CartController

    @PostMapping(value = "/cart/orders")
    public @ResponseBody ResponseEntity orderCartItem(@RequestBody CartOrderDto cartOrderDto, Principal principal){
        List<CartOrderDto> cartOrderDtoList = cartOrderDto.getCartOrderDtoList();

        if(cartOrderDtoList == null || cartOrderDtoList.size() == 0){
            return new ResponseEntity<String>("주문할 상품을 선택해주세요", HttpStatus.FORBIDDEN);
        }

        for(CartOrderDto cartOrder : cartOrderDtoList){
            if(!cartService.validateCartItem(cartOrder.getCartItemId(), principal.getName())){
                return new ResponseEntity<String>("주문 권한이 없습니다.", HttpStatus.FORBIDDEN);
            }
        }

        Long orderId = cartService.orderCartItem(cartOrderDtoList, principal.getName());
        return new ResponseEntity<Long>(orderId, HttpStatus.OK);
    }

✨ 장바구니에서 선택한 상품 주문을 요청하도록 JS 수정

-> cartList.html

        // 장바구니에서 선택한 상품 주문을 처리
        function orders(){
            var token = $("meta[name='_csrf']").attr("content");
            var header = $("meta[name='_csrf_header']").attr("content");

            var url = "/cart/orders";

            var dataList = new Array();
            var paramData = new Object();

            // 장바구니에서 체크된 장바구니 상품의 아이디 전달 위해
            // 리스트에 장바구니 상품 아이디를 객체로 만들어 저장
            $("input[name=cartChkBox]:checked").each(function() {
                var cartItemId = $(this).val();
                var data = new Object();
                data["cartItemId"] = cartItemId;
                dataList.push(data);
            });

            // 장바구니 상품 아이디를 저장한 리스트를 다시 저장
            paramData['cartOrderDtoList'] = dataList;

            var param = JSON.stringify(paramData);

            $.ajax({
                url      : url,
                type     : "POST",
                contentType : "application/json",
                data     : param,
                beforeSend : function(xhr){
                    /* 데이터를 전송하기 전에 헤더에 csrf값을 설정 */
                    xhr.setRequestHeader(header, token);
                },
                dataType : "json",
                cache   : false,
                success  : function(result, status){
                    alert("주문이 완료 되었습니다.");
                    location.href='/orders';
                },
                error : function(jqXHR, status, error){

                    if(jqXHR.status == '401'){
                        alert('로그인 후 이용해주세요');
                        location.href='/members/login';
                    } else{
                        alert(jqXHR.responseJSON.message);
                    }

                }
            });

0개의 댓글

관련 채용 정보