관리 메뉴

거니의 velog

(45) 장바구니 API 만들기 3 _ 수정 본문

SpringBoot_React 풀스택 프로젝트

(45) 장바구니 API 만들기 3 _ 수정

Unlimited00 2024. 3. 8. 16:38

4. 장바구니 서비스 계층의 설계/구현

* 장바구니 서비스는 service 패키지 내에 CartService와 CartServiceImpl로 구현한다.

* CartService에는 장바구니 아이템을 추가하거나 수정하는 기능이 CartItemDTO를 이용하므로 하나의 메서드로 설계하고 사용자의 장바구니 아이템들의 조회와 장바구니 아이템의 삭제 기능을 선언한다.

package com.unlimited.mallapi.service;

import java.util.List;

import org.springframework.transaction.annotation.Transactional;

import com.unlimited.mallapi.dto.CartItemDTO;
import com.unlimited.mallapi.dto.CartItemListDTO;

/**
 * 장바구니 서비스를 정의한 인터페이스입니다.
 *
 * @Transactional: 해당 인터페이스의 모든 메서드에 대한 트랜잭션 처리를 지정하는 어노테이션
 */
@Transactional(rollbackFor = Exception.class)
public interface CartService {

    /**
     * 장바구니에 상품을 추가 또는 수정하는 메서드입니다.
     *
     * @param cartItemDTO 장바구니 상품 정보를 담은 DTO
     * @return 업데이트된 장바구니 상품 목록
     */
    public List<CartItemListDTO> addOrModify(CartItemDTO cartItemDTO);

    /**
     * 특정 회원의 장바구니 상품 목록을 조회하는 메서드입니다.
     *
     * @param email 조회할 회원의 이메일
     * @return 특정 회원의 장바구니 상품 목록
     */
    public List<CartItemListDTO> getCartItems(String email);

    /**
     * 장바구니 상품을 삭제하는 메서드입니다.
     *
     * @param cino 삭제할 장바구니 상품의 고유 식별 번호
     * @return 삭제 후의 장바구니 상품 목록
     */
    public List<CartItemListDTO> remove(Long cino);

}

* 메서드들의 리턴 타입이 모두 List<CartItemListDTO> 인 것은 장바구니 아이템을 처리한 후에는 화면에 새로 갱신해야 하는 장바구니 아이템들의 데이터가 필요하기 때문이다.

* CartServiceImpl에서의 구현은 아래와 같다.

package com.unlimited.mallapi.service;

import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Service;

import com.unlimited.mallapi.domain.Cart;
import com.unlimited.mallapi.domain.CartItem;
import com.unlimited.mallapi.domain.Member;
import com.unlimited.mallapi.domain.Product;
import com.unlimited.mallapi.dto.CartItemDTO;
import com.unlimited.mallapi.dto.CartItemListDTO;
import com.unlimited.mallapi.repository.CartItemRepository;
import com.unlimited.mallapi.repository.CartRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

/**
 * 장바구니 서비스 구현 클래스입니다.
 * 
 * @Service: 해당 클래스가 서비스 역할을 한다는 것을 스프링에 알려주는 어노테이션
 * @RequiredArgsConstructor: 필요한 모든 final 필드에 대한 생성자를 자동으로 생성해주는 롬복 어노테이션
 * @Log4j2: 롬복을 사용하여 로깅을 위한 로거를 자동으로 생성해주는 어노테이션
 */
@RequiredArgsConstructor
@Service
@Log4j2
public class CartServiceImpl implements CartService {

    private final CartRepository cartRepository;
    private final CartItemRepository cartItemRepository;

    /**
     * 장바구니에 상품을 추가 또는 수정하는 메서드입니다.
     * 
     * @param cartItemDTO 장바구니 상품 정보를 담은 DTO
     * @return 업데이트된 장바구니 상품 목록
     */
    @Override
    public List<CartItemListDTO> addOrModify(CartItemDTO cartItemDTO) {
        String email = cartItemDTO.getEmail(); // 사용자 이메일
        Long pno = cartItemDTO.getPno(); // 상품 고유 번호
        int qty = cartItemDTO.getQty(); // 상품 수량
        Long cino = cartItemDTO.getCino(); // 장바구니 상품 고유 번호

        log.info("======================================================");
        log.info(cartItemDTO.getCino() == null);

        if (cino != null) { // 장바구니 아이템 번호가 있어서 수량만 변경하는 경우
            Optional<CartItem> cartItemResult = cartItemRepository.findById(cino);
            CartItem cartItem = cartItemResult.orElseThrow();
            cartItem.changeQty(qty);
            cartItemRepository.save(cartItem);
            return getCartItems(email);
        }

        // 장바구니 아이템 번호 cino가 없는 경우
        // 사용자의 카트
        Cart cart = getCart(email);

        CartItem cartItem = null;

        // 이미 동일한 상품이 담긴적이 있을 수 있으므로
        cartItem = cartItemRepository.getItemOfPno(email, pno);

        if (cartItem == null) {
            Product product = Product.builder().pno(pno).build();
            cartItem = CartItem.builder().product(product).cart(cart).qty(qty).build();
        } else {
            cartItem.changeQty(qty);
        }

        // 상품 아이템 저장
        cartItemRepository.save(cartItem);

        return getCartItems(email);
    }

