728x90
1. 장바구니 담기
상품 상세 페이지에서 장바구니에 담을 수량 선택 후
장바구니 담기 버튼 클릭 시, 상품이 장바구니에 담기는 기능 구현해보자
Index
- 장바구니에 담을 상품 정보를 전달할 DTO 생성
- 장바구니 상품을 생성하는 메소드 추가
- 장바구니 찾는 쿼리문
- 장바구니 상품을 찾는 쿼리문
- 서비스에서 상품을 장바구니에 넣는 로직 작성
- 장바구니 컨트롤러 매핑
- 장바구니 담는 테스트 코드 작성
- 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
- 장바구니 조회 페이지에 전달할 DTO 생성 (상세 정보 DTO)
- 장바구니 ID로 장바구니 상세 정보 DTO를 조회 하는 쿼리문 작성
- 장바구니에 들어있는 장바구니 상세 정보 리스트를 조회하는 로직
- 장바구니 페이지로 이동할 컨트롤러 매핑
- 상품 수량을 회원 장바구니 상품의 수량과 동기화
- 장바구니 상품의 수량을 업데이트
- 장바구니 상품 수량 업데이트 요청을 처리
✨ 장바구니 조회 페이지에 전달할 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);
}
}
});
728x90
'🛠 백엔드 > 쇼핑몰 클론코딩' 카테고리의 다른 글
[2] JPA (0) | 2023.01.08 |
---|---|
[7] Order (0) | 2023.01.07 |
[6] Item 등록 및 조회 (0) | 2023.01.04 |
[5] 연관매핑 (0) | 2022.12.30 |
[4] Spring Security (1) | 2022.12.30 |