728x90
320x100
주문, 결제 구현 전 장바구니 기능을 구현하면서, 사용자가 상품을 장바구니에 넣고 -> 주문 -> 결제가 이루어지기까지 전체 과정을 먼저 설계했다.
엔티티
엔티티 설계
아래는 우리 프로젝트 전체 ERD이다.
이 많은 테이블이 주문/결제를 위해 존재한다고 봐도 무방... 그러나 카테고리나 상품 색상, 썸네일등 필수적이지 않은 테이블을 제외하고 살펴보면,
- 1. 상품 테이블(Product) : 상품 정보를 담은 엔티티. 이는 상품 이름과 가격, 썸네일 등을 담고 있다.
- 상품 관리 테이블(ProductManagement) : 상품을 참조해 사이즈/색상 등의 세부 옵션별로 상품 저장
- 실제로 사용자가 보는 상품 리스트, 장바구니/주문에 사용되는 객체
- 2. 따라서 상품을 장바구니에 담으면 ProductManagement 테이블을 참조하는 Cart 객체 생성
- Cart와 ProductManagement는 ManyToOne
- 따라서 장바구니 하나에는 상품 종류는 하나만 가능(수량은 별개)
- 3. 사용자가 장바구니에서 선택해 주문 -> 주문 테이블(Orders) 생성.
- 주문 테이블은 장바구니가 아닌 상품 관리 테이블 참조
- 서비스 메서드에서 주문할 장바구니 객체를 조회해 그 안에 담긴 상품 관리 객체를 반환해 주도록 함.
- 주문 테이블에 상품 정보 필요 -> 장바구니 객체를 가져와도 이후에 상품을 조회해야 함
- 때문에 엔티티를 담는 최초에 한 번 조회해 저장
- Orders와 ProductManagement는 ManyToMany(다대다, M:N) - 중간 엔티티로 관리
- 주문 테이블에는 결제에 필요한 정보만 담는다.
- 4. 주문 테이블의 정보로 결제하고, 결제가 완료되면 결제 내역(PaymentHistory) 테이블에 저장
- 결제 내역은 상품별로 저장(주문별 x) - 후에 리뷰 관리를 위해
장바구니 엔티티
@Entity
@Getter
@Setter
@Table(name = "cart")
public class Cart {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cart_id")
private Long cartId; // PK
@ManyToOne
@JoinColumn(name = "member_id", nullable = false)
private Member member; // 회원
@ManyToOne
@JoinColumn(name = "Product_Mgt_id", nullable = false)
private ProductManagement productManagement; // 상품
@Column(name = "quantity", nullable = false)
private Long quantity; // 수량
@Column(name = "price", nullable = false)
private Long price; // 가격
public Cart(Member member, ProductManagement productManagement, Long quantity, Long price) {
this.member = member;
this.productManagement = productManagement;
this.quantity = quantity;
this.price = price;
}
public Cart() {
}
public void setQuantity(Long quantity) {
this.quantity = quantity;
}
public void setPrice(Long price) {
this.price = price;
}
}
가격은 장바구니에 담긴 상품의 개수에 따른 가격이다. 2개 이상 같은 제품이 담길 경우 때문에 추가함
컨트롤러
@RestController
@RequestMapping("/api/v1/cart")
@RequiredArgsConstructor
public class CartController {
private final CartService cartService;
/**
* 장바구니 담기
* @param request
* @param productMgtId
* @return
*/
@PostMapping("/add/{productMgtId}")
public ResponseEntity<String> addCart(@Valid @RequestBody CartRequestDto request, @PathVariable Long productMgtId) {
Long createdId = cartService.addCart(request, productMgtId);
return ResponseEntity.ok("장바구니에 등록되었습니다. cart_id : " + createdId);
}
/**
* 내 장바구니 리스트
* @param memberId
* @return
*/
@GetMapping("/{memberId}")
public List<CartDto> getMyCarts(@PathVariable Long memberId) {
return cartService.allCarts(memberId);
}
}
선택한 상품의 id를 URL 로 받고, 개수와 유저 정보를 Body로 넣어 요청받도록 했다.
DTO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CartRequestDto {
private Long memberId;
private Long quantity;
public CartRequestDto(Cart cart) {
this(
cart.getMember().getId(),
cart.getQuantity()
);
}
}
서비스 메서드에서 사용할 Dto
서비스 로직
사용자 정보와 상품 정보를 가져온다. 그 후 상품 + 사용자 정보로 장바구니에 이미 존재하는지 먼저 찾는다.
동일 사용자가 같은 상품/옵션을 선택했다면 수량과 가격을 올리고, 같은 상품/다른 옵션 또는 다른 상품을 선택했다면 장바구니에 따로 담는다. 이를 위해 상품 옵션 정보를 담은 상품 관리 테이블을 만들었던 것.
@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class CartService {
private final CartRepository cartRepository;
public final ProductRepositoryV1 productRepository;
public final MemberRepositoryV1 memberRepository;
public final ProductManagementRepository productManagementRepository;
/**
* 장바구니 담기
* @param request
* @param productMgtId
* @return
*/
public Long addCart(CartRequestDto request, Long productMgtId) { // 0408 수정 productId -> productMgtId
// 회원 정보
Member member = memberRepository.findById(request.getMemberId())
.orElseThrow(() -> new NoSuchElementException(MEMBER_NOT_FOUND));
// 상품 정보
ProductManagement productMgt = productManagementRepository.findById(productMgtId)
.orElseThrow(() -> new NoSuchElementException(PRODUCT_NOT_FOUND + " productMgtId: " + productMgtId));
// 회원, 상품 정보로 장바구니 객체 가져오기 - 없다면 null 반환
Cart existingCart = cartRepository.findByProductManagementAndMember(productMgt, member).orElse(null);
if (existingCart != null) { // 객체가 있음 - 이미 담은 상품과 옵션인 경우 수량, 가격을 수정
existingCart.setQuantity(existingCart.getQuantity() + request.getQuantity()); // 현재 수량 + 담은 수량
existingCart.setPrice(existingCart.getPrice() + productMgt.getProduct().getPrice() * request.getQuantity()); // 현재 가격 + 담은 가격
cartRepository.save(existingCart);
return existingCart.getCartId();
} else { // 객체가 없음 - 담기지 않은 상품이라면 새로 저장
Long price = productMgt.getProduct().getPrice() * request.getQuantity();
Cart cart = new Cart(member, productMgt, request.getQuantity(), price);
cartRepository.save(cart);
return cart.getCartId();
}
}
}
조회는 간단하게 DTO로 변환해 리스트로 가져왔다. 참고하는 상품 필드 중 클라이언트에서 필요로 하는 것들이 있어 매핑 후 전달
// CartDto - 상품 정보에서 원하는 정보 가져오기
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CartDto {
private Long cartId;
private Long memberId;
private String memberEmail;
private String memberPhone;
private LocalDateTime memberJoinedAt;
private List<WishList> wishLists;
private Long productId;
private ProductType productType;
private String productName;
private String color;
private String size;
private Integer productPrice;
private String productInfo;
private Boolean isDiscount;
private Boolean isSoldOut;
private Long quantity;
private Long totalPrice;
public CartDto(Cart cart) {
this(
cart.getCartId(),
cart.getMember().getId(),
cart.getMember().getEmail(),
cart.getMember().getPhone(),
cart.getMember().getJoinedAt(),
cart.getMember().getWishLists(),
cart.getProductManagement().getProduct().getProductId(),
cart.getProductManagement().getProduct().getProductType(),
cart.getProductManagement().getProduct().getProductName(),
cart.getProductManagement().getColor().getColor(),
cart.getProductManagement().getSize().toString(),
cart.getProductManagement().getProduct().getPrice(),
cart.getProductManagement().getProduct().getProductInfo(),
cart.getProductManagement().getProduct().getIsDiscount(),
cart.getProductManagement().isSoldOut(),
cart.getQuantity(),
cart.getPrice()
);
}
public static CartDto fromEntity(Cart cart) {
return CartDto.builder()
.cartId(cart.getCartId())
.memberId(cart.getMember().getId())
.memberEmail(cart.getMember().getEmail())
.memberPhone(cart.getMember().getPhone())
.memberJoinedAt(cart.getMember().getJoinedAt())
.wishLists(cart.getMember().getWishLists())
.productId(cart.getProductManagement().getProduct().getProductId())
.productType(cart.getProductManagement().getProduct().getProductType())
.productName(cart.getProductManagement().getProduct().getProductName())
.color(cart.getProductManagement().getColor().getColor())
.size(cart.getProductManagement().getSize().toString())
.productPrice(cart.getProductManagement().getProduct().getPrice())
.productInfo(cart.getProductManagement().getProduct().getProductInfo())
.isDiscount(cart.getProductManagement().getProduct().getIsDiscount())
.isSoldOut(cart.getProductManagement().isSoldOut())
.quantity(cart.getQuantity())
.totalPrice(cart.getPrice())
.build();
}
}
/**
* 유저의 전체 장바구니 리스트 조회
* @return
*/
public List<CartDto> allCarts(Long memberId) {
List<Cart> carts = cartRepository.findByMemberId(memberId);
return carts.stream()
.map(CartDto::fromEntity)
.collect(Collectors.toList());
}
테스트
원하는 옵션을 선택해 장바구니에 담고,
장바구니 목록을 확인하면 잘 들어와 있는 것을 확인!
테이블에도 잘 저장된당.
장바구니 끝!
주문/결제 기능 구현하기
https://chaeyami.tistory.com/253
300x250
반응형
GitHub 댓글