Skip to content

Commit c0957fc

Browse files
[Test] Cart API Test (#240)
* feat #200: product 엔티티에 이미지 매핑, CartResponse에 상품이미지 주석해제 * feat #200: CartRepository에 ProductImage fetch join 추가로 N+1 쿼리 개선 - Cart → OptionValue → OptionGroup → Product → ProductImage 경로에 LEFT JOIN FETCH 추가 - 장바구니 조회 시 상품 이미지 지연로딩 제거로 성능 개선 * test #200: CartController 테스트 및 REST Docs 문서화 추가 - CartController CRUD API 테스트 코드 작성 (MockMvc 기반) - 장바구니 생성, 조회, 수정, 삭제 성공 케이스 검증 - Spring REST Docs 스니펫(cart-create, cart-get-list, cart-update, cart-delete) 생성 - TestConfig 적용으로 Mock 서비스 주입 - CartRepository fetch join 개선 반영 * refactor #200: Cart 생성 요청에서 memberId 제거 및 응답에 cartId 포함하도록 수정 - CartCreateRequest에서 memberId 필드 제거 (토큰 기반 회원 식별로 변경) - CartResponse에 cartId 포함하여 생성 후 식별 가능하도록 수정 - CartController 및 CartControllerTest 로직 수정 - REST Docs 스니펫 응답 필드(cartId) 반영 * Testconfig 수정 --------- Co-authored-by: isak-kang <wbsldj59@naver.com>
1 parent 05dfbef commit c0957fc

10 files changed

Lines changed: 268 additions & 56 deletions

File tree

src/docs/asciidoc/_cart.adoc

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
== 장바구니 (Cart) API
2+
3+
=== 1. 장바구니 추가
4+
`POST /carts`
5+
6+
요청 본문:
7+
include::{snippets}/cart-create/request-fields.adoc[]
8+
9+
응답:
10+
include::{snippets}/cart-create/http-response.adoc[]
11+
12+
---
13+
14+
=== 2. 장바구니 목록 조회
15+
`GET /carts`
16+
17+
응답 본문:
18+
include::{snippets}/cart-get-list/response-fields.adoc[]
19+
20+
응답:
21+
include::{snippets}/cart-get-list/http-response.adoc[]
22+
23+
---
24+
25+
=== 3. 장바구니 수량 수정
26+
`PATCH /carts/{cartId}`
27+
28+
경로 파라미터 (Path Parameters):
29+
include::{snippets}/cart-update/path-parameters.adoc[]
30+
31+
요청 본문:
32+
include::{snippets}/cart-update/request-fields.adoc[]
33+
34+
응답:
35+
include::{snippets}/cart-update/http-response.adoc[]
36+
37+
---
38+
39+
=== 4. 장바구니 삭제
40+
`DELETE /carts/{cartId}`
41+
42+
경로 파라미터 (Path Parameters):
43+
include::{snippets}/cart-delete/path-parameters.adoc[]
44+
45+
응답:
46+
include::{snippets}/cart-delete/http-response.adoc[]

src/docs/asciidoc/index.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ include::_owner_order.adoc[]
2323
// include::_product.adoc[]
2424
include::_product_image.adoc[]
2525

26+
include::_cart.adoc[]
27+

src/main/java/com/irum/come2us/domain/cart/application/service/CartService.java

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,86 +15,59 @@
1515
import java.util.UUID;
1616
import java.util.stream.Collectors;
1717
import lombok.RequiredArgsConstructor;
18-
import lombok.extern.slf4j.Slf4j;
1918
import org.springframework.stereotype.Service;
2019
import org.springframework.transaction.annotation.Transactional;
2120

2221
@Service
2322
@RequiredArgsConstructor
2423
@Transactional
25-
@Slf4j
2624
public class CartService {
2725

2826
private final CartRepository cartRepository;
2927
private final ProductOptionValueRepository productOptionValueRepository;
3028
private final MemberUtil memberUtil;
3129

30+
@Transactional
3231
public CartResponse createCart(CartCreateRequest request) {
3332
Member currentMember = memberUtil.getCurrentMember();
3433

35-
// 옵션값 검증
3634
ProductOptionValue optionValue =
3735
productOptionValueRepository
3836
.findById(request.optionValueId())
3937
.orElseThrow(
4038
() -> new CommonException(CartErrorCode.OPTION_VALUE_NOT_FOUND));
4139

42-
// 기존 동일 옵션 Cart 존재 시 수량 합산
4340
Cart existing =
4441
cartRepository.findByMemberIdAndOptionValueId(
4542
currentMember.getMemberId(), request.optionValueId());
4643

47-
Cart saved;
44+
Cart target;
4845
if (existing != null) {
49-
int oldQuantity = existing.getQuantity();
50-
int updatedQuantity = oldQuantity + request.quantity();
46+
int updatedQuantity = existing.getQuantity() + request.quantity();
5147
existing.updateQuantity(updatedQuantity);
52-
saved = existing;
53-
54-
log.info(
55-
"장바구니 수량 합산: memberId={}, optionValueId={}, oldQuantity={}, newQuantity={}",
56-
currentMember.getMemberId(),
57-
request.optionValueId(),
58-
oldQuantity,
59-
updatedQuantity);
48+
target = existing;
6049
} else {
61-
Cart newCart = Cart.createCart(currentMember, optionValue, request.quantity());
62-
saved = cartRepository.save(newCart);
63-
64-
log.info(
65-
"장바구니 신규 추가: memberId={}, optionValueId={}, quantity={}",
66-
currentMember.getMemberId(),
67-
request.optionValueId(),
68-
request.quantity());
50+
target = Cart.createCart(currentMember, optionValue, request.quantity());
51+
cartRepository.save(target);
6952
}
7053

71-
return CartResponse.from(saved);
54+
return CartResponse.from(target);
7255
}
7356

74-
public CartResponse updateCart(UUID cartId, CartUpdateRequest request) {
57+
public void updateCart(UUID cartId, CartUpdateRequest request) {
7558
Cart cart =
7659
cartRepository
7760
.findById(cartId)
7861
.orElseThrow(() -> new CommonException(CartErrorCode.CART_NOT_FOUND));
7962

8063
memberUtil.assertMemberResourceAccess(cart.getMember());
81-
82-
if (cart.getQuantity().equals(request.quantity())) {
83-
throw new CommonException(CartErrorCode.CART_NOT_MODIFIED);
84-
}
85-
8664
cart.updateQuantity(request.quantity());
87-
log.info("장바구니 수정 완료: cartId={}, updatedQuantity={}", cartId, request.quantity());
88-
return CartResponse.from(cart);
8965
}
9066

9167
@Transactional(readOnly = true)
9268
public List<CartResponse> getCartListByMember() {
9369
Member currentMember = memberUtil.getCurrentMember();
94-
9570
List<Cart> carts = cartRepository.findAllWithProductByMemberId(currentMember.getMemberId());
96-
log.info("장바구니 조회 완료: memberId={}, count={}", currentMember.getMemberId(), carts.size());
97-
9871
return carts.stream().map(CartResponse::from).collect(Collectors.toList());
9972
}
10073

@@ -103,10 +76,7 @@ public void deleteCart(UUID cartId) {
10376
cartRepository
10477
.findById(cartId)
10578
.orElseThrow(() -> new CommonException(CartErrorCode.CART_NOT_FOUND));
106-
10779
memberUtil.assertMemberResourceAccess(cart.getMember());
10880
cartRepository.delete(cart);
109-
110-
log.info("장바구니 삭제 완료: cartId={}", cartId);
11181
}
11282
}

src/main/java/com/irum/come2us/domain/cart/domain/repository/CartRepository.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@ public interface CartRepository extends JpaRepository<Cart, UUID> {
2323
Cart findByMemberIdAndOptionValueId(
2424
@Param("memberId") @NotNull Long memberId, @Param("optionValueId") UUID optionValueId);
2525

26-
// 특정 회원의 장바구니 + 상품/옵션 통합 조회
27-
// TODO: 이미지 조인 추후 추가 예정
26+
/**
27+
* 특정 회원의 장바구니 + 상품/옵션/이미지 통합 조회 - Cart → OptionValue → OptionGroup → Product → ProductImage -
28+
* LEFT JOIN FETCH 로 대표 이미지까지 미리 로딩
29+
*/
2830
@Query(
2931
"""
3032
SELECT DISTINCT c
3133
FROM Cart c
3234
JOIN FETCH c.optionValue ov
3335
JOIN FETCH ov.optionGroup og
3436
JOIN FETCH og.product p
37+
LEFT JOIN FETCH p.productImages pi
3538
WHERE c.member.memberId = :memberId
3639
""")
3740
List<Cart> findAllWithProductByMemberId(@Param("memberId") Long memberId);

src/main/java/com/irum/come2us/domain/cart/presentation/controller/CartController.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import java.util.UUID;
1010
import lombok.RequiredArgsConstructor;
1111
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.http.HttpStatus;
13+
import org.springframework.http.ResponseEntity;
1214
import org.springframework.web.bind.annotation.*;
1315

1416
@RestController
@@ -20,30 +22,35 @@ public class CartController {
2022
private final CartService cartService;
2123

2224
@PostMapping
23-
public CartResponse createCart(@Valid @RequestBody CartCreateRequest request) {
25+
public ResponseEntity<CartResponse> createCart(@Valid @RequestBody CartCreateRequest request) {
2426
log.info(
2527
"장바구니 추가 요청: optionValueId={}, quantity={}",
2628
request.optionValueId(),
2729
request.quantity());
28-
return cartService.createCart(request);
30+
CartResponse response = cartService.createCart(request);
31+
return ResponseEntity.status(HttpStatus.CREATED).body(response);
2932
}
3033

3134
@PatchMapping("/{cartId}")
32-
public CartResponse updateCart(
35+
public ResponseEntity<Void> updateCart(
3336
@PathVariable UUID cartId, @Valid @RequestBody CartUpdateRequest request) {
3437
log.info("장바구니 수정 요청: cartId={}, quantity={}", cartId, request.quantity());
35-
return cartService.updateCart(cartId, request);
38+
cartService.updateCart(cartId, request);
39+
return ResponseEntity.noContent().build();
3640
}
3741

3842
@GetMapping
39-
public List<CartResponse> getMyCartList() {
40-
log.info("본인 장바구니 조회 요청");
41-
return cartService.getCartListByMember();
43+
public ResponseEntity<List<CartResponse>> getMyCartList() {
44+
log.info("장바구니 목록 조회 요청");
45+
List<CartResponse> responses = cartService.getCartListByMember();
46+
log.info("장바구니 조회 결과: {}개 항목", responses.size());
47+
return ResponseEntity.ok(responses);
4248
}
4349

4450
@DeleteMapping("/{cartId}")
45-
public void deleteCart(@PathVariable UUID cartId) {
51+
public ResponseEntity<Void> deleteCart(@PathVariable UUID cartId) {
4652
log.info("장바구니 삭제 요청: cartId={}", cartId);
4753
cartService.deleteCart(cartId);
54+
return ResponseEntity.noContent().build();
4855
}
4956
}

src/main/java/com/irum/come2us/domain/cart/presentation/dto/request/CartCreateRequest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,5 @@
55
import java.util.UUID;
66

77
public record CartCreateRequest(
8-
@NotNull(message = "회원 ID는 필수 입력값입니다.") Long memberId,
98
@NotNull(message = "옵션 값 ID는 필수 입력값입니다.") UUID optionValueId,
109
@Min(value = 1, message = "수량은 1개 이상이어야 합니다.") int quantity) {}

src/main/java/com/irum/come2us/domain/cart/presentation/dto/response/CartResponse.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public record CartResponse(
1010
UUID optionValueId,
1111
String productName,
1212
String optionValueName,
13-
// String imageUrl, // 상품 이미지 구현 후 주석 해제
13+
String imageUrl,
1414
int quantity,
1515
int basePrice, // 상품 기본가
1616
int extraPrice, // 옵션 추가금
@@ -26,17 +26,17 @@ public static CartResponse from(Cart cart) {
2626
int unit = base + extra;
2727
int total = unit * cart.getQuantity();
2828

29-
// String imageUrl = (product.getProductImages() != null &&
30-
// !product.getProductImages().isEmpty())
31-
// ? product.getProductImages().get(0).getImageUrl()
32-
// : null;
29+
String imageUrl =
30+
(product.getProductImages() != null && !product.getProductImages().isEmpty())
31+
? product.getProductImages().get(0).getImageUrl()
32+
: null;
3333

3434
return CartResponse.builder()
3535
.cartId(cart.getId())
3636
.optionValueId(optionValue.getId())
3737
.productName(product.getName())
3838
.optionValueName(optionValue.getName())
39-
// .imageUrl(imageUrl)
39+
.imageUrl(imageUrl)
4040
.quantity(cart.getQuantity())
4141
.basePrice(base)
4242
.extraPrice(extra)

src/main/java/com/irum/come2us/domain/product/domain/entity/Product.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,8 @@ public void updateCategory(Category category) {
132132
this.category = category;
133133
}
134134

135-
// TODO: 이미지 매핑, 리뷰 매핑
135+
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
136+
private List<ProductImage> productImages = new ArrayList<>();
137+
138+
// TODO: 리뷰 매핑
136139
}

0 commit comments

Comments
 (0)