Spring WebFlux를 이용하여 간단하게 상품 등록/삭제, 카트에 상품 담기/삭제 기능을 구현해보도록 하겠다.
1. 프로젝트 생성
2. 환경 구성
build.bradle 에 다음 dependency를 추가해준다.
implementation 'de.flapdoodle.embed:de.flapdoodle.embed.mongo:3.3.0'
implementation 'org.mongodb:mongodb-driver-sync'
testImplementation 'de.flapdoodle.embed:de.flapdoodle.embed.mongo:3.3.0'
프로젝트 구조는 다음과 같다.
3. 코드
Cart.java
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Cart {
@Id
private String id;
private List<CartItem> cartItems;
public Cart(String id) {
this(id, new ArrayList<CartItem>());
}
public void removeItem(CartItem cartItem) {
this.cartItems.remove(cartItem);
}
public Optional<CartItem> getCartItemByItemId(String itemId) {
return this.getCartItems().stream()
.filter(cartItem -> cartItem.getItem().getId().equals(itemId))
.findFirst();
}
}
Item.java
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Item {
@Id
private String id;
private String name;
private String origin;
private int price;
public Item(String name, String origin, int price) {
this.name = name;
this.origin = origin;
this.price = price;
}
}
CartItem.java
@Getter
@Setter
@ToString
@NoArgsConstructor
public class CartItem {
private Item item;
private int quantity;
public CartItem(Item item) {
this.item = item;
this.quantity = 1;
}
public void increment() {
this.quantity += 1;
}
public void decrement() {
this.quantity -= 1;
}
public boolean isOne() {
if (this.quantity == 1) {
return true;
}
return false;
}
}
CarReactiveRepository.java
public interface CartReactiveRepository extends ReactiveCrudRepository<Cart, String> {
}
ItemReactiveRepository.java
두 메서드는 테스트 코드에서 사용한 메서드라 작성하지 않아도 된다.
public interface ItemReactiveRepository extends ReactiveCrudRepository<Item, String>, ReactiveQueryByExampleExecutor<Item> {
Flux<Item> findByNameContaining(String itemName);
Mono<Item> findByName(String name);
}
ShopController.java
@Controller
@RequiredArgsConstructor
public class ShopController {
private final ItemReactiveRepository itemReactiveRepository;
private final CartReactiveRepository cartReactiveRepository;
private final CartService cartService;
@GetMapping("/")
public Mono<Rendering> home() {
return Mono.just(Rendering.view("shop")
.modelAttribute("items", itemReactiveRepository.findAll())
.modelAttribute("cart", cartReactiveRepository.findById("My Cart")
.defaultIfEmpty(new Cart("My Cart"))).build());
}
@GetMapping("/item")
public Mono<Rendering> itemRegisterPage() {
return Mono.just(Rendering.view("register").build());
}
@PostMapping("/item")
public Mono<String> registerItem(@RequestBody ItemRequestDto itemRequestDto) {
return cartService.registerItem(itemRequestDto)
.thenReturn("redirect:/");
}
@PostMapping("/cart/item/{id}")
public Mono<String> addCart(@PathVariable String id) {
return cartService.addToCart("My Cart", id)
.thenReturn("redirect:/");
}
@DeleteMapping("/cart/item/deleteAll/{id}")
public Mono<String> delToCartAll(@PathVariable String id) {
return cartService.deleteAllItemFromCart("My Cart", id)
.thenReturn("redirect:/");
}
@DeleteMapping("/cart/item/delete/{id}")
public Mono<String> delToCartItem(@PathVariable String id) {
return cartService.deleteOneItemFromCart("My Cart", id)
.thenReturn("redirect:/");
}
@DeleteMapping("/item/delete/{id}")
public Mono<String> deleteItem(@PathVariable String id) {
return itemReactiveRepository.deleteById(id)
.thenReturn("redirect:/");
}
}
CartService.java
public interface CartService {
public Mono<Item> registerItem(ItemRequestDto itemRequestDto);
public Mono<Cart> delToCartCount(String cartId, String id);
public Mono<Cart> delToCartAll(String cartId, String id);
public Mono<Cart> addToCart(String cartId, String id);
}
CartServiceImpl.java
생성한 CartService 인터페이스의 구현체인 CartServiceImpl을 생성했다.
@Service
@RequiredArgsConstructor
@Slf4j
public class CartServiceImpl implements CartService {
private final ItemReactiveRepository itemReactiveRepository;
private final CartReactiveRepository cartReactiveRepository;
@Override
public Mono<Item> registerItem(ItemRequestDto itemRequestDto) {
Item newItem = new Item(itemRequestDto.getName(), itemRequestDto.getOrigin(), itemRequestDto.getPrice());
return itemReactiveRepository.save(newItem);
}
@Override
public Mono<Cart> deleteOneItemFromCart(String cartId, String itemId) {
return cartReactiveRepository.findById(cartId)
.defaultIfEmpty(new Cart(cartId))
.flatMap(cart -> {
cart.getCartItemByItemId(itemId).ifPresent(cartItem -> {
if (cartItem.isOne()) {
//카트에 상품 수량이 1이면 상품 삭제
cart.removeItem(cartItem);
} else {
//카트에 상품 수량이 1 이상이면 수량 -1
cartItem.decrement();
}
});
return Mono.just(cart).log();
})
.flatMap(cart -> cartReactiveRepository.save(cart));
}
@Override
public Mono<Cart> deleteAllItemFromCart(String cartId, String itemId) {
return cartReactiveRepository.findById(cartId)
.defaultIfEmpty(new Cart(cartId))
.flatMap(cart -> {
//카트에 상품이 존재한다면 삭제
cart.getCartItemByItemId(itemId).ifPresent(cart::removeItem);
return Mono.just(cart).log();
})
.flatMap(cart -> cartReactiveRepository.save(cart));
}
@Override
public Mono<Cart> addToCart(String cartId, String itemId) {
return cartReactiveRepository.findById(cartId)
.defaultIfEmpty(new Cart(cartId))
.flatMap(cart -> cart.getCartItemByItemId(itemId)
.map(cartItem -> {
//카트에 상품이 있는 경우 개수만 +1
cartItem.increment();
return Mono.just(cart).log();
}).orElseGet(() -> itemReactiveRepository.findById(itemId)
//카트에 상품이 없는 경우 CartItem을 새로 생성해서 카트에 담기
.map(CartItem::new)
.doOnNext(cartItem -> cart.getCartItems().add(cartItem))
.map(cartItem -> cart))).flatMap(cart -> cartReactiveRepository.save(cart));
}
}
shop.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Shop Home</title>
</head>
<body>
<h2>Item List</h2>
<table border="1">
<thead>
<tr>
<td><b>상품번호</b></td>
<td><b>상품명</b></td>
<td><b>원산지</b></td>
<td><b>가격</b></td>
<td colspan="2"><b></b></td>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}">
<td th:text="${item.id}"></td>
<td th:text="${item.name}"></td>
<td th:text="${item.origin}"></td>
<td th:text="${item.price}"></td>
<td>
<form th:method="post" th:action="@{'/cart/item/' + ${item.id}}">
<input type="submit" value="담기">
</form>
</td>
<td>
<form th:method="delete" th:action="@{'/item/delete/' + ${item.id}}">
<input type="submit" value="삭제">
</form>
</td>
</tr>
</tbody>
</table>
<button style="background-color:blue; color:white; margin-top: 10px" onclick="window.location.href='/item'">
상품 등록
</button>
<h2>My Cart</h2>
<table border="1">
<thead>
<tr>
<td><b>상품번호</b></td>
<td><b>상품명</b></td>
<td><b>원산지</b></td>
<td><b>가격</b></td>
<td><b>수량</b></td>
<td colspan="2"><b></b></td>
</tr>
</thead>
<tbody>
<tr th:each="cartItem : ${cart.cartItems}">
<td th:text="${cartItem.item.id}"></td>
<td th:text="${cartItem.item.name}"></td>
<td th:text="${cartItem.item.origin}"></td>
<td th:text="${cartItem.item.price}"></td>
<td th:text="${cartItem.quantity}"></td>
<td>
<form th:method="delete" th:action="@{'/cart/item/delete/' + ${cartItem.item.id}}">
<input type="submit" value="단일 삭제">
</form>
</td>
<td>
<form th:method="delete" th:action="@{'/cart/item/deleteAll/' + ${cartItem.item.id}}">
<input type="submit" value="전체 삭제">
</form>
</td>
</tr>
</tbody>
</table>
</body>
</html>
register.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Item Register</title>
</head>
<body>
<h3>등록할 상품 정보를 입력해주세요.</h3>
<label for="name">상품명:</label>
<input type="text" id="name" name="name"><br><br>
<label for="origin">원산지:</label>
<input type="text" id="origin" name="origin"><br><br>
<label for="price">가격:</label>
<input type="number" id="price" name="price"><br><br>
<button style="background-color:blue; color:white;" onclick="register()">생성하기</button>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
function register() {
var name = $('#name').val();
var origin = $('#origin').val();
var price = $('#price').val();
$.ajax({
url: '/item',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
'name': name,
'origin': origin,
'price': price
}),
success: function(data) {
alert('상품을 등록하였습니다.');
window.location.href = '/';
},
error: function(data) {
alert('상품 등록에 실패하였습니다.');
}
});
}
</script>
</body>
</html>
728x90
'Spring' 카테고리의 다른 글
[Spring] Jacoco 적용하여 코드 커버리지 확인 및 Codecov사용한 PR에 커버리지 코멘트 추가 (0) | 2023.05.27 |
---|---|
[Spring] SpringBoot Prometheus, Grafana를 사용한 모니터링(Feat. Docker) (0) | 2023.05.25 |
[Spring] @RequestParam으로 Enum 타입 매개변수 받기 (0) | 2023.05.15 |
[Spring] @RequestPart 사용하여 MultipartFile과 Dto 함께 받기, Postman 테스트 (0) | 2023.05.14 |
[Spring] AWS S3 서비스를 사용하여 이미지 업로드 (0) | 2023.05.09 |