개발/Project(Spring-쇼핑몰)

Project (7-2) 장바구니 조회, 수정, 삭제

잇(IT) 2023. 8. 17. 13:43
728x90

- 이전에 장바구니에 담는 기능을 추가했다면, 이번에는 담은 장바구니를 조회하는 기능에 대해 구현할 것이다.

 

- 장바구니 조회 페이지에 데이터를 전달할 DTO 클래스를 생성한다.

- 이번에는 JPQL로 쿼리 작성 시 생성자를 이용해서 DTO로 바로 변환하는 방법을 사용한다.

- CartDetailDto.java

@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) {
//        1. 장바구니 페이지에 전달할 데이터를 생성자의 파라미터로 만들어준다.
        this.cartItemId = cartItemId;
        this.itemNm = itemNm;
        this.price = price;
        this.count = count;
        this.imgUrl = imgUrl;
    }
}

- 모든 필드를 파라미터로 받는 생성자를 생성한다.


- 장바구니 페이지에 전달할 CartDetailDto 리스트를 쿼리 하나로 조회하는 JPQL문을 작성한다. 연관 관계 매핑을 지연 로딩으로 설정할 경우 엔티티에 매핑된 다른 엔티티를 조회할 때 추가적으로 쿼리문이 실행된다.

- 성능 최적화가 필요한 경우 DTO의 생성자를 이용하여 반환 값으로 DTO 객체를 생성할 수 있다.

- CartItemRepository.java

//장바구니에 들어갈 상품을 저장하거나 조회하기 위해서 CartItemRepository 인터페이스를 생성한다.
public interface CartItemRepository extends JpaRepository<CartItem, Long> {

    CartItem findByCartIdAndItemId(Long cartId, Long itemId);
//    카트 아이디와 상품 아이디를 이용해서 상품이 장바구니에 들어있는지 조회한다.

    @Query("select new com.shop.dto.CartDetailDto(ci.id, i.itemNm, i.price, ci.count, im.imgUrl) " +
//            1. CartDetailDto의 생성자를 이용하여 DTO를 반환할 때는 "new com.shop.dto.CartDetailDto(ci.id,
//            i.itemNm, i.price, ci.count, im.imgUrl)"처럼 new 키워드와 해당 DTO 패키지, 클래스명을 적어준다.
//            또한, 생성자의 파라미터 순서는 DTO 클래스에 명시한 순으로 넣어주어야 한다.
            "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' " +
//            2, 3. 장바구니에 담겨있는 상품의 대표 이미지만 가지고 오도록 조건문을 작성한다.
            "order by ci.regTime desc"
    )
    List<CartDetailDto> findCartDetailDtoList(Long cartId);
}

- @Query에 대한 내용은 우선 보류 *******

|

|

|

|

|

|

|


- 현재 로그인한 회원의 정보를 이용하여 장바구니에 들어있는 상품을 조회하는 로직을 작성한다.

- CartService.java

...

@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());
//        1. 현재 로그인한 회원의 장바구니 엔티티를 조회한다.
        if (cart == null) {
//            2. 장바구니에 상품을 한 번도 안 담았을 경우 장바구니 엔티티가 없으므로 빈 리스트를 반환한다.
            return cartDetailDtoList;
        }

        cartDetailDtoList = cartItemRepository.findCartDetailDtoList(cart.getId());
//        3. 장바구니에 담겨있는 상품 정보를 조회한다.

        return cartDetailDtoList;
    }

- findCartDetailDtoList는 cartId를 기준으로 데이터를 가져오기 때문에 cartRepository를 통해 장바구니를 조회할 회원의 Id를 조회하여 해당 회원의 cart 엔티티를 가져온다.

- 만약 장바구니에 상품을 한번도 담지 않으면 장바구니 엔티티가 해당 회원에게 없기 때문에 빈 리스트를 반환하고, 아니라면 findCartDetailDtoList를 통해 해당 쿼리 조건에 맞는 데이터를 가져와 List에 담는다.


- 장바구니 페이지로 이동할 수 있도록 CartController 클래스에 메소드를 추가한다.

- CartController.java

@GetMapping(value = "/cart")
    public String orderHist(Principal principal, Model model) {
        List<CartDetailDto> cartDetailList = cartService.getCartList(principal.getName());
//        1. 현재 로그인한 사용자의 이메일 정보를 이용하여 장바구니에 담겨있는 상품 정보를 조회한다.

        model.addAttribute("cartItems", cartDetailList);
//        2. 조회한 장바구니 상품 정보를 뷰로 전달한다.
        return "cart/cartList";
    }

- @GetMapping을 통해 /cart url로 들어오는 get 요청에 대해 처리한다.

- principal을 통해 가져온 Name의 값(email)을 바탕으로 getCartList 메서드를 실행한다.

- 회원 email을 통해 조회한 장바구니 제품 리스트를 model에 담아 cart/cartList 뷰에 전달한다.