    // 사용자의 장바구니가 없었다면 새로운 장바구니를 생성하고 반환
    private Cart getCart(String email) {
        Cart cart = null;
        Optional<Cart> result = cartRepository.getCartOfMember(email);

        if (result.isEmpty()) {
            log.info("Cart of the member is not exist!!");
            Member member = Member.builder().email(email).build();
            Cart tempCart = Cart.builder().owner(member).build();
            cart = cartRepository.save(tempCart);
        } else {
            cart = result.get();
        }
        return cart;
    }

    /**
     * 특정 회원의 장바구니 상품 목록을 조회하는 메서드입니다.
     * 
     * @param email 조회할 회원의 이메일
     * @return 특정 회원의 장바구니 상품 목록
     */
    @Override
    public List<CartItemListDTO> getCartItems(String email) {
        return cartItemRepository.getItemsOfCartDTOByEmail(email);
    }

    /**
     * 장바구니 상품을 삭제하는 메서드입니다.
     * 
     * @param cino 삭제할 장바구니 상품의 고유 식별 번호
     * @return 삭제 후의 장바구니 상품 목록
     */
    @Override
    public List<CartItemListDTO> remove(Long cino) {
        Long cno = cartItemRepository.getCartFromItem(cino);
        log.info("cart no: " + cno);
        cartItemRepository.deleteById(cino);
        return cartItemRepository.getItemsOfCartDTOByCart(cno);
    }

}

5. 컨트롤러 계층과 테스트

* 장바구니 관련 기능은 controller 패키지에 CartController를 추가해서 구현한다. 외부에서 호출하는 경로는 /api/cart/ 로 시작하도록 구현한다. /api/member 를 제외하면 모든 기능은 JWTCheckFilter를 거치기 때문에 CartController는 스프링 시큐리티 관련 기능을 사용하도록 구현한다.

package com.unlimited.mallapi.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.unlimited.mallapi.service.CartService;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@RestController
@RequiredArgsConstructor
@Log4j2
@RequestMapping("/api/cart")
public class CartController {

    private final CartService cartService;

}

* CartController는 CartService 타입의 객체를 주입받도록 설계한다.


(1) 장바구니 아이템의 추가/수정

* 장바구니 아이템의 추가/수정에서는 장바구니 아이템의 수량(qty)에 중점을 두고 코드를 작성한다. 만일 수량이 0보다 작은 상태가 되면 실제로는 삭제로 처리하고 장바구니 아이템 목록을 반환한다.

package com.unlimited.mallapi.controller;

import java.util.List;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.unlimited.mallapi.dto.CartItemDTO;
import com.unlimited.mallapi.dto.CartItemListDTO;
import com.unlimited.mallapi.service.CartService;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

/**
 * 장바구니 관련 요청을 처리하는 컨트롤러 클래스입니다.
 * 
 * @RestController: 해당 클래스가 RESTful API 요청을 처리하는 컨트롤러임을 나타내는 어노테이션
 * @RequiredArgsConstructor: 필요한 모든 final 필드에 대한 생성자를 자동으로 생성해주는 롬복 어노테이션
 * @Log4j2: 롬복을 사용하여 로깅을 위한 로거를 자동으로 생성해주는 어노테이션
 * @RequestMapping: 클래스 레벨에서 요청 URL을 지정하는 어노테이션
 */
@RestController
@RequiredArgsConstructor
@Log4j2
@RequestMapping("/api/cart")
public class CartController {

    private final CartService cartService;

