Skip to content

Commit 60da1dd

Browse files
authored
리뷰 기능 구현 (#332)
1 parent 4d3cd22 commit 60da1dd

29 files changed

+2882
-103
lines changed

src/main/java/com/back/domain/cart/controller/CartController.java

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,56 @@ public ResponseEntity<RsData<CartResponseDto>> toggleSelection(
9494
}
9595

9696
@GetMapping("/selected")
97-
@Operation(summary = "선택된 장바구니 아이템 조회", description = "선택된 장바구니 아이템들만 조회합니다.")
98-
public ResponseEntity<RsData<List<CartResponseDto>>> getSelectedCartItems(@AuthenticationPrincipal User user) {
97+
@Operation(
98+
summary = "선택된 장바구니 아이템 조회",
99+
description = "선택된 장바구니 아이템들을 조회합니다. validateForOrder=true 시 유효한 아이템만 반환합니다."
100+
)
101+
public ResponseEntity<RsData<List<CartResponseDto>>> getSelectedCartItems(
102+
@AuthenticationPrincipal User user,
103+
@RequestParam(defaultValue = "false") boolean validateForOrder) {
104+
105+
List<CartResponseDto> responseDtos = cartService.getSelectedCartItems(user, validateForOrder);
106+
String message = validateForOrder ?
107+
"선택 주문 가능한 장바구니 아이템을 조회했습니다." :
108+
"선택된 장바구니 아이템을 조회했습니다.";
109+
return ResponseEntity.ok(RsData.of("200", message, responseDtos));
110+
}
111+
112+
@GetMapping("/all")
113+
@Operation(
114+
summary = "전체 장바구니 아이템 조회",
115+
description = "모든 장바구니 아이템을 조회합니다. validateForOrder=true 시 유효한 아이템만 반환합니다."
116+
)
117+
public ResponseEntity<RsData<List<CartResponseDto>>> getAllCartItems(
118+
@AuthenticationPrincipal User user,
119+
@RequestParam(defaultValue = "false") boolean validateForOrder) {
120+
121+
List<CartResponseDto> responseDtos = cartService.getAllCartItems(user, validateForOrder);
122+
String message = validateForOrder ?
123+
"전체 주문 가능한 장바구니 아이템을 조회했습니다." :
124+
"전체 장바구니 아이템을 조회했습니다.";
125+
return ResponseEntity.ok(RsData.of("200", message, responseDtos));
126+
}
127+
128+
@PostMapping("/validate")
129+
@Operation(summary = "장바구니 주문 가능 여부 검증", description = "전체 또는 선택된 장바구니 아이템들의 주문 가능 여부를 검증합니다.")
130+
public ResponseEntity<RsData<Void>> validateCartItemsForOrder(
131+
@AuthenticationPrincipal User user,
132+
@RequestParam(defaultValue = "false") boolean isFullOrder) {
133+
134+
cartService.validateCartItemsForOrder(user, isFullOrder);
135+
String message = isFullOrder ? "전체 주문 가능합니다." : "선택 주문 가능합니다.";
136+
return ResponseEntity.ok(RsData.of("200", message));
137+
}
138+
139+
@GetMapping("/total-amount")
140+
@Operation(summary = "장바구니 총 금액 계산", description = "전체 또는 선택된 장바구니 아이템들의 총 금액을 계산합니다.")
141+
public ResponseEntity<RsData<Integer>> calculateTotalAmount(
142+
@AuthenticationPrincipal User user,
143+
@RequestParam(defaultValue = "false") boolean isFullOrder) {
99144

100-
List<CartResponseDto> responseDtos = cartService.getSelectedCartItems(user);
101-
return ResponseEntity.ok(RsData.of("200", "선택된 장바구니 아이템을 조회했습니다.", responseDtos));
145+
Integer totalAmount = cartService.calculateTotalAmount(user, isFullOrder);
146+
String message = isFullOrder ? "전체 장바구니 총 금액" : "선택된 장바구니 총 금액";
147+
return ResponseEntity.ok(RsData.of("200", message, totalAmount));
102148
}
103149
}

src/main/java/com/back/domain/cart/dto/request/CartRequestDto.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
/**
77
* 장바구니 추가/수정 요청 DTO
8+
* - 일반 장바구니: optionInfo 사용
9+
* - 펀딩 장바구니: fundingId, fundingPrice, fundingStock 사용
810
*/
911
public record CartRequestDto(
1012
@NotNull(message = "상품 ID는 필수입니다")
@@ -14,8 +16,13 @@ public record CartRequestDto(
1416
@Min(value = 1, message = "수량은 1개 이상이어야 합니다")
1517
Integer quantity, // 수량
1618

17-
String optionInfo, // 옵션 정보 ("옵션 : 없음")
19+
String optionInfo, // 옵션 정보 (일반 장바구니만 사용)
1820

1921
@NotNull(message = "장바구니 타입은 필수입니다")
20-
String cartType // "NORMAL" 또는 "FUNDING"
22+
String cartType, // "NORMAL" 또는 "FUNDING"
23+
24+
// 펀딩 장바구니 전용 필드
25+
String fundingId, // 펀딩 고유 ID (펀딩 장바구니만 사용)
26+
Integer fundingPrice, // 펀딩 단일 가격 (펀딩 장바구니만 사용)
27+
Integer fundingStock // 펀딩 단일 재고 (펀딩 장바구니만 사용)
2128
) {}

src/main/java/com/back/domain/cart/dto/response/CartResponseDto.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,15 @@ public record CartResponseDto(
1414
String productImageUrl, // 상품 이미지 URL
1515
Integer price, // 상품 가격
1616
Integer quantity, // 수량
17-
String optionInfo, // 옵션 정보
17+
String optionInfo, // 옵션 정보 (일반 상품용)
1818
Boolean isSelected, // 선택 여부 (체크마크)
19-
String cartType, // 장바구니 타입
19+
String cartType, // 장바구니 타입 (NORMAL, FUNDING)
20+
21+
// 펀딩 전용 필드
22+
String fundingId, // 펀딩 고유 ID
23+
Integer fundingPrice, // 펀딩 단일 가격
24+
Integer fundingStock, // 펀딩 단일 재고
25+
2026
LocalDateTime createdAt // 생성일시
2127
) {
2228
/**
@@ -35,6 +41,12 @@ public static CartResponseDto from(Cart cart) {
3541
cart.getOptionInfo(),
3642
cart.isSelected(),
3743
cart.getCartType().name(),
44+
45+
// 펀딩 전용 필드
46+
cart.getFundingId(),
47+
cart.getFundingPrice(),
48+
cart.getFundingStock(),
49+
3850
cart.getCreateDate()
3951
);
4052
}

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

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,16 @@ public class Cart extends BaseEntity {
3333
@Column(nullable = false)
3434
private CartType cartType = CartType.NORMAL;
3535

36-
private String optionInfo;
36+
private String optionInfo; // 일반 상품의 경우에만 사용 (JSON 형태)
37+
38+
// 펀딩 상품 관련 필드 (펀딩 상품일 때만 사용)
39+
private String fundingId; // 펀딩 고유 ID
40+
private Integer fundingPrice; // 펀딩 단일 가격 (상품 가격과 동일할 수 있음)
41+
private Integer fundingStock; // 펀딩 단일 재고
3742

3843
public enum CartType {
3944
NORMAL, // 일반 장바구니
40-
FUNDING; // 펀딩 장바구니
45+
FUNDING; // 펀딩 장바구니
4146

4247
public static CartType fromString(String cartTypeStr) {
4348
if (cartTypeStr == null) {
@@ -53,13 +58,17 @@ public static CartType fromString(String cartTypeStr) {
5358

5459
@Builder
5560
public Cart(User user, Product product, Integer quantity, String optionInfo,
56-
CartType cartType, Boolean isSelected) {
61+
CartType cartType, Boolean isSelected, String fundingId,
62+
Integer fundingPrice, Integer fundingStock) {
5763
this.user = user;
5864
this.product = product;
5965
this.quantity = quantity != null && quantity > 0 ? quantity : 1;
6066
this.optionInfo = optionInfo;
6167
this.cartType = cartType != null ? cartType : CartType.NORMAL;
6268
this.isSelected = isSelected != null ? isSelected : true;
69+
this.fundingId = fundingId;
70+
this.fundingPrice = fundingPrice;
71+
this.fundingStock = fundingStock;
6372
}
6473

6574
// ===== 도메인 메서드 =====
@@ -82,6 +91,68 @@ public void changeOptionInfo(String optionInfo) {
8291
this.optionInfo = optionInfo;
8392
}
8493

94+
// 펀딩 상품 관련 메서드
95+
public void updateFundingInfo(String fundingId, Integer fundingPrice, Integer fundingStock) {
96+
if (this.cartType != CartType.FUNDING) {
97+
throw new IllegalArgumentException("펀딩 장바구니가 아닙니다.");
98+
}
99+
this.fundingId = fundingId;
100+
this.fundingPrice = fundingPrice;
101+
this.fundingStock = fundingStock;
102+
}
103+
104+
// 장바구니 타입 확인 메서드
105+
public boolean isNormalCart() {
106+
return this.cartType == CartType.NORMAL;
107+
}
108+
109+
public boolean isFundingCart() {
110+
return this.cartType == CartType.FUNDING;
111+
}
112+
113+
// 유효한 장바구니 아이템인지 확인
114+
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+
124+
// 수량 검증
125+
if (this.quantity <= 0) {
126+
return false;
127+
}
128+
129+
// 펀딩 장바구니의 경우 펀딩 재고 확인
130+
if (isFundingCart()) {
131+
// fundingStock이 설정되어 있으면 그것을 기준으로, 없으면 product의 stock 사용
132+
int availableStock = (this.fundingStock != null) ? this.fundingStock : this.product.getStock();
133+
return this.quantity <= availableStock;
134+
}
135+
136+
// 일반 장바구니의 경우 일반 재고 확인
137+
return this.quantity <= this.product.getStock();
138+
}
139+
140+
// 총 가격 계산
141+
public int getTotalPrice() {
142+
if (this.product == null) {
143+
return 0;
144+
}
145+
146+
int unitPrice;
147+
if (isFundingCart() && fundingPrice != null) {
148+
unitPrice = fundingPrice;
149+
} else {
150+
unitPrice = this.product.getDiscountPrice();
151+
}
152+
153+
return unitPrice * this.quantity;
154+
}
155+
85156
// ===== 도메인 메서드 (선택 상태 조회) =====
86157
public Boolean isSelected() {
87158
return this.isSelected;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ public interface CartRepository extends JpaRepository<Cart, Long> {
4040
* 선택된 장바구니 아이템 조회
4141
*/
4242
List<Cart> findByUserAndIsSelectedTrue(User user);
43+
44+
/**
45+
* 선택된 장바구니 아이템 조회 - N+1 방지를 위한 Fetch Join
46+
*/
47+
@Query("SELECT c FROM Cart c JOIN FETCH c.product p WHERE c.user = :user AND c.isSelected = true")
48+
List<Cart> findByUserAndIsSelectedTrueWithProduct(@Param("user") User user);
4349

4450
/**
4551
* 주문 완료 후 장바구니에서 제거 (UUID 기반)

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

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.List;
1818
import java.util.Optional;
1919
import java.util.stream.Collectors;
20+
import java.util.stream.Stream;
2021

2122
@Service
2223
@RequiredArgsConstructor
@@ -57,13 +58,17 @@ public CartResponseDto addToCart(User user, CartRequestDto requestDto) {
5758
}
5859

5960
// 4. 새로운 장바구니 아이템 생성 (중복이 없을 때만)
61+
// 펀딩 장바구니일 경우, 펀딩 정보를 requestDto에서 받아서 저장
6062
Cart newCart = Cart.builder()
6163
.user(user)
6264
.product(product)
6365
.quantity(requestDto.quantity())
64-
.optionInfo(requestDto.optionInfo())
66+
.optionInfo(cartType == Cart.CartType.NORMAL ? requestDto.optionInfo() : null) // 일반만 옵션 사용
6567
.cartType(cartType)
6668
.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)
6772
.build();
6873

6974
Cart savedCart = cartRepository.save(newCart);
@@ -168,15 +173,93 @@ public CartResponseDto toggleSelection(User user, Long cartId) {
168173
}
169174

170175
/**
171-
* 선택된 장바구니 아이템들만 조회
176+
* 선택된 장바구니 아이템 조회
177+
* @param user 사용자
178+
* @param validateForOrder 주문용 유효성 검증 여부 (true: 유효한 것만, false: 모두)
172179
*/
173-
public List<CartResponseDto> getSelectedCartItems(User user) {
174-
List<Cart> selectedCarts = cartRepository.findByUserAndIsSelectedTrue(user);
175-
return selectedCarts.stream()
180+
public List<CartResponseDto> getSelectedCartItems(User user, boolean validateForOrder) {
181+
// N+1 방지를 위해 Fetch Join 사용
182+
List<Cart> selectedCarts = cartRepository.findByUserAndIsSelectedTrueWithProduct(user);
183+
184+
Stream<Cart> stream = selectedCarts.stream();
185+
186+
// 주문용일 때만 유효성 검증
187+
if (validateForOrder) {
188+
stream = stream.filter(Cart::isValid);
189+
}
190+
191+
return stream
176192
.map(CartResponseDto::from)
177193
.collect(Collectors.toList());
178194
}
179195

196+
/**
197+
* 전체 장바구니 아이템 조회
198+
* @param user 사용자
199+
* @param validateForOrder 주문용 유효성 검증 여부 (true: 유효한 것만, false: 모두)
200+
*/
201+
public List<CartResponseDto> getAllCartItems(User user, boolean validateForOrder) {
202+
List<Cart> allCarts = cartRepository.findByUserWithProduct(user);
203+
204+
Stream<Cart> stream = allCarts.stream();
205+
206+
// 주문용일 때만 유효성 검증
207+
if (validateForOrder) {
208+
stream = stream.filter(Cart::isValid);
209+
}
210+
211+
return stream
212+
.map(CartResponseDto::from)
213+
.collect(Collectors.toList());
214+
}
215+
216+
/**
217+
* 주문 가능한 장바구니 아이템들 검증
218+
*/
219+
public void validateCartItemsForOrder(User user, boolean isFullOrder) {
220+
List<Cart> cartItems = isFullOrder ?
221+
cartRepository.findByUserWithProduct(user) :
222+
cartRepository.findByUserAndIsSelectedTrue(user);
223+
224+
if (cartItems.isEmpty()) {
225+
throw new ServiceException("CART_EMPTY", "주문할 장바구니 아이템이 없습니다.");
226+
}
227+
228+
// 각 아이템의 유효성 검증
229+
for (Cart cart : cartItems) {
230+
if (!cart.isValid()) {
231+
String productName = cart.getProduct() != null ? cart.getProduct().getName() : "알 수 없는 상품";
232+
throw new ServiceException("CART_INVALID",
233+
String.format("장바구니 아이템 '%s'이(가) 주문 불가능한 상태입니다.", productName));
234+
}
235+
}
236+
237+
// 펀딩 상품과 일반 상품이 섞여있는지 확인
238+
boolean hasNormalProducts = cartItems.stream()
239+
.anyMatch(cart -> cart.getCartType() == Cart.CartType.NORMAL);
240+
boolean hasFundingProducts = cartItems.stream()
241+
.anyMatch(cart -> cart.getCartType() == Cart.CartType.FUNDING);
242+
243+
if (hasNormalProducts && hasFundingProducts) {
244+
throw new ServiceException("CART_MIXED_TYPES",
245+
"일반 상품과 펀딩 상품은 함께 주문할 수 없습니다.");
246+
}
247+
}
248+
249+
/**
250+
* 장바구니 총 금액 계산 (전체/선택)
251+
*/
252+
public Integer calculateTotalAmount(User user, boolean isFullOrder) {
253+
List<Cart> cartItems = isFullOrder ?
254+
cartRepository.findByUserWithProduct(user) :
255+
cartRepository.findByUserAndIsSelectedTrue(user);
256+
257+
return cartItems.stream()
258+
.filter(Cart::isValid)
259+
.mapToInt(Cart::getTotalPrice)
260+
.sum();
261+
}
262+
180263
/**
181264
* 장바구니 조회 및 권한 확인
182265
*/

src/main/java/com/back/domain/order/order/controller/OrderController.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,17 +130,17 @@ public ResponseEntity<Void> approveOrderCancellation(
130130
}
131131

132132
/**
133-
* 주문 상태 변경 (관리자용)
133+
* 주문 상태 변경 (관리자 또는 작가용)
134134
*/
135135
@PatchMapping("/{orderId}/status")
136-
@PreAuthorize("hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_ROOT')")
137-
@Operation(summary = "주문 상태 변경", description = "관리자가 주문 상태를 변경합니다. (관리자 전용)")
136+
@PreAuthorize("hasAnyRole('ADMIN', 'ROOT', 'ARTIST')")
137+
@Operation(summary = "주문 상태 변경", description = "관리자 또는 작가가 주문 상태를 변경합니다. 작가는 자신의 상품 주문만 변경 가능합니다.")
138138
public ResponseEntity<Void> changeOrderStatus(
139139
@PathVariable Long orderId,
140140
@Valid @RequestBody OrderStatusChangeRequestDto requestDto,
141-
@AuthenticationPrincipal User admin
141+
@AuthenticationPrincipal User user
142142
) {
143-
orderService.changeOrderStatus(orderId, requestDto, admin);
143+
orderService.changeOrderStatus(orderId, requestDto, user);
144144
return ResponseEntity.ok().build();
145145
}
146146
}

0 commit comments

Comments
 (0)