- cartList.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<head>
    <meta name="_csrf" th:content="${_csrf.token}"/>
    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">

    <script th:inline="javascript">

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

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

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

        function changeCount(obj){
            // 4. 장바구니에 들어있는 상품의 수량을 변경 시 상품의 가격과 상품의 수량을 곱해서 상품 금액을 변경해준다.
            // 변경된 총 주문 금액을 계산하기 위해서 마지막에 getOrderTotalPrice() 함수를 호출한다.
            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);
        }

        function checkAll(){
            // 5. 장바구니에 들어있는 전체 상품을 체크하거나 체크 해제하는 함수이다.
            // 변경된 총 주문 금액을 계산하기 위해서 마지막에 getOrderTotalPrice() 함수를 호출한다.
            if($("#checkall").prop("checked")){
                $("input[name=cartChkBox]").prop("checked",true);
            }else{
                $("input[name=cartChkBox]").prop("checked",false);
            }
            getOrderTotalPrice();
        }
        
        </script>

</th:block>

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
    <style>
        .content-mg{
            margin-left:25%;
            margin-right:25%;
            margin-top:2%;
            margin-bottom:100px;
        }
        .repImgDiv{
            margin-right:15px;
            margin-left:15px;
            height:auto;
        }
        .repImg{
            height:100px;
            width:100px;
        }
        .fs18{
            font-size:18px
        }
        .fs24{
            font-size:24px
        }
    </style>
</th:block>

<div layout:fragment="content" class="content-mg">

    <h2 class="mb-4">
        장바구니 목록
    </h2>

    <div>

        <table class="table">
            <colgroup>
                <col width="15%"/>
                <col width="70%"/>
                <col width="15%"/>
            </colgroup>
            <thead>
            <tr class="text-center">
                <td>
                    <input type="checkbox" id="checkall" onclick="checkAll()"> 전체선택
                </td>
                <td>상품정보</td>
                <td>상품금액</td>
            </tr>
            </thead>
            <tbody>
            <tr th:each="cartItem : ${cartItems}">
                <td class="text-center align-middle">
                    <input type="checkbox" name="cartChkBox" th:value="${cartItem.cartItemId}">
                </td>
                <td class="d-flex">
                    <div class="repImgDiv align-self-center">
                        <img th:src="${cartItem.imgUrl}" class = "rounded repImg" th:alt="${cartItem.itemNm}">
                    </div>
                    <div class="align-self-center">
                        <span th:text="${cartItem.itemNm}" class="fs24 font-weight-bold"></span>
                        <div class="fs18 font-weight-light">
                            <span class="input-group mt-2">
                                <span th:id="'price_' + ${cartItem.cartItemId}"
                                      th:data-price="${cartItem.price}"
                                      th:text="${cartItem.price} + '원'" class="align-self-center mr-2">
                                </span>
                                <input type="number" name="count" th:id="'count_' + ${cartItem.cartItemId}"
                                       th:value="${cartItem.count}" min="1"
                                       onchange="changeCount(this)" class="form-control mr-2" >
                                <button type="button" class="close" aria-label="Close">
                                    <span aria-hidden="true" th:data-id="${cartItem.cartItemId}" onclick="deleteCartItem(this)">&times;</span>
                                    <!--<삭제> 버튼을 누르면 deleteCartItem() 함수가 호출되도록 onclick 속성을 추가한다.-->
                                </button>
                            </span>
                        </div>
                    </div>
                </td>
                <td class="text-center align-middle">
                    <span th:id="'totalPrice_' + ${cartItem.cartItemId}"
                          name="totalPrice" th:text="${cartItem.price * cartItem.count} + '원'">
                    </span>
                </td>
            </tr>
            </tbody>
        </table>

        <h2 class="text-center">
            총 주문 금액 : <span id="orderTotalPrice" class="text-danger">0원</span>
        </h2>

        <div class="text-center mt-3">
            <button type="button" class="btn btn-primary btn-lg" onclick="orders()">주문하기</button>
        </div>

    </div>

</div>

</html>

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">

    <script th:inline="javascript">

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

- 해당 name="cartChkBox"의 체크박스의 변화가 일어나면 getOrderTotalPrice() 메서드가 실행된다.

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

- name=cartChkBox를 전부 찾아서 해당 체크박스가 checked일 경우 fuction() 즉, 아래 함수들을 실행한다.

- 체크박스가 체크된 name=cartChkBox에 해당하는 value()값을 찾아서 변수에 대입한다.

- 해당 cartItem의 가격과 수량을 조회해 orderTotalPrice를 구해낸다.

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

- getOrderTotalPrice() 메서드가 실행되면 마지막으로 .html메서드가 실행되면서 실시간으로 화면에 보이는 최종금액이 변경된다.


- 장바구니에서 상품의 수량을 변경할 경우 실시간으로 해당 회워느이 장바구니 상품의 수량도 변경하도록 로직을 수정한다.

- CartItem.java

.....

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

- CartService 클래스에 장바구니 상품의 수량을 업데이트하는 로직을 추가한다. 자바스크립트 코드에서 업데이트할 장바구니 상품 번호는 조작이 가능하기 때문에 현재 로그인한 회원과 해당 장바구니 상품을 저장한 회원이 같은지 검사하는 로직도 작성한다.

- CartService.java

