Skip to content

Commit 9bdf24b

Browse files
authored
펀딩 장바구니 분리 (#346)
1 parent 0aeb250 commit 9bdf24b

File tree

6 files changed

+215
-74
lines changed

6 files changed

+215
-74
lines changed
Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,50 @@
11
package com.back.domain.cart.dto.request;
22

3+
import io.swagger.v3.oas.annotations.media.Schema;
34
import jakarta.validation.constraints.Min;
45
import jakarta.validation.constraints.NotNull;
56

67
/**
78
* 장바구니 추가/수정 요청 DTO
8-
* - 일반 장바구니: optionInfo 사용
9+
* - 일반 장바구니: productId, optionInfo 사용
910
* - 펀딩 장바구니: fundingId, fundingPrice, fundingStock 사용
1011
*/
12+
@Schema(description = "장바구니 추가 요청")
1113
public record CartRequestDto(
12-
@NotNull(message = "상품 ID는 필수입니다")
13-
Long productId, // 상품 ID
14+
@Schema(description = "상품 ID (일반 장바구니만 필수)", example = "123", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
15+
Long productId, // 상품 ID (일반 장바구니만 사용)
1416

1517
@NotNull(message = "수량은 필수입니다")
1618
@Min(value = 1, message = "수량은 1개 이상이어야 합니다")
19+
@Schema(description = "수량", example = "1", requiredMode = Schema.RequiredMode.REQUIRED)
1720
Integer quantity, // 수량
1821

22+
@Schema(description = "옵션 정보 (일반 장바구니만 사용)", example = "색상: 화이트", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
1923
String optionInfo, // 옵션 정보 (일반 장바구니만 사용)
2024

2125
@NotNull(message = "장바구니 타입은 필수입니다")
26+
@Schema(description = "장바구니 타입", example = "NORMAL", allowableValues = {"NORMAL", "FUNDING"}, requiredMode = Schema.RequiredMode.REQUIRED)
2227
String cartType, // "NORMAL" 또는 "FUNDING"
2328

2429
// 펀딩 장바구니 전용 필드
25-
String fundingId, // 펀딩 고유 ID (펀딩 장바구니만 사용)
30+
@Schema(description = "펀딩 ID (펀딩 장바구니만 필수)", example = "456", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
31+
Long fundingId, // 펀딩 ID (펀딩 장바구니만 사용)
32+
33+
@Schema(description = "펀딩 가격 (펀딩 장바구니만 사용)", example = "50000", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
2634
Integer fundingPrice, // 펀딩 단일 가격 (펀딩 장바구니만 사용)
35+
36+
@Schema(description = "펀딩 재고 (펀딩 장바구니만 사용)", example = "100", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
2737
Integer fundingStock // 펀딩 단일 재고 (펀딩 장바구니만 사용)
28-
) {}
38+
) {
39+
/**
40+
* 유효성 검증
41+
*/
42+
public void validate() {
43+
if ("NORMAL".equals(cartType) && productId == null) {
44+
throw new IllegalArgumentException("일반 장바구니는 productId가 필수입니다.");
45+
}
46+
if ("FUNDING".equals(cartType) && fundingId == null) {
47+
throw new IllegalArgumentException("펀딩 장바구니는 fundingId가 필수입니다.");
48+
}
49+
}
50+
}

src/main/java/com/back/domain/cart/entity/Cart.java

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ public class Cart extends BaseEntity {
2020
private User user;
2121

2222
@ManyToOne(fetch = FetchType.LAZY)
23-
@JoinColumn(name = "product_id", nullable = false)
24-
private Product product;
23+
@JoinColumn(name = "product_id", nullable = true)
24+
private Product product; // 일반 장바구니만 사용
25+
26+
@ManyToOne(fetch = FetchType.LAZY)
27+
@JoinColumn(name = "funding_id_ref", nullable = true)
28+
private com.back.domain.funding.entity.Funding funding; // 펀딩 장바구니만 사용
2529

2630
@Column(nullable = false)
2731
private Integer quantity;
@@ -57,18 +61,27 @@ public static CartType fromString(String cartTypeStr) {
5761
}
5862

5963
@Builder
60-
public Cart(User user, Product product, Integer quantity, String optionInfo,
61-
CartType cartType, Boolean isSelected, String fundingId,
62-
Integer fundingPrice, Integer fundingStock) {
64+
public Cart(User user, Product product, com.back.domain.funding.entity.Funding funding,
65+
Integer quantity, String optionInfo, CartType cartType, Boolean isSelected,
66+
String fundingId, Integer fundingPrice, Integer fundingStock) {
6367
this.user = user;
6468
this.product = product;
69+
this.funding = funding;
6570
this.quantity = quantity != null && quantity > 0 ? quantity : 1;
6671
this.optionInfo = optionInfo;
6772
this.cartType = cartType != null ? cartType : CartType.NORMAL;
6873
this.isSelected = isSelected != null ? isSelected : true;
6974
this.fundingId = fundingId;
7075
this.fundingPrice = fundingPrice;
7176
this.fundingStock = fundingStock;
77+
78+
// 검증: 타입에 맞는 참조가 있는지 확인
79+
if (this.cartType == CartType.NORMAL && this.product == null) {
80+
throw new IllegalArgumentException("일반 장바구니는 Product가 필수입니다.");
81+
}
82+
if (this.cartType == CartType.FUNDING && this.funding == null) {
83+
throw new IllegalArgumentException("펀딩 장바구니는 Funding이 필수입니다.");
84+
}
7285
}
7386

7487
// ===== 도메인 메서드 =====
@@ -112,41 +125,53 @@ public boolean isFundingCart() {
112125

113126
// 유효한 장바구니 아이템인지 확인
114127
public boolean isValid() {
115-
if (this.product == null) {
116-
return false;
117-
}
118-
119-
// 기본 검증: 상품이 삭제되지 않았고 재고가 있는지
120-
if (this.product.isDeleted() || this.product.getStock() <= 0) {
121-
return false;
122-
}
123-
124128
// 수량 검증
125129
if (this.quantity <= 0) {
126130
return false;
127131
}
128132

129-
// 펀딩 장바구니의 경우 펀딩 재고 확인
130133
if (isFundingCart()) {
131-
// fundingStock이 설정되어 있으면 그것을 기준으로, 없으면 product의 stock 사용
132-
int availableStock = (this.fundingStock != null) ? this.fundingStock : this.product.getStock();
134+
// 펀딩 장바구니 검증
135+
if (this.funding == null) {
136+
return false;
137+
}
138+
// fundingStock이 설정되어 있으면 그것을 기준으로, 없으면 funding의 stock 사용
139+
int availableStock = (this.fundingStock != null) ? this.fundingStock : this.funding.getStock();
133140
return this.quantity <= availableStock;
141+
} else {
142+
// 일반 장바구니 검증
143+
if (this.product == null) {
144+
return false;
145+
}
146+
147+
// 상품이 삭제되지 않았고 재고가 있는지
148+
if (this.product.isDeleted() || this.product.getStock() <= 0) {
149+
return false;
150+
}
151+
152+
// 일반 재고 확인
153+
return this.quantity <= this.product.getStock();
134154
}
135-
136-
// 일반 장바구니의 경우 일반 재고 확인
137-
return this.quantity <= this.product.getStock();
138155
}
139156

140157
// 총 가격 계산
141158
public int getTotalPrice() {
142-
if (this.product == null) {
143-
return 0;
144-
}
145-
146159
int unitPrice;
147-
if (isFundingCart() && fundingPrice != null) {
148-
unitPrice = fundingPrice;
160+
161+
if (isFundingCart()) {
162+
// 펀딩 장바구니: fundingPrice 사용
163+
if (this.fundingPrice != null) {
164+
unitPrice = this.fundingPrice;
165+
} else if (this.funding != null) {
166+
unitPrice = (int) this.funding.getPrice();
167+
} else {
168+
return 0;
169+
}
149170
} else {
171+
// 일반 장바구니: Product 가격 사용
172+
if (this.product == null) {
173+
return 0;
174+
}
150175
unitPrice = this.product.getDiscountPrice();
151176
}
152177

@@ -185,14 +210,32 @@ public boolean isOwnedBy(User user) {
185210

186211
/**
187212
* 상품 정보를 반환 (디미터의 법칙 적용)
213+
* 타입에 따라 Product 또는 Funding 정보 반환
188214
*/
189215
public ProductInfo getProductInfo() {
190-
return new ProductInfo(
191-
this.product.getId(),
192-
this.product.getName(),
193-
this.product.getPrice(),
194-
null // 이미지는 서비스에서 처리
195-
);
216+
if (isFundingCart() && this.funding != null) {
217+
// 펀딩 장바구니: Funding 정보 사용
218+
return new ProductInfo(
219+
this.funding.getId(),
220+
this.funding.getTitle(),
221+
this.fundingPrice != null ? this.fundingPrice : (int) this.funding.getPrice(),
222+
this.funding.getImageUrl() // Funding 이미지
223+
);
224+
} else if (this.product != null) {
225+
// 일반 장바구니: Product 정보 사용
226+
String imageUrl = null;
227+
if (this.product.getImages() != null && !this.product.getImages().isEmpty()) {
228+
imageUrl = this.product.getImages().get(0).getFileUrl();
229+
}
230+
return new ProductInfo(
231+
this.product.getId(),
232+
this.product.getName(),
233+
this.product.getPrice(),
234+
imageUrl // Product 첫 번째 이미지
235+
);
236+
}
237+
238+
throw new IllegalStateException("유효하지 않은 장바구니 상태입니다.");
196239
}
197240

198241
/**

src/main/java/com/back/domain/cart/repository/CartRepository.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
1616
public interface CartRepository extends JpaRepository<Cart, Long> {
1717

1818
/**
19-
* 사용자와 상품, 장바구니 타입으로 조회
19+
* 사용자와 상품, 장바구니 타입으로 조회 (일반 장바구니)
2020
*/
2121
Optional<Cart> findByUserAndProductAndCartType(User user, Product product, Cart.CartType cartType);
2222

23+
/**
24+
* 사용자와 펀딩, 장바구니 타입으로 조회 (펀딩 장바구니)
25+
*/
26+
Optional<Cart> findByUserAndFundingAndCartType(User user, com.back.domain.funding.entity.Funding funding, Cart.CartType cartType);
27+
2328
/**
2429
* 사용자의 모든 장바구니 삭제
2530
*/
@@ -33,7 +38,10 @@ public interface CartRepository extends JpaRepository<Cart, Long> {
3338
/**
3439
* 사용자의 장바구니 조회 (상품 정보 포함, N+1 문제 해결)
3540
*/
36-
@Query("SELECT c FROM Cart c JOIN FETCH c.product p WHERE c.user = :user")
41+
@Query("SELECT c FROM Cart c " +
42+
"LEFT JOIN FETCH c.product p " +
43+
"LEFT JOIN FETCH c.funding f " +
44+
"WHERE c.user = :user")
3745
List<Cart> findByUserWithProduct(@Param("user") User user);
3846

3947
/**
@@ -44,7 +52,10 @@ public interface CartRepository extends JpaRepository<Cart, Long> {
4452
/**
4553
* 선택된 장바구니 아이템 조회 - N+1 방지를 위한 Fetch Join
4654
*/
47-
@Query("SELECT c FROM Cart c JOIN FETCH c.product p WHERE c.user = :user AND c.isSelected = true")
55+
@Query("SELECT c FROM Cart c " +
56+
"LEFT JOIN FETCH c.product p " +
57+
"LEFT JOIN FETCH c.funding f " +
58+
"WHERE c.user = :user AND c.isSelected = true")
4859
List<Cart> findByUserAndIsSelectedTrueWithProduct(@Param("user") User user);
4960

5061
/**

src/main/java/com/back/domain/cart/service/CartService.java

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,52 +27,99 @@ public class CartService {
2727
private final CartRepository cartRepository;
2828
private final CartCalculator cartCalculator;
2929
private final ProductRepository productRepository;
30+
private final com.back.domain.funding.repository.FundingRepository fundingRepository;
3031

3132
/**
3233
* 장바구니에 상품 추가
3334
*/
3435
@Transactional
3536
public CartResponseDto addToCart(User user, CartRequestDto requestDto) {
36-
// 1. 상품 존재 확인
37+
// 1. 유효성 검증
38+
requestDto.validate();
39+
40+
// 2. 장바구니 타입 변환
41+
Cart.CartType cartType = Cart.CartType.fromString(requestDto.cartType());
42+
43+
if (cartType == Cart.CartType.NORMAL) {
44+
// 일반 장바구니 처리
45+
return addNormalCart(user, requestDto, cartType);
46+
} else {
47+
// 펀딩 장바구니 처리
48+
return addFundingCart(user, requestDto, cartType);
49+
}
50+
}
51+
52+
/**
53+
* 일반 장바구니 추가
54+
*/
55+
private CartResponseDto addNormalCart(User user, CartRequestDto requestDto, Cart.CartType cartType) {
56+
// 상품 존재 확인
3757
Product product = productRepository.findById(requestDto.productId())
3858
.orElseThrow(() -> new ServiceException("PRODUCT_NOT_FOUND", "존재하지 않는 상품입니다."));
39-
40-
// 2. 장바구니 타입 변환 (도메인에서 처리)
41-
Cart.CartType cartType = Cart.CartType.fromString(requestDto.cartType());
42-
43-
// 3. 중복 상품 확인 및 처리
59+
60+
// 중복 확인
4461
Optional<Cart> existingCart = cartRepository.findByUserAndProductAndCartType(user, product, cartType);
45-
62+
4663
if (existingCart.isPresent()) {
47-
// 기존 상품이 있으면 수량 증가
4864
Cart cart = existingCart.get();
4965
cart.changeQuantity(cart.getQuantity() + requestDto.quantity());
50-
51-
// 옵션 정보가 있으면 업데이트
66+
5267
if (requestDto.optionInfo() != null) {
5368
cart.changeOptionInfo(requestDto.optionInfo());
5469
}
55-
56-
Cart savedCart = cartRepository.save(cart);
57-
return CartResponseDto.from(savedCart);
70+
71+
return CartResponseDto.from(cartRepository.save(cart));
5872
}
59-
60-
// 4. 새로운 장바구니 아이템 생성 (중복이 없을 때만)
61-
// 펀딩 장바구니일 경우, 펀딩 정보를 requestDto에서 받아서 저장
73+
74+
// 새로운 장바구니 생성
6275
Cart newCart = Cart.builder()
6376
.user(user)
6477
.product(product)
78+
.funding(null)
6579
.quantity(requestDto.quantity())
66-
.optionInfo(cartType == Cart.CartType.NORMAL ? requestDto.optionInfo() : null) // 일반만 옵션 사용
80+
.optionInfo(requestDto.optionInfo())
6781
.cartType(cartType)
6882
.isSelected(true)
69-
.fundingId(cartType == Cart.CartType.FUNDING ? requestDto.fundingId() : null)
70-
.fundingPrice(cartType == Cart.CartType.FUNDING ? requestDto.fundingPrice() : null)
71-
.fundingStock(cartType == Cart.CartType.FUNDING ? requestDto.fundingStock() : null)
83+
.fundingId(null)
84+
.fundingPrice(null)
85+
.fundingStock(null)
7286
.build();
73-
74-
Cart savedCart = cartRepository.save(newCart);
75-
return CartResponseDto.from(savedCart);
87+
88+
return CartResponseDto.from(cartRepository.save(newCart));
89+
}
90+
91+
/**
92+
* 펀딩 장바구니 추가
93+
*/
94+
private CartResponseDto addFundingCart(User user, CartRequestDto requestDto, Cart.CartType cartType) {
95+
// 펀딩 존재 확인
96+
com.back.domain.funding.entity.Funding funding = fundingRepository.findById(requestDto.fundingId())
97+
.orElseThrow(() -> new ServiceException("FUNDING_NOT_FOUND", "존재하지 않는 펀딩입니다."));
98+
99+
// 중복 확인
100+
Optional<Cart> existingCart = cartRepository.findByUserAndFundingAndCartType(user, funding, cartType);
101+
102+
if (existingCart.isPresent()) {
103+
Cart cart = existingCart.get();
104+
cart.changeQuantity(cart.getQuantity() + requestDto.quantity());
105+
return CartResponseDto.from(cartRepository.save(cart));
106+
}
107+
108+
// 새로운 펀딩 장바구니 생성
109+
Cart newCart = Cart.builder()
110+
.user(user)
111+
.product(null)
112+
.funding(funding)
113+
.quantity(requestDto.quantity())
114+
.optionInfo(null)
115+
.cartType(cartType)
116+
.isSelected(true)
117+
.fundingId(requestDto.fundingId().toString())
118+
.fundingPrice(requestDto.fundingPrice() != null ? requestDto.fundingPrice() : (int) funding.getPrice())
119+
.fundingStock(requestDto.fundingStock() != null ? requestDto.fundingStock() : funding.getStock())
120+
.build();
121+
122+
return CartResponseDto.from(cartRepository.save(newCart));
76123
}
77124

78125
/**

0 commit comments

Comments
 (0)