    /**
     * 장바구니에 상품을 추가하거나 수정하는 메서드입니다.
     * 
     * @param itemDTO 장바구니 상품 정보를 담은 DTO
     * @return 업데이트된 장바구니 상품 목록
     */
    @PreAuthorize("#itemDTO.email == authentication.name")
    @PostMapping("/change")
    public List<CartItemListDTO> changeCart(@RequestBody CartItemDTO itemDTO) {
        log.info(itemDTO);

        if (itemDTO.getQty() <= 0) {
            return cartService.remove(itemDTO.getCino());
        }

        return cartService.addOrModify(itemDTO);
    }

}

* changeCart() 에서는 현재 로그인한 사용자의 이메일과 파라미터로 전달된 CartItemDTO의 이메일 주소가 같아야만 호출이 가능하도록 @PreAuthorize 표현식을 적용한다. 만일 두 이메일 정보가 일치하지 않는다면 접근 권한이 없는(Access Denied) 상황으로 처리된다.


* 근데? 오류가 뜨는 상황이라서 일단 @PreAuthorize("permitAll()") 로 접근 권한을 다 열어 놓았으니 일단 추후에 수정 바람....

이렇게 하면 된다는데 추후 적용해 보고 테스트할 예정


* 테스트를 위해서는 우선 /api/member/login 을 이용해서 로그인을 통해 만들어지는 Access Token을 활용해야 한다.

* /api/cart/change 를 호출할 때 Authorization 헤더를 지정한다.

* 특정한 상품으로 추가하는 경우 장바구니 아이템의 번호가 없는 상태가 되므로 이를 JSON으로 전송한다.

* 상품번호(pno)를 변경하면 새로운 상품이 추가되는지 확인한다.

* 만일 장바구니 아이템 번호가 전달되면 이를 이용하므로 상품번호는 없어도 무방하다. 예를 들어 아래의 왼쪽과 같이 3번 아이템의 수량(qty)을 변경하고자 했다면 아래의 그림과 같이 cino 값을 전달하면 된다.


(2) 사용자의 장바구니 목록

* 사용자의 장바구니 목록은 /api/cart/items 로 조회할 수 있도록 구성한다. 호출 시에는 시큐리티를 통해서 사용자의 인증 정보를 이용하도록 구성한다.

    /**
     * 현재 사용자의 장바구니 상품 목록을 조회하는 메서드입니다.
     * 
     * import java.security.Principal;
     * 
     * @param principal 현재 사용자를 나타내는 Principal 객체
     * @return 장바구니 상품 목록
     */
    @PreAuthorize("hasAnyRole('ROLE_USER')")
    @GetMapping("/items")
    public List<CartItemListDTO> getCartItems(Principal principal) {
        // Principal 객체를 이용하여 현재 사용자의 이메일을 가져옵니다.
        String email = principal.getName();
        log.info("--------------------------------------------");
        log.info("email: " + email);

        // 현재 사용자의 이메일을 이용하여 장바구니 상품 목록을 조회합니다.
        return cartService.getCartItems(email);
    }

* getItems() 의 파라미터로 java.security.Principal 타입을 지정하는데 이를 이용하면 현재 사용자의 정보에 접근이 가능하다. 주로 다운캐스팅해서 사용하거나 예제와 같이 getName()을 이용해서 username(예제에서는 이메일) 값을 파악할 때 사용한다.

* Postman에서는 별도의 파라미터의 전달없이 GET 방식으로 확인이 가능하다.


(3) 장바구니 아이템의 삭제

* 장바구니 아이템의 삭제는 장바구니 아이템 번호(cino)를 이용해서 DELETE 방식으로 호출하는 경우에 동작하게 한다. 다만, 실제 해당 수량(qty)이 0 이하가 된다면 삭제와 동일한 의미이기 때문에 실제로 직접 호출될 가능성은 많지 않다.

    /**
     * 장바구니에서 특정 상품을 제거하는 메서드입니다.
     *
     * @param cino 장바구니 상품 번호
     * @return 업데이트된 장바구니 상품 목록
     */
    @PreAuthorize("hasAnyRole('ROLE_USER')")
    @DeleteMapping("/{cino}")
    public List<CartItemListDTO> removeFromCart(@PathVariable("cino") Long cino) {
        log.info("장바구니 상품 번호: " + cino);
        return cartService.remove(cino);
    }

* 장바구니는 시큐리티와 같이 접목되기 때문에 각 기능에 대해서 테스트를 반드시 실행해서 동작에 문제가 없는지 확인한 후에 리액트 개발을 이어가야만 한다.