- 이전에 장바구니에 담는 기능을 추가했다면, 이번에는 담은 장바구니를 조회하는 기능에 대해 구현할 것이다.
- 장바구니 조회 페이지에 데이터를 전달할 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)">×</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)">×</span>
- 아래 HTML 코드를 확인해보면 onclick이 실행되면 deleteCartItem이 실행되는 코드가 있는 것을 확인할 수 있다.
- x 버튼이 눌리게 되면 해당 url로 delete 요청이 보내지게 된다.
- 해당 delete 요청은 controller에서 deleteMapping에 해당하는 부분에서 받아서 처리한다.
- @DeleteMapping 요청이 오게되면 해당 cartItemId를 삭제하게 된다.
'Portfolio, Project > Project(Programming)' 카테고리의 다른 글
홈페이지 만들기 - 1 (0) | 2023.08.21 |
---|---|
Project (7-3) 장바구니 상품 주문하기 (0) | 2023.08.18 |
Project (7-1) 장바구니 담기 (0) | 2023.08.16 |
Project (6-3) 주문 취소하기 (0) | 2023.08.16 |
Project (6-2) 주문 이력 조회하기 (0) | 2023.08.15 |