@Transactional(readOnly = true)
    public boolean validateCartItem(Long cartItemId, String email) {
        Member curMember = memberRepository.findByEmail(email);
//        1. 현재 로그인한 회원을 조회한다.
        CartItem cartItem = cartItemRepository.findById(cartItemId).orElseThrow(EntityNotFoundException::new);
        Member savedMember = cartItem.getCart().getMember();
//        2. 장바구니 상품을 저장한 회원을 조회한다.

        if (!StringUtils.equals(curMember.getEmail(),
                savedMember.getEmail())) {
            return false;
        }
        return true;
//        3, 4. 현재 로그인한 회원과 장바구니 상품을 저장한 회원이 다를 경우 false를, 같으면 true를 반환한다.
    }

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

        cartItem.updateCount(count);
    }

- CartService에서 updateCartItemCount메서드를 통해 상품의 수량을 업데이트하게 된다.


- Controller에 장바구니 상품의 수량을 업데이트하는 요청을 처리할 수 있도록 로직을 추가한다.

- CartController.java

@PatchMapping(value = "/cartItem/{cartItemId}")
//    1. HTTP 메소드에서 PATCH는 요청된 자원의 일부를 업데이트할 때 PATCH를 사용한다.
//    장바구니 상품의 수량만 업데이트하기 때문에 @PatchMapping을 사용한다.
    public @ResponseBody ResponseEntity updateCartItem(@PathVariable("cartItemId") Long cartItemId,
                                                       int count, Principal principal) {
//        cartList.html에서 /cartItem/{cartItemId} 경로로 요청을 보내는데, ?count= url로 요청을 보낸다.
//        updateCartItem 파라미터에서 int count 앞에 @RequestParam 어노테이션이 생략되었다고 보면 된다.

        if (count <= 0) {
//            2. 장바구니에 담겨있는 상품의 개수를 0개 이하로 업데이트 요청을 할 때 에러 메시지를 담아서 반환한다.
            return new ResponseEntity<String>("최소 1개 이상 담아주세요", HttpStatus.BAD_REQUEST);
        } else if (!cartService.validateCartItem(cartItemId, principal.getName())) {
//            3. 수정 권한을 체크한다.
            return new ResponseEntity("수정 권한이 없습니다.", HttpStatus.FORBIDDEN);
        }
        cartService.updateCartItemCount(cartItemId, count);
//        4. 장바구니 상품의 개수를 업데이트한다.
        return new ResponseEntity<Long>(cartItemId, HttpStatus.OK);
    }

- cartList.html 파일에서 장바구니 상품의 수량을 수정할 경우 업데이트 요청을 하도록 자바스크립트 함수를 추가한다.

- 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",
                // 1. 부분 업데이트이므로 PATCH TYPE으로 설정한다.
                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);
                    }

                }
            });
        }

- patch 타입으로 /cartItem/[cartItemId]?count=[count 숫자] url로 요청을 보내게 된다.

 

- patch로 요청을 보냈기 때문에 controller에서 patch요청에 대한 메서드가 실행되고 위에 작성한 것과 같이, Javascript를 통해 넘어온 count의 경우 (int count) 파라미터로 받게 되는데 해당 변수 앞에 @RequestParam 어노테이션이 생략된 것이며 해당 count를 받아 사용한다.

 

function changeCount(obj){
            // 4. 장바구니에 들어있는 상품의 수량을 변경 시 상품의 가격과 상품의 수량을 곱해서 상품 금액을 변경해준다.
            // 변경된 총 주문 금액을 계산하기 위해서 마지막에 getOrderTotalPrice() 함수를 호출한다.
            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);
        }

- 이전에 작성한 changeCount메서드 마지막에 변경이 발생했을 때 updateCartItemCount가 실행되도록 메서드를 추가한 것을 확인할 수 있다.


- 장바구니 상품 삭제하기

- 장바구니 상품 번호를 파라미터로 받아서 삭제하는 로직을 CartService에 추가한다.

- CartService.java

.....

public void deleteCartItem(Long cartItemId) {
        CartItem cartItem = cartItemRepository.findById(cartItemId).orElseThrow(EntityNotFoundException::new);
        cartItemRepository.delete(cartItem);
    }

- delete의 경우 JPA의 기본 메서드 중 하나이기 때문에 별도의 메서드를 작성해주지 않아도 JPA에서 삭제를 처리해준다.


- 장바구니 상품을 삭제하는 요청을 처리하는 로직을 Controller에 추가한다.

- CartController.java

.....

@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);
    }

- 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",
                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);
                    }

                }
            });
        }
<span aria-hidden="true" th:data-id="${cartItem.cartItemId}" onclick="deleteCartItem(this)">&times;</span>

- 아래 HTML 코드를 확인해보면 onclick이 실행되면 deleteCartItem이 실행되는 코드가 있는 것을 확인할 수 있다.

- x 버튼이 눌리게 되면 해당 url로 delete 요청이 보내지게 된다.

- 해당 delete 요청은 controller에서 deleteMapping에 해당하는 부분에서 받아서 처리한다.

- @DeleteMapping 요청이 오게되면 해당 cartItemId를 삭제하게 된다.

